Fullstack com Node.js, React e GraphQL - 3.1: Testes unitários de models
Fala aí, pessoal! Nesta segunda parte do terceiro post da série Fullstack com Node.js, React e GraphQL vamos adicionar testes unitários aos nossos modelos do Sequelize.
Introdução
Estava escrevendo a quarta parte da série, quando a Polícia do TDD bateu na minha porta, dizendo que ia cassar meu alvará de desenvolvedor porque estava escrevendo código sem testes!
Até agora a gente escreveu pouco código da aplicação de fato (a maior parte foi setup de ferramentas e bibliotecas, que confiamos que já foram testadas), mas esse pouco já uma parte importante, que são os modelos.
Os modelos poderia ser testados indiretamente, em testes de integração e end-do-end, mas para garantir uma melhor cobertura e ainda aprender algumas coisas novas (pelo menos pra mim), vamos adicionar testes unitários pra eles.
Ferramentas
Para os nossos testes de backend vamos usar Mocha, Chai e Istanbul. Vamos instalar as seguintes dependências:
npm i -D mocha chai chai-as-promised nyc cross-env
Mocha é o framework de testes e também o executável que vai rodar os testes e apresentar o resultado.
Chai é a biblioteca de asserções (o Mocha não inclui uma), que permite usar três sintaxes distintas:
const result = sum(1, 2)
// assert
assert.equal(result, 3)
// should
result.should.equal(3)
// expect
expect(result).to.equal(3)
Vamos usar aqui o formato expect, simplesmente por gosto pessoal (acho que fica melhor de ler).
Chai as Promised é uma extensão para o Chai, que adiciona asserções sobre Promises. Ex.:
// espera que a promise seja resolvida
expect(operacaoQueRetornaPromise()).to.be.fulfilled
// espera que a promise seja rejeitada
expect(operacaoQueRetornaPromise()).to.be.rejected
// pega uma valor depois que a promise for resolvida
expect(operacaoQueRetornaPromise()).to.eventually.equal(3)
nyc é o executável do Istanbul, que será usado para medir a cobertura dos nossos testes. (Sim, é NYC de New York City)
Por fim, o cross-env, que é um comando para setar variável de ambiente de forma multi-plataforma. Mais embaixo você vai ver porque precisamos disto.
Banco de testes
Nossos testes vão usar o banco de dados para salvar os modelos criados durante o teste. Precisamos então configurar o ambiente test no config/database.js, bem parecido com o banco de desenvolvimento:
test: {
username: 'mymoney',
password: secret.DATABASE_PASSWORD,
database: 'mymoney_test',
host: '127.0.0.1',
dialect: 'postgres',
logging: false
}
Ambos são bancos locais, com a diferença no nome do banco, mymoney_test
e na opção logging: false
, para desativar os logs de SQL no terminal e não poluir a saída do resultado de testes.
Para criar o banco, usamos:
npx cross-env NODE_ENV=test sequelize db:create
Aqui precisamos setar a variável de ambiente NODE_ENV
com o valor test
para o Sequelize saber que vamos atuar no banco de testes. Em sistemas Unix (Mac, Linux) o comando é NODE_ENV=test
e no Windows é set NODE_ENV=test
. Para abstrair essa diferença usamos o cross-env para setar da mesma maneira em qualquer sistema.
Scripts NPM
Vamos criar dois scripts NPM para facilitar a execução dos testes:
"test": "cross-env NODE_ENV=test mocha --recursive",
"test:coverage": "nyc --reporter=html --reporter=text npm run test",
O primeiro, test
, usa o mocha para rodar os testes e mostrar quais passaram e quais falharam.
O segundo, test:coverage
, usa o nyc para gerar o relatório de cobertura, tanto no terminal quanto em arquivos HTML detalhados.
Setup das ferramentas
Por padrão o Mocha espera que os testes estejam na pasta test. Vamos colocar nossos testes de models em test/models para organizar.
Vamos criar também a pasta test/support com arquivos auxiliares
No arquivo test/support/setup.js habilitamos a extensão chai-as-promised para não precisarmos fazer em todos os testes.
// test/support/setup.js
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)
Já no test/support/hooks.js temos códigos que serão executados antes e depois dos testes.
// test/support/hooks.js
const { sequelize } = require('../../src/models')
/*
antes de cada teste
usamos sync para limpar as tabelas
*/
beforeEach(async () => {
await sequelize.sync({ force: true })
})
/*
depois da execução de todos os testes
fechamos a conexão com o banco
*/
after(() => {
sequelize.close()
})
Precisamos ainda dizer para o nyc/istanbul excluir alguns arquivos das métricas de cobertura. Basta adicionar no package.json:
"nyc": {
"exclude": [
"config",
"src/models/index.js",
"test/**"
]
}
E por fim, adicionar no .gitignore alguns arquivos gerados pelo nyc que não precisamos comitar:
.nyc_output
coverage
Ufa! Chega de setup, vamos testar!
Escrita dos testes
Vou colocar aqui como exemplo os testes de um modelo inteiro. Os outros são muito parecidos.
const expect = require('chai').expect
const { Investment, Transaction } = require('../../src/models')
describe('Transaction', () => {
describe('attributes', () => {
it('should have amount and date', async () => {
const transaction = await Transaction.create({
amount: 1,
date: '2018-03-15',
})
expect(transaction.get('amount')).to.equal('1.00')
expect(transaction.get('date')).to.equal('2018-03-15')
})
})
describe('validations', () => {
it('should validate amount', () => {
const transaction = Transaction.build({ date: '2018-03-15' })
expect(transaction.validate()).to.be.rejected
})
it('should validate date', () => {
const transaction = Transaction.build({ amount: 1 })
expect(transaction.validate()).to.be.rejected
})
})
describe('relations', () => {
it('should belong to Investment', async () => {
const transaction = await Transaction.create(
{
amount: 1,
date: '2018-03-15',
Investment: { name: 'Inv' },
},
{ include: [Investment] }
)
expect(transaction.get('Investment').get('name')).to.equal('Inv')
})
})
})
Testamos basicamente 3 coisas: os atributos do modelo, as validações dos atributos e seus relacionamentos com outros modelos. (Veja os grupos describe
separando estes tipos de testes)
Para testar os atributos, simplesmente criamos uma instância do modelo passando dados de testes, usando create
, que salva o modelo no banco, e depois verificamos os valores.
Para testar as validações, tentamos criar o modelo com dados incorretos e esperamos que a execução do método validate
seja rejeitada (esta função retorna uma Promise).
Para testar os relacionamentos, criamos o modelo testado juntamente com outros modelos que se relacionam com ele. Depois tentamos acessar o modelo relacionado através do modelo principal. Ex:
expect(transaction.get('Investment').get('name')).to.equal('Inv')
Execução e resultado
Se rodarmos npm run test
veremos um resultado assim:
E se rodarmos npm run test:coverage
, ele mostra no terminal a porcentagem de cobertura de cada arquivo:
E também gera um HTML em coverage/index.html com os detalhes da cobertura, inclusive mostrando as linhas de código onde os testes passaram ou não.
Resultado final
O código do projeto até este ponto está em: https://github.com/doug2k1/my-money/tree/v2.1.0
No próximo capítulo
Na próxima parte vamos criar a interface administrativa usando Forest Admin. Stay tuned!
Feedbacks?
E aí, o que está achando até agora? Algo que precisa melhorar?
Tags: full-stack|mocha|nodejs