Como aumentar a cobertura dos testes de seu código com Sinon

Como aumentar a cobertura dos testes de seu código com Sinon

Esta postagem vai dar uma visão geral dos testes em nodejs e como integrar ao seu processo de desenvolvimento.

Para isso vamos utilizar um código que já foi desenvolvido e necessitamos apenas aumentar a cobertura por "caminhos" que o fluxo de teste normal não foi capaz de capturar, por ter uma necessidade de executar o código por um fluxo que seria de erro 500. Como esse é fluxo que é considerado de exceção é necessário fazer o uso de mocks ou stubs, afim de que seja possível reproduzir um erro na aplicação.

Explicando a aplicação

É uma aplicação desenvolvida em nodejs, que possui apenas um endpoint para fazer login, utilizando o método http POST. A aplicação já possui alguns testes, então o nosso objetivo é testar apenas o fluxo que não está testado. Que para o nosso caso serão os momentos em código que existe o erro 500.

Essa é uma visão de um diagrama de máquina de estado. Agora vamos ao código:

//login.controller.js
const postgres = require('../../lib/postgres');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

exports.hashPassword = (req, res, next) => {
    crypto.scrypt(req.body.password.toString(), 'salt', 256, (err, derivedKey) => {
        if (err) {
            return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not do login', param: req.params.id }] });
        }
        req.body.kdfResult = derivedKey.toString('hex');
        next();
    });
};

exports.lookupLogin = (req, res, next) => {
    const sql = 'SELECT e.employee_id, e.login FROM employee e WHERE e.login=$1 AND e.password = $2';
    postgres.query(sql, [req.body.login, req.body.kdfResult], (err, result) => {
        if (err) {
            return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not do login', param: req.params.id }] });
        }
        if (result.rows.length === 0) {
            return res.status(404).json({ errors: [{ location: req.path, msg: 'User or password does not match', param: req.params.id }] });
        }
        req.employee = result.rows[0];
        next();
    });
};

exports.logEmployee = (req, res) => {
    res.status(200).json({ token: 'Bearer ' + jwt.sign(req.employee, process.env.SECRET, { expiresIn: 1800 }) });
    res.end();
};

Agora vamos executar a cobertura, para verificar quais linhas não estão cobertas pelos testes de integração atuais. Temos a seguinte saída:

------------------------|---------|----------|---------|---------|------
File                    | % Stmts | % Branch | % Funcs | % Lines | Uncov 
------------------------|---------|----------|---------|---------|------
All files               |   95.08 |       70 |     100 |   96.67 |                   
 tokenAuth              |   92.86 |       50 |     100 |     100 |                   
  app.js                |   92.86 |       50 |     100 |     100 | 13                
 tokenAuth/lib          |     100 |      100 |     100 |     100 |                   
  postgres.js           |     100 |      100 |     100 |     100 |                   
 tokenAuth/server       |     100 |      100 |     100 |     100 |                   
  index.js              |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/contro|   90.48 |    66.67 |     100 |   90.48 |                   
  login.controller.js   |   90.48 |    66.67 |     100 |   90.48 | 8,19             
 tokenAuth/server/routes|     100 |      100 |     100 |     100 |                   
  login.route.js        |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/valida|     100 |      100 |     100 |     100 |                   
  login.validator.js    |     100 |      100 |     100 |     100 |                   
------------------------|---------|----------|---------|---------|------

Podemos ver que o nosso arquivo login.controller.js possui as linhas 13 e 24 que não estão cobertas, o nosso objetivo vai ser criar stubs e mocks para simular uma falha no momento em que estamos fazendo uma chamada ao nosso servidor.

Agora sim, temos por onde iniciar o nosso stub das funções.

Um stub é um pedaço de código usado para substituir alguma outra funcionalidade de programação. Um stub pode simular o comportamento de um código existente (como um procedimento em uma máquina remota; tais métodos são frequentemente chamados de mocks) ou ser um substituto temporário para código ainda a ser desenvolvido.

