Jest Testes unitários e de integração. - ZeTheGreat/iotNodeJS GitHub Wiki

Jest- Como utilizar testes unitários e de integração.

Olá, bem nesta página vamos aprender como utilizar Jest numa aplicação node.js e algumas funcionalidades como: covarge, SQLlite, truncate e factories girl.

Plus: Bem minha maquina é um ubuntu e estou fazendo em meu visual studio code com os seguintes wigbets: GitLens, Jest Snippets, JS Refactor, jshint.

Setup

Bem para começarmos nosso aprendizado devemos escolher um projeto, eu escolherei este aqui.

Projeto

Bem o projeto baixado tem arquivos de funcionamento dentro do /src

Porém oque são esses arquivos fora de /src? Bem, aqui vai a explicação:

  • .env: Env de environmet, é um documento aonde esta guardado algumas informações do banco, como por exemplo HOST,USER,PASS e NAME. E também APP_SECRET para determinar a forma que a chave funcionara. Essas informações se encontram neste arquivo para mascarar os dados e facilitar a troca.

  • .env.test: Como explicado em cima a única mudança entre essas variáveis é que neste documento contem informações para realização do test da aplicação como, o DB_DIALECT que fala sobre qual dialeto o sequelizer tem que transformar o código js e muda o APP_SECRETE para a variável de assinatura test.

  • .gitignore: É um arquivo para quando ocorrer a passagem para o github os determinados arquivos tem que ser ignorados, como por exemplo o node_modules, que contem as dependências da aplicação. Ela não é enviada pois as dependências tem que ser atualizadas, então existe arquivos que vamos explicar mais a frente que cuidarão de baixar essa pasta atualizada de novo.

  • .jshintrc: É apenas um documento para avisar o jshint que a versão utilizada do ES é a 8.

  • .sequelizerc: Este arquivo serve para mapear aonde estarão os arquivos utilizados pelo sequelizer, que é uma das ferramentas que estamos utilizados para traduzir nosso js para algumas linguagens de banco.

  • jest.config.js: Este é o documento para estilizar o uso do Jest, e explicaremos algumas configurações dentro dele.

  • package-lock.json: Este é a lista de suas dependências feitas pelo npm.

  • package.json: Este é o json aonde se encontra as características do seu projeto como nome, versão, qual pagina index. Também se encontram os scripts que são os comandos que você através do cmd pode estar invocando. E por últimos as dependências tanto de desenvolvedor quanto as dependências de funcionamento.

  • yarn-error.log: Este documento serve para armazenar os erros que acontecem quando se roda o yarn.

  • yarn.lock: Este é a lista de suas dependências feitas pelo yarn.

Uma vez que entendemos o porque de cada arquivo, vamos para a pasta /src, que por sua vez tem os arquivos:

  • app.js: Este arquivo é aonde sera organizado na hora de subir o servidor, lembra no arquivos de configuração os .env.test .env, então neste app.js é aonde ele ira pegar as informações usando a biblioteca dotenv. Neste arquivo também é estanciado o express e depois nele nós usamos o express.json que ira tratar de toda parte de comunicação de chave, depois mandamos para o arquivo de rotas aonde dependendo da url, o usuário sera mandando para o lugar apropriado. Por final nós exportamos a função AppController para deixar acessível para outras arquivos.js.

  • routes.js: Dentro deste arquivo nós controlamos o acesso e para aonde ele deveria ir, no nosso caso estamos apenas vendo se o usuário existe dentro do banco e logo em seguida conferindo se ele está logado, para caso os dois sejam sim voltarmos para ele a resposta 200 ou seja de que esta tudo bem. Mais uma vez estamos exportando as routes para que outros arquivos.js as usem.

  • server.js: Neste arquivo nós apenas chamamos o arquivo app.js citado antes e colocamos o mesmo para ou escutar a porta que nós daremos a ele pelo cmd ao levantar a aplicação ou na porta padrão que é a 3000.

