Controllers

Os controllers serão responsáveis por receber as requisições das rotas, interagir com o Model quando necessário e retornar a resposta para o usuário. No nosso código atual, as rotas estão com muita responsabilidade e difíceis de testar isoladamente, pois dependemos do express. Para corrigir esse comportamento precisamos adicionar os controllers. Vamos criar os controllers guiados por testes de unidade, assim será possível validar o comportamento de forma separada do nosso sistema em si.

Configurando os testes de unidade

Como vimos no capítulo de testes de unidade, testes de unidade servem para testar pequenas partes do software isoladamente.

Para começar, crie um diretório chamado unit dentro do diretório test, na raiz do projeto. Assim como fizemos nos testes de integração, criaremos os arquivos de configuração para os testes. Vamos criar um arquivo chamado helpers.js dentro de unit, com o seguinte código:

1 import chai from 'chai';
2 
3 global.expect = chai.expect;

Em seguida, vamos criar o arquivo mocha.opts para as configurações do Mocha. Ele deve possuir o seguinte código:

1 --require @babel/register
2 --require test/unit/helpers.js
3 --reporter spec
4 --slow 5000

A última etapa de configuração dos testes de unidade será a criação de um comando para executar os testes. Vamos adicionar o seguinte script no package.json:

1 "test:unit": "NODE_ENV=test mocha --opts test/unit/mocha.opts test/unit/**/*_spec.js"

Para testar se o comando esta funcionando basta executar:

1 $ npm run test:unit

A saída do terminal deve informar que não conseguiu encontrar os arquivos de teste:

1 Warning: Could not find any test files matching pattern: test/unit/**/*_spec.js
2 No test files found

Vamos criar nosso primeiro teste de unidade para o nosso futuro controller de produtos. A separação de diretórios será semelhante a da aplicação com controllers, models e etc.

Criaremos um diretório chamado controllers dentro de unit e dentro dele um arquivo com o cenário de teste que vamos chamar de products_spec.js. Em seguida vamos executar os testes unitários novamente, a saída deve ser a seguinte:

1 0 passing (2ms)

Ok, nenhum teste está passando pois ainda não criamos nenhum.

Testando o controller unitariamente

Vamos começar a escrever o teste. O primeiro passo será adicionar a descrição desse cenário de testes, como no código a seguir:

1 describe('Controllers: Products', () => {
2 
3 });

Esse cenário irá englobar todos os testes do controller de products. Vamos criar cenários para cada um dos métodos, o próximo cenário terá o seguinte código:

1 describe('Controllers: Products', () => {
2 
3     describe('get() products', () => {
4 
5     });
6 
7 });

Precisamos agora criar nosso primeiro caso de teste para o método get. Começaremos nosso caso de teste descrevendo o seu comportamento:

1 describe('Controllers: Products', () => {
2 
3   describe('get() products', () => {
4     it('should return a list of products', () => {
5 
6     });
7   });
8 
9 });

Segundo a descrição do nosso teste, o método get deve retornar uma lista de produtos. Esse é o comportamento que iremos garantir que está sendo contemplado. Começaremos iniciando um novo controller como no código a seguir:

 1 import ProductsController from '../../../src/controllers/products';
 2 
 3 describe('Controllers: Products', () => {
 4 
 5   describe('get() products', () => {
 6     it('should return a list of products', () => {
 7 
 8       const productsController = new ProductsController();
 9 
10     });
11   });
12 });

Importamos o ProductsController do diretório onde ele deve ser criado e dentro do caso de teste inicializamos uma nova instância. Nesse momento se executarmos nossos testes de unidade devemos receber o seguinte erro:

1 Error: Cannot find module '../../../src/controllers/products'
2     at Function.Module._resolveFilename (module.js:455:15)
3     at Function.Module._load (module.js:403:25)
4 

A mensagem de erro explica que o módulo products não foi encontrado, como esperado. Vamos criar o nosso controller para que o teste passe. Vamos adicionar um diretório chamado controllers em src e dentro dele vamos criar o arquivo products.js, que será o controller para o recurso de products da API:

1 class ProductsController {
2 
3 }
4 
5 export default ProductsController;

Com o controller criado no diretório correto o nosso teste deve estar passando, vamos tentar novamente os testes unitários:

1 $ npm run test:unit

A saída do terminal deve ser a seguinte:

1   Controllers: Products
2     get() products
3       ✓ should return a list of products
4 
5 
6   1 passing (176ms)

Até o momento ainda não validamos o nosso comportamento esperado, apenas foi validado que o nosso controller existe. Agora precisamos garantir que o comportamento esperado no teste está sendo coberto, para isso precisamos testar se o método get chama a função de resposta do express. Antes de começar esse passo precisamos instalar o Sinon, uma biblioteca que irá nos ajudar a trabalhar com spies, stubs e mocks, os quais serão necessários para garantir o isolamento dos testes unitários.