Para o nosso caso, estamos substituindo um código que nós não desenvolvemos, afim de forçar o nosso código a percorrer um caminho específico. Que é o caminho dos erros 500.

Sabendo disso, vamos ao código, desejamos criar um stub da função crypto.scrypt essa  função faz parte do pacote crypto que pertence ao runtime nodejs (não é necessário instalar uma lib externa). Nosso objetivo é o forçar o nosso código a percorrer o caminho do erro 500. Que acontece em minha implementação  do código da função hashPassword. Vamos então escrever os testes.

sinon = require('sinon');

it('Mock hash password error', (done) => {
  const mock_hash_password_error = {
        severity: 'ERROR',
        code: '42P01',
        detail: 'The value of "keylen" is out of range. It must be >= 0 && <= 2147483647.',
     };
  const stub = sinon.stub(crypto, "scrypt").yields(new Error(mock_hash_password_error));
    api.post('/login/')
         .send({
             "login": "admin",
             "password": "abc123"
         })
         .set('Accept', 'application/json; charset=utf-8')
         .expect(500)
         .end((err, res) => {
             if (err) throw err;
             stub.restore();
             expect(res.body).to.have.property('errors');
             expect(res.body.errors).to.be.an('array');
             expect(res.body.errors[0].msg).equal('Could not do login');
             done();
        });
 });

Após escrever estes testes a linha 8 deve estar coberta, agora fica faltando a cobertura da linha 19, neste caso como lá é utilizado await então vamos utilizar o método stub.reject para gerar o json que passamos basta fazer uma query em que o nome da tabela não existe, será retornado este erro, aí coloquei ali para representar melhor o erro de um caso real.

it('Check error on query database', (done) => {
    const mocked_error = {
         length: 107,
         severity: 'ERROR',
         code: '42P01',
         detail: 'mocked database error',
         position: '61',
         file: 'parse_relation.c',
         line: '1373',
         routine: 'parserOpenTable'
     };

    const stub = sinon.stub(pg, 'query').rejects(mocked_error);
        api.post('/login/')
            .send({
                "login": "admin",
                "password": "abc123"
            })
            .set('Accept', 'application/json; charset=utf-8')
            .expect(500)
            .end((err, res) => {
                if (err) throw err;
                stub.restore();
                expect(res.body).to.have.property('errors')
                expect(res.body.errors).to.be.an('array')
                expect(res.body.errors[0].msg).equal('Could not do login')
            done();
      });
});

Agora o nosso código já está cobrindo a linha 19.

Agora ao executar o testes de unidade agora teremos essa linha coberta:

--------------------|---------|----------|---------|---------|----------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncov 
--------------------|---------|----------|---------|---------|----------
All files           |   98.53 |     87.5 |     100 |     100 |                   
 tokenAuth          |   92.86 |       50 |     100 |     100 |                   
  app.js            |   92.86 |       50 |     100 |     100 | 13                
 tokenAuth/lib      |     100 |      100 |     100 |     100 |                   
  postgres.js       |     100 |      100 |     100 |     100 |                   
 tokenAuth/server   |     100 |      100 |     100 |     100 |                   
  index.js          |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/c |     100 |      100 |     100 |     100 |                   
  login.controller. |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/r |     100 |      100 |     100 |     100 |                   
  login.route.js    |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/s |     100 |      100 |     100 |     100 |                   
  login.service.js  |     100 |      100 |     100 |     100 |                   
 tokenAuth/server/v |     100 |      100 |     100 |     100 |                   
  login.validator.j |     100 |      100 |     100 |     100 |                   
--------------------|---------|----------|---------|---------|----------

Dessa forma podemos aumentar a cobertura de nosso código através do stub de funções externas, para este caso nós fizemos o stub de uma função fornecida pela biblioteca padrão, e outra função que é fornecida por uma lib de terceiros que fizemos a instalação via o npm (neste caso foi o a função query da lib pg). Uma versão completa desta implementação, com testes de cobertura e 100% cobertura está presente no GitHub