Agora vamos para as três pastas que são:

  • app

  • controllers

  • SessionController.js: Neste arquivo nós verificamos usando o protocolo de requisição.body, aonde teremos as informações email e password, usando essas informações comparamos com o banco e caso ele não acerte o usuário ou senha ele não ganhara o token do usuário.

  • middleware

  • aut.js: já neste arquivo nós estamos verificando se a chave dada ao logar está correta.

  • models

  • index.js: Este arquivo é gerado automaticamente pelo sequileze para gerenciar o sequileze dentro de sua aplicação.

  • User.js: Este arquivo é a classe usuário e suas funções, ou seja, seu formato e como ele ira gerar e autentificar suas chaves.

  • config

  • database.js: Este arquivo usa as variáveis do arquivo .env ou .env.test para criar, e acessar a database especificada.

  • database

  • migrations

  • 20190418115453-create-users.js: Este arquivo é JavaScript, porém tem sintaxe json que o sequalizer usa para criar o banco. No nosso caso vamos criar tanto em Postgres quanto em sqlite.

  • seeders

Dependencias

Vou falar aqui sobre algumas dependências importantes para o funcionamento de nossa aplicação.

-bcryptjs: Está extensão utilizaremos para fazer nossa encriptação.

-dotenv: Está usaremos para poder usar variáveis de ambiente através de chamadas guardando elas em um arquivo apenas.

-express: É uma extensão para facilitar a administração da aplicação com rota etc.

-factory-girl: É uma aplicação para automatizar a criação de objetos para testar as funcionalidades da aplicação.

-faker: Serve para gerar informações aleatórias e complementa a factory-girl.

-jshint: Já esta é boa para corrigir pequenos erros no seu código os deixando mais coerente.

-jsonwebtoken: Este é para usar as token em formato .json.

-pg: Está e para utilizar o Postgres o banco de dados.

-sequelize: Para usar um arquivo .js com um json no meio se transformar em sintaxe de banco de dados automaticamente.

-jest: Esta daqui é essencial hoje pois é com ela que nós vamos realizar os testes.

-nodemon: Serve para gerenciar o run do servidor em node.

-sequelize-cli: Está extensão é para gerar uma comand line interface do sequilize.

-sqlite3: Para que nos testes nós não utilizemos o banco de dados de verdade.

-supertest: Uma extensão para testes.

Test setup

Bem agora que temos uma visão sobre nosso aplicação vamos para o setup dos testes.

Primeiramente gostaria de levar sua atenção ao jest.config.js. Dentro do arquivo existem alguns comentários com no final ****, fui eu mesmo quem fiz essa parte do documento e ela serve para chamar a atenção de vocês, pois são partes importantes do setup e aqui estão eles:

// Stop running tests after `n` failures **** 
//Este parâmetro serve para parar o test depois do primeiro erro.
bail: true,


// Automatically clear mock calls and instances between every test ****
// já este serve para limpar os mocks logo após os testes
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test ****
// este é para uma extensão do teste chamada coverage que gera um arquivo depois da compilação
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected ****
// aqui para verificar qual parte deve ou não deve "!" ser testada.
collectCoverageFrom: ["src/**", "!src/database/migrations/**", "!src/server.js"],

// The directory where Jest should output its coverage files ****
// aqui é aonde esta a aplicação do coverage.
coverageDirectory: "__tests__/coverage",

// The test environment that will be used for testing ****
// aqui é para especificar em qual ambiente você esta trabalhando
testEnvironment: "node",

// The glob patterns Jest uses to detect test files ****
//aqui é para falar quais são os arquivos para testar e aonde eles estão.
testMatch: [
 "**/__tests__/**/*.test.js?(x)",
 ],

Outro arquivo que nós devemos nos atentar também é o package.json ,pois , nele nós realizamos dentro de scripts algumas configurações usando variáveis de ambiente como NODE_ENV mais atentamente nesta parte:

"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js --ignore __tests__",
"pretest": "NODE_ENV=test sequelize db:migrate",
"test": "NODE_ENV=test jest",
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all"
},