Mocks, Stubs e Spies com Sinon.js

Para instalar o Sinon basta executar o seguinte comando:

1 $ npm install --save-dev sinon@^7.5.0

Após a instalação ele já estará disponível para ser utilizado em nossos testes. Voltando ao teste, vamos importar o Sinon e também usar um spy para verificar se o método get do controller está realizando o comportamento esperado. O código do teste deve ficar assim:

 1 import ProductsController from '../../../src/controllers/products';
 2 import sinon from 'sinon';
 3 
 4 describe('Controllers: Products', () => {
 5   const defaultProduct = [{
 6     name: 'Default product',
 7     description: 'product description',
 8     price: 100
 9   }];
10 
11   describe('get() products', () => {
12     it('should return a list of products', () => {
13       const request = {};
14       const response = {
15         send: sinon.spy()
16       };
17 
18       const productsController = new ProductsController();
19       productsController.get(request, response);
20 
21       expect(response.send.called).to.be.true;
22       expect(response.send.calledWith(defaultProduct)).to.be.true;
23     });
24   });
25 });

Muita coisa aconteceu nesse bloco de código, mas não se preocupe, vamos passar por cada uma das alterações.

A primeira adição foi o import do Sinon, módulo que instalamos anteriormente.

Logo após a descrição do nosso cenário de teste principal adicionamos uma constant chamada defaultProduct que armazena um array com um objeto referente a um produto com informações estáticas. Ele será útil para reaproveitarmos código nos casos de teste.

Dentro do caso de teste foram adicionadas duas constants: request, que é um objeto fake da requisição enviada pela rota do express, que vamos chamar de req na aplicação, e response, que é um objeto fake da resposta enviada pela rota do express, a qual vamos chamar de res na aplicação.

Note que a propriedade send do objeto response recebe um spy do Sinon, como vimos anteriormente, no capítulo de test doubles, os spies permitem gravar informações como quantas vezes uma função foi chamada, quais parâmetros ela recebeu e etc. O que será perfeito em nosso caso de uso pois precisamos validar que a função send do objeto response está sendo chamada com os devidos parâmetros.

Até aqui já temos a configuração necessária para reproduzir o comportamento que esperamos. O próximo passo é chamar o método get do controller passando os objetos request e response que criamos. E o último passo é verificar se o método get está chamando a função send com o defaultProduct como parâmetro. Para isso foram feitas duas asserções, a primeira verifica se a função send foi chamada, e a segunda verifica se ela foi chamada com o defaultProduct como parâmetro.

Nosso teste está pronto, se executarmos os testes unitários devemos receber o seguinte erro:

 1   Controllers: Products
 2     get() products
 3       1) should return a list of products
 4 
 5 
 6   0 passing (156ms)
 7   1 failing
 8 
 9   1) Controllers: Products get() products should return a list of products:
10      TypeError: productsController.get is not a function
11       at Context.it (test/unit/controllers/products_spec.js:19:26)

O erro explica que productsController.get não é uma função, então vamos adicionar essa função ao controller. A função get deverá possuir a lógica que agora está na rota de produtos. Vamos adicionar o método get no ProductsController, o código deve ficar assim:

 1 class ProductsController {
 2 
 3   get(req, res) {
 4     return res.send([{
 5       name: 'Default product',
 6       description: 'product description',
 7       price: 100
 8     }])
 9   }
10 }
11 
12 export default ProductsController;

O método get deve receber os objetos de requisição e resposta e enviar um array com um produto estático como resposta.

Vamos executar os testes novamente, a saída do terminal deve ser a seguinte:

1   Controllers: Products
2     get() products
3       ✓ should return a list of products
4 
5 
6   1 passing (189ms)

Integrando controllers e rotas

Nosso controller está feito e estamos obtendo o comportamento esperado, mas até então não integramos com a aplicação. Para realizar essa integração basta alterar a rota de produtos para usar o controller. Edite o arquivo products.js em src/routes, removendo o bloco de código que foi movido para o controller, e adicione a chamada para o método get. A rota de produtos deve ficar assim:

1 import express from 'express';
2 import ProductsController from '../controllers/products';
3 
4 const router = express.Router();
5 const productsController = new ProductsController();
6 router.get('/', (req, res) => productsController.get(req, res));
7 
8 export default router;

Vamos executar os testes de integração para garantir que o controller foi integrado corretamente com o resto da nossa aplicação.

1 $ npm run test:integration

A saída do terminal deve ser a seguinte:

1   Routes: Products
2     GET /products
3       ✓ should return a list of products
4 
5 
6   1 passing (251ms)

Os código desta etapa esta disponível aqui