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