Aqui dentro nós setamos o start e dev, porem o importante vem em pretest, aonde nós damos a função para o sequelize construir dentro do sqlite o banco. O test que é a rodar o jest e por fim o posttest, que servira para limpar o sqlite depois que terminarmos os testes.

Agora vamos nos atentar no arquivo de configuração database.js que esta /src/config, e vamos verificar como é configurado o nosso banco.

module.exports = {
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
dialect: process.env.DB_DIALECT || 'postgres',
storage:'./__tests__/database.sqlite',
logging: false,
define: {
    timestamps: true,
    underscored: true,
    underscoredAll: true,
}
    
};

Aqui estamos usando as variáveis de ambiente para setar o host, username, password, database, dialect etc.. Estas variáveis estão alocadas dentro de .env e é por isso que naquele arquivo no futuro iremos mexer nele, lembre-se disso. Também existe a variavel storage, que fala aonde o sqlite sera armazenado e algumas configurações como logging, para tirar o log do banco, timestamps para armazenar o tempo de update e inser, undercored e underscoreall para ao passar o nome da classe ou coluna como por exemplo DataNascimento, ele ao ir para o banco de dados virar Data_nascimento.

Importante: Veja que no dialect estamos falando que temos ou process.env.DB_DIALECT ou o postgres, fazemos isso pois ao rodar como teste dentro de .env.test temos a variável DB_DIALECT sendo setada para sqlite que muda a qual banco se deve ser usado na aplicação.

Teste em sí

Bem agora que nós sabemos sobre como a aplicação funciona e porque no jest.cong.js as configurações estão deste jeito nós vamos para os testes.

Primeiro para organizar tudo vamos criar uma pasta chamada __tests__e dentro dela vamos separar os testes unitários dos de integração. Fazendo um parentese os testes unitários são aqueles que testam uma função pura, que não necessita de nenhuma outra função, já os de integração testa também as conexões que uma classe faz com dependências ou variáveis compartilhadas.

Importante: Nós não estamos utilizando a forma de programação TDD, ou seja temos tudo já programado e estamos testando, isso se deve ao fator de este documento ter apenas o propósito de ensinar a enxergar as necessidades de testes em uma aplicação.

Sabendo de tudo isso vamos criar duas pastas a integrations e a unit, para abrigar nossos testes. Vamos começar pela de /integration, dentro dela crie um arquivo chamado session.test.js!

Importante: Caso você não lembre, este foi o formato que nós descrevemos la no jest.conf.js. outro motivo desse formato é para na hora de pesquisar dentro das pastas você não trocar o nome dos arquivos.js.

Espera espera aiii, antes de começarmos a fazer os testes existe algumas ferramentas que podem nos ajudar ainda mais nessa caminhada, o primeiro sera o factory girls. Para utilizar esta extensão vamos criar um arquivo dentro de __tests__ chamado factories.js

const { factory }= require('factory-girl');
const { User } = require('../src/app/models');
const faker =require('faker');

factory.define('User', User, {
    name: faker.name.findName(),
    email: faker.internet.email(),
    password: faker.internet.password()
});

module.exports = factory;

Bem aqui nós estamos importando 2 extensões a factory-girl e o faker, e também estamos utilizando a classe usuário criado por nós. Então vamos mandar a extensão factory definir um usuário toda vez que chamada e esse factory ira criar um nome, email e password aleatórios usando o faker.

Outra coisa importante de fazer é uma pasta chamada /utils e dentro dela vamos criar um truncate.

const {sequelize} = require('../../src/app/models');

module.exports = () =>{
    return  Promise.all(Object.keys(sequelize.models).map(key =>{
    return sequelize.models[key].destroy({ truncate :true, force: true});
}));
};

Importante: Truncate é apenas um código para caso de algum erro aconteça dentro dos tests, a aplicação continuar a apagar o banco de dados, tendo em vista que aquele Postest dentro de scripts dentro do package.json citado antes nunca chegue acontecer devido ao erro.

Como ultima tarefa, vamos ao arquivo .env e vamos alterar as variáveis para a que nós estamos utilizando então preencha com suas informações.

