Configurando testes de integração
Iremos testar de fora para dentro, ou seja, começaremos pelos testes de integração e seguiremos para os testes de unidade.
Instalando Mocha, Chai e Supertest
Para começar vamos instalar as ferramentas de testes com o comando abaixo:
1 $ npm install --save-dev mocha@^6.2.2 chai@^4.2.0 supertest@^4.0.2
Vamos instalar três módulos:
- Mocha: módulo que ira executar as suites de teste.
- Chai: módulo usado para fazer asserções.
- Supertest: módulo usado para emular e abstrair requisições http.
Separando execução de configuração
Na sequência, será necessário alterar a estrutura de diretórios da nossa aplicação atual, criando um diretório chamado src, onde ficará o código fonte. Dentro dele iremos criar um arquivo chamado app.js que terá a responsabilidade de iniciar o express e carregar os middlewares. Ele ficará assim:
1 import express from 'express';
2 import bodyParser from 'body-parser';
3
4 const app = express();
5 app.use(bodyParser.json());
6
7 app.get('/', (req, res) => res.send('Hello World!'));
8
9 export default app;
Aqui copiamos o código do server.js e removemos a parte do app.listen, a qual iniciava a aplicação, e adicionamos o export default app para exportar o app como um módulo.
Agora precisamos alterar o server.js no diretório raiz para que ele utilize o app.js:
1 import app from './app';
2 const port = 3000;
3
4 app.listen(port, () => {
5 console.log(`app running on port ${port}`);
6 });
Note que agora separamos a responsabilidade de inicializar o express e carregar os middlewares da parte de iniciar a aplicação em sí. Como nos testes a aplicação será inicializada pelo supertest e não pelo express como é feito no server.js, esse separação torna isso fácil.
Configurando os testes
Agora que a aplicação está pronta para ser testada, vamos configurar os testes. O primeiro passo é criar o diretório test na raiz do projeto, e dentro dele o diretório onde ficarão os testes de integração, vamos chamar esse diretório de integration.
A estrutura de diretórios ficará assim:
1 ├── package.json
2 ├── src
3 │ └── app.js
4 │ └── server.js
5 └── test
6 │ └── integration
Dentro de integration vamos criar os arquivos de configuração para os testes de integração. O primeiro será referente as configurações do Mocha, vamos criar um arquivo chamado mocha.opts dentro do diretório integration com o seguinte código:
1 --require @babel/register
2 --require test/integration/helpers.js
3 --reporter spec
4 --slow 5000
No primeiro require configuramos o Mocha com Babel, ja o segundo será o arquivo referente as configurações de suporte para os testes, o qual criaremos a seguir. Na linha seguinte definimos qual será o reporter, nesse caso, o spec. Reporters definem o estilo da saída do teste no terminal.
E na última linha o slow referente a demora máxima que um caso de teste pode levar, como testes de integração tendem a depender de agentes externos como banco de dados e etc, é necessário ter um tempo maior de slow para eles.
O próximo arquivo que iremos criar nesse mesmo diretório é o helpers.js. Ele terá o seguinte código:
1 import supertest from 'supertest';
2 import chai from 'chai';
3 import app from '../../src/app.js';
4
5 global.app = app;
6 global.request = supertest(app);
7 global.expect = chai.expect;
O arquivo helpers.js é responsável por inicializar as configurações de testes que serão usadas em todos os testes de integração, removendo a necessidade de ter de realizar configurações em cada cenário de teste.
Primeiro importamos os módulos necessários para executar os testes de integração que são o supertest e o chai e também a nossa aplicação express que chamamos de app.
Depois definimos as globais usando global. Globais fazem parte do Mocha, tudo que for definido como global poderá ser acessado em qualquer teste sem a necessidade de ser importado.
No nosso arquivo helpers configuramos o app para ser global, ou seja, caso seja necessário usá-lo em um caso de teste basta chamá-lo diretamente. Também é definido um global chamado request, que é o supertest recebendo o express por parâmetro.
Lembram que falei da vantagem de separar a execução da aplicação da configuração do express? Agora o express pode ser executado por um emulador como o supertest.
E por último o expect do Chai que será utilizado para fazer as asserções nos casos de teste.
Criando o primeiro caso de teste
Com as configurações finalizadas agora nos resta criar nosso primeiro caso de teste. Vamos criar um diretório chamado routes dentro do diretório integration e nele vamos criar o arquivo products_spec.js que vai receber o teste referente as rotas do recurso products da nossa API.
A estrutura de diretórios deve estar assim:
1 ├── package.json
2 ├── src
3 │ └── app.js
4 │ └── server.js
5 └── test
6 │ └── integration
7 │ ├── helpers.js
8 │ ├── mocha.opts
9 │ └── routes
10 │ └── products_spec.js
Agora precisamos escrever nosso caso de teste, vamos começar com o seguinte código no arquivo products_spec.js:
1 describe('Routes: Products', () => {
2
3 });
O describe é uma global do Mocha usada para descrever suítes de testes que contém um ou mais casos de testes e/ou contém outras suítes de testes. Como esse é o describe que irá englobar todos os testes desse arquivo seu texto descreve a responsabilidade geral da suíte de testes que é testar a rota products.
Agora vamos adicionar um produto padrão para os nossos testes:
1 describe('Routes: Products', () => {
2 const defaultProduct = {
3 name: 'Default product',
4 description: 'product description',
5 price: 100
6 };
7 });
Como a maioria dos testes precisará de um produto, tanto para inserir quanto para verificar nas buscas, criamos uma constante chamada defaultProduct para ser reusada pelos casos de teste.
O próximo passo é descrever a nossa primeira suíte de testes:
1 describe('Routes: Products', () => {
2 const defaultProduct = {
3 name: 'Default product',
4 description: 'product description',
5 price: 100
6 };
7
8 describe('GET /products', () => {
9 it('should return a list of products', done => {
10
11 });
12 });
13 });
Adicionamos mais um describe para deixar claro que todas as suítes de teste dentro dele fazem parte do método http GET na rota /products. Isso facilita a legibilidade do teste e deixa a saída do terminal mais clara.
A função it também é uma global do Mocha e é responsável por descrever um caso de teste.
Descrições de casos de teste seguem um padrão declarativo, como no exemplo acima: “Isso deve retornar uma lista de produtos”.
Note que também é passado um parâmetro chamado done para o caso de teste, isso ocorre porque testes que executam funções assíncronas, como requisições http, precisam informar ao Mocha quando o teste finalizou e fazem isso chamando a função done.
Vejamos na implementação a seguir:
1 describe('Routes: Products', () => {
2 const defaultProduct = {
3 name: 'Default product',
4 description: 'product description',
5 price: 100
6 };
7
8 describe('GET /products', () => {
9 it('should return a list of products', done => {
10
11 request
12 .get('/products')
13 .end((err, res) => {
14 expect(res.body[0]).to.eql(defaultProduct);
15 done(err);
16 });
17 });
18 });
19 });
Na implementação do teste usamos o supertest que exportamos globalmente como request no helpers.js. O supertest nos permite fazer uma requisição http para uma determinada rota e verificar a sua resposta.
Quando a requisição terminar a função end será chamada pelo supertest e vai receber a resposta ou um erro, caso ocorra. No exemplo acima é verificado se o primeiro elemento da lista de produtos retornada é igual ao nosso defaultProduct.
O expect usado para fazer a asserção faz parte do Chai e foi exposto globalmente no helpers.js.
Para finalizar, notificamos o Mocha que o teste finalizou chamando a função done que recebe err como parâmetro, caso algum erro ocorra ele irá mostrar a mensagem de erro no terminal.
Executando os testes
Escrito nosso teste, vamos executá-lo. Para automatizar a execução vamos adicionar a seguinte linha no package.json dentro de scripts:
1 "test:integration": "NODE_ENV=test mocha --opts test/integration/mocha.opts test/int\
2 egration/**/*_spec.js"
Estamos adicionando uma variável de ambiente como test, que além de boa prática também nos será útil em seguida, e na sequência as configurações do Mocha.
Para executar os testes agora basta executar o seguinte comando no terminal, dentro do diretório root da aplicação:
1 $ npm run test:integration
A saída deve ser a seguinte:
1 Routes: Products
2 GET /products
3 1) should return a list of products
4
5 0 passing (172ms)
6 1 failing
7
8
9 1) Routes: Products GET /products should return a list of products:
10 Uncaught AssertionError: expected undefined to deeply equal { Object (name, descri\
11 ption, ...) }
Isso quer dizer que o teste está implementado corretamente, sem erros de sintaxe por exemplo, mas está falhando pois ainda não temos esse comportamento na aplicação. Essa é a etapa RED do TDD, conforme vimos anteriormente.
Fazendo os testes passarem
Escrevemos nossos testes e eles estão no estado RED, ou seja, implementados mas não estão passando. O próximo passo, seguindo o TDD, é o GREEN onde vamos implementar o mínimo para fazer o teste passar.
Para isso, precisamos implementar uma rota na nossa aplicação que suporte o método http GET e retorne uma lista com, no mínimo, um produto igual ao nosso defaultProduct do teste.
Vamos alterar o arquivo app.js e adicionar a seguinte rota:
1 app.get('/products', (req, res) => res.send([{
2 name: 'Default product',
3 description: 'product description',
4 price: 100
5 }]));
Como vimos no capítulo sobre os middlewares do express, os objetos de requisição (req) e resposta (res) são injetados automaticamente pelo express nas rotas. No caso acima usamos o método send do objeto de resposta para enviar uma lista com um produto como resposta da requisição, o que deve ser suficiente para que nosso teste passe.
Com as alterações o app.js deve estar assim:
1 import express from 'express';
2 import bodyParser from 'body-parser';
3
4 const app = express();
5 app.use(bodyParser.json());
6
7 app.get('/', (req, res) => res.send('Hello World!'));
8 app.get('/products', (req, res) => res.send([{
9 name: 'Default product',
10 description: 'product description',
11 price: 100
12 }]));
13
14 export default app;
Agora que já temos a implementação, podemos executar nosso teste novamente:
1 $ npm run test:integration
A saída deve ser de sucesso, como essa:
1 Routes: Products
2 GET /products
3 ✓ should return a list of products
4
5
6 1 passing (164ms)
Nosso teste está passando, e estamos no estado GREEN do TDD, ou seja, temos o teste e a implementação suficiente para ele passar. O próximo passo será o REFACTOR onde iremos configurar as rotas.
O código dessa etapa está disponível neste link.