Caso você não saiba vai aqui uma pequena explicação

  • DB_HOST: O host que segura sua aplicação, caso o b anco esteja na sua própria maquina apenas coloque 127.0.0.1

  • DB_USER: Quando se cria um banco de dados ele pede também para criar um usuário e no caso é o nome deste usuário que estou colocando aqui.

  • DB_PASS: É a senha que eu coloquei para entrar com o usuário a cima

  • DB_NAME: Dentro do DB você pode criar uma database, porém a padrão é postgres.

OK, Estamos pronto agora para começar nossa análise e testes!!!

Bem primeiro temos que analisar oque nossa aplicação faz!

  • Encriptar a senha

  • Autentificar usuário

  • Gerar um token

  • Autentificar token

Vamos fazer um por um agora

Encriptar a senha

Dentro da pasta /__tests__/unit/ criaremos um arquivo chamado user.test.js, nele iremos testar se o a aplicação encripta a senha

const {User}= require('../../src/app/models');
const bcrypt = require('bcryptjs');
const truncate = require('../utils/truncate');

Primeiro passo é importar nossas extensões para auxiliar no teste.

describe('User',() => {
   beforeEach(async () =>{
       await truncate();
   });

Segundo criaremos um describe com a string de autentificação. O describe é um método que fala para o test que vira uma bateria de testes it logo após. E o beforeEach é uma função para toda vez que rodar um teste antes, rodar o truncate que é o limpador do banco que criamos no passado.

    it('should encrypyt user password', async () =>{
       const user = await User.create({
           name:'roberto',
           email:"[email protected]",
           password:"123456",
        });


        const compareHash = await bcrypt.compare('123456',user.password_hash);

        expect(compareHash).toBe(true);
   });

E aqui temos o teste em sí, primeiro passo ele cria um usuário, logo após ele testa a comparação da senha criptografada com a criptografia da senha utilizada. Logo após isso ele roda o comando expect que seria traduzido mais ou menos assim, espero_que(comparação dos hash).seja(valor resultante).

O arquivo no total ficou assim:

const {User}= require('../../src/app/models');
const bcrypt = require('bcryptjs');
const truncate = require('../utils/truncate');

describe('User',() => {
   beforeEach(async () =>{
       await truncate();
   });

   it('should encrypyt user password', async () =>{
       const user = await User.create({
           name:'roberto',
           email:"[email protected]",
           password:"123456",
        });


        const compareHash = await bcrypt.compare('123456',user.password_hash);

        expect(compareHash).toBe(true);
   });
});

Autentificar usuário

Então temos que criar testes para essas funções, começaremos com a Autentificação das credenciais dentro do arquivo session.test.js criado no passado.

const request = require('supertest');

const app = require('../../src/app');
const factory = require('../factories');
const truncate = require('../utils/truncate');

Como antes, o primeiro passo é importar nossas extensões para auxiliar no teste.

describe('Authentication', ()=> {
   beforeEach(async() => {
       await truncate();
   });

Como antes estamos criando um describe e mandando antes de cada teste rodar o truncate

it('should authenticate with valid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });

        const response = await request(app)
        .post('/sessions').send({
            email:user.email,
            password:"123"
        });

        expect(response.status).toBe(200);


    });

Este é o nosso primeiro teste que ira verificar se o usuário consegue logar, perceba que estamos usando o factory para criar nosso usuário de forma aleatória e estamos setando a mão a senha. Logo após pedimos para a variável criada do supertest para segurar a respota que vem do request.post.send mandando como entrada o email e a senha.

E depois um comando que traduzido seria espero_que(resultado do request).seja.(resultado esperado).

    it('should not authenticate with invalid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:user.email,
             password:"1234"
         });
 
         expect(response.status).toBe(401);
    });

    it('should not authenticate with invalid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:"[email protected]",
             password:"123"
         });
 
         expect(response.status).toBe(401);
    });

Já neste dois tests estamos vendo se o test irá voltar a reposta 401, que seria a de acesso negado, pois estamos colocando credencias erradas.

Gerar um token

No mesmo arquivo session.test.js vamos fazer mais alguns tests

    it('should return jwt token when authenticated', async() =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:user.email,
             password:"123"
         });
 
         expect(response.body).toHaveProperty('token');
    });

Aqui estamos logando com sucesso usando as credencias corretas e verificando se depois de logar o usuário nos da uma token.

Autentificar token

Agora que sabemos que existe uma token vamos verificar como a autentificamos.

    it('should be able to acess private routes when authenticated',async() =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .get('/dashboard')
         .set('Authorization', `Bearer ${user.generateToken()}`);
 
         expect(response.status).toBe(200);
    });

Neste teste estamos vendo se com a token nós podemos correta nós podemos ter acesso a pagina.

    it("should not be able to access private routes without jwt token", async () => {
        const response = await request(app).get("/dashboard");
    
        expect(response.status).toBe(401);
      });
    
      it("should not be able to access private routes with invalid jwt token", async () => {
        const response = await request(app)
          .get("/dashboard")
          .set("Authorization", `Bearer 123123`);
    
        expect(response.status).toBe(401);
      });

Já aqui estamos verificando duas possibilidades, a primeira é se ele consegue acessar se não tem nenhuma token e o segundo verifica que se consegue entrar sem nenhuma chave.

o código inteiro ficaria:

const request = require('supertest');

const app = require('../../src/app');
const factory = require('../factories');
const truncate = require('../utils/truncate');

describe('Authentication', ()=> {
    beforeEach(async() => {
        await truncate();
    });

    it('should authenticate with valid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });

        const response = await request(app)
        .post('/sessions').send({
            email:user.email,
            password:"123"
        });

        expect(response.status).toBe(200);


    });

    it('should not authenticate with invalid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:user.email,
             password:"1234"
         });
 
         expect(response.status).toBe(401);
    });

    it('should not authenticate with invalid credentials', async () =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:"[email protected]",
             password:"123"
         });
 
         expect(response.status).toBe(401);
    });

    it('should return jwt token when authenticated', async() =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .post('/sessions').send({
             email:user.email,
             password:"123"
         });
 
         expect(response.body).toHaveProperty('token');
    });

    it('should be able to acess private routes when authenticated',async() =>{
        const user = await factory.create('User',{
            password:'123'
        });
         const response = await request(app)
         .get('/dashboard')
         .set('Authorization', `Bearer ${user.generateToken()}`);
 
         expect(response.status).toBe(200);
    });
    
    it("should not be able to access private routes without jwt token", async () => {
        const response = await request(app).get("/dashboard");
    
        expect(response.status).toBe(401);
      });
    
      it("should not be able to access private routes with invalid jwt token", async () => {
        const response = await request(app)
          .get("/dashboard")
          .set("Authorization", `Bearer 123123`);
    
        expect(response.status).toBe(401);
      });

});

UFAAAAAAAAAAAA, finalmente chegamos na parte de testar, vamos direto para pasta diretório do nosso projeto e daremos o comando yarn install ou npm install.

IMPORTANTE: Não de os dois comandos, apenas um dos dois, este projeto foi feito para suportar tanto npm quanto yarn, porem não os dois juntos.

Seja qual foi feita sua escolha, ao terminar o install de yarn test ou npm test.

Ao realizar o comando você irá perceber que todas os testes deram certo, isso se deve ao fato de nós termos feito tudo direitinho :3.

Agora como ultima funcionalidade que irei mostrar, você precisara olhar para sua pata __tests__, lá você encontrara agora uma pasta chamada coverage, dentro dela você encontrará uma pasta icov-report, aonde você ira acessar um arquivo chamado index.html.

Ele abrirá uma pagina com todas informações de seu teste, caso você perceba existe apenas um local que não testamos que é o index.js dentro de models. a linha em especifico é esta

db[modelName].associate(db);

Nós não a testamos devido ao fato de na nossa aplicação não fazemos nenhuma associação com banco de dados exterior, então não tem porque testar.

É isso, isso foi tudo pessoal, muito obrigado pela atenção até aqui!!

Referencia

link