O passo Refactor do TDD

Lembram que nosso teste de integração está no passo GREEN do TDD? Ou seja, está com lógica suficiente para passar mas não está com a implementação real. Agora que o controller já está completo, integrando com o model, é o melhor momento para refatorar o resto dos componentes fazendo a integração com o model e controller.

Integração entre rota, controller e model

Nesse passo vamos refatorar nossa rota de products para que ela consiga criar o controller corretamente, passando o model como dependência. Altere o arquivo src/routes/products.js para que ele fique como o código a seguir:

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

A única mudança é que a nova instância do controller recebe o model Product por parâmetro. A integração parece estar pronta, vamos executar os testes de integração:

1 $ npm run test:integration

A saida sera como a seguinte:

 1 Routes: Products
 2     GET /products
 3       1) should return a list of products
 4 
 5 
 6   0 passing (286ms)
 7   1 failing
 8 
 9   1) Routes: Products GET /products should return a list of products:
10      Uncaught AssertionError: expected undefined to deeply equal { Object (name, des\
11 cription, ...) }
12       at Test.request.get.end (test/integration/routes/products_spec.js:41:34)
13       at Test.assert (node_modules/supertest/lib/test.js:179:6)
14       at Server.assert (node_modules/supertest/lib/test.js:131:12)
15       at emitCloseNT (net.js:1549:8)
16       at _combinedTickCallback (internal/process/next_tick.js:71:11)
17       at process._tickCallback (internal/process/next_tick.js:98:9)

O teste falhou, e isso é esperado, pois agora utilizamos o MongoDB e vamos precisar criar um produto antes de executar o teste para que seja possível reproduzir o cenário que queremos.

Vamos adicionar o que precisamos no teste de integração da rota de productos, abra o arquivo test/integration/routes/products_spec.js. A primeira coisa é a resposta que esperamos do MongoDB. O MongoDB adiciona alguns campos aos documentos salvos que são _v, corresponde a versão do documento e _id que é o identificador único do documento, normalmente um uuid.v4.

 1 const defaultProduct = {
 2       name: 'Default product',
 3       description: 'product description',
 4       price: 100
 5     };
 6 +  const expectedProduct = {
 7 +    __v: 0,
 8 +    _id: '56cb91bdc3464f14678934ca',
 9 +    name: 'Default product',
10 +    description: 'product description',
11 +    price: 100
12 +  };
13 +

Logo abaixo do defaultProduct adicionamos uma constant chamada expectedProduct correspondente ao produto que esperamos ser criado pelo MongoDB. Agora já possuímos o produto que queremos salvar que é defaultProduct e também o que esperamos de resposta do MongoDB.

Como estamos testando a rota products que retorna todos os produtos, precisamos ter produtos no banco de dados para poder validar o comportamento. Para isso iremos utilizar o callback do Mocha chamado beforeEach, que significa: antes de cada. Esse callback é executado pelo Mocha antes de cada caso de teste, então ele é perfeito para nosso cenário onde precisamos ter um produto disponível no banco antes de executar o teste.

Logo abaixo do código anterior adicione o seguinte código:

1 +  beforeEach(async() => {
2 +    await Product.deleteMany();
3 +
4 +    const product = new Product(defaultProduct);
5 +    product._id = '56cb91bdc3464f14678934ca';
6 +    return await product.save();
7 +  });
8 +

O que o código acima faz, é criar um novo produto utilizando os dados da constant defaultProduct e atribuir a nova instância do produto a constant product. Na linha seguinte a propriedade product._id do objeto criado pelo mongoose é sobrescrita por um id estático que geramos. Por padrão o mongoose gera um uuid para cada novo documento, mas no caso do teste precisamos saber qual é o id do documento que estamos salvando para poder comparar dentro do caso de teste, se utilizarmos o uuid gerado pelo mongoose o teste nunca conseguirá comparar o mesmo id. Dessa maneira sobrescrevemos por um id gerado por nós mesmos. Existem vários sites na internet para gerar uuid, aqui no livro por exemplo foi utilizado este: uuid generator.

Após a atribuição do id retornamos uma Promise que remove todos os produtos do banco de dados e depois salva o produto que criamos.

O próximo passo é garantir que iremos deixar o terreno limpo após executar o teste. Quando criamos testes que criam dados em banco de dados, escrevem arquivos em disco, ou seja, testes que podem deixar rastros para outros testes devemos limpar todo o estado e garantir que após a execução do teste não terá nenhum vestígio para os próximos. Para isso vamos adicionar também o callback afterEach que significa: Depois de cada, para garantir que o MongoDB ficará limpo, ou seja, sem dados. Para isso adicione o seguinte código logo abaixo do anterior:

1 +  afterEach(async() => await Product.deleteMany());

O último passo é atualizar o caso de teste para que ele verifique o expectedProduct no lugar do defaultProduct:

 1    describe('GET /products', () => {
 2       it('should return a list of products', done => {
 3   
 4         request
 5         .get('/products')
 6         .end((err, res) => {
 7 -         expect(res.body[0]).to.eql(defaultProduct);
 8 +         expect(res.body).to.eql([expectedProduct]);
 9           done(err);
10         });
11       });

O código final do products_spec.js deve estar similar a este:

 1 import Product from '../../../src/models/product';
 2 
 3 describe('Routes: Products', () => {
 4   let request;
 5   let app;
 6 
 7   before(async () => {
 8     app = await setupApp();
 9     request = supertest(app);
10   });
11 
12   after(async () => await app.database.connection.close());
13 
14   const defaultProduct = {
15     name: 'Default product',
16     description: 'product description',
17     price: 100
18   };
19   const expectedProduct = {
20     __v: 0,
21     _id: '56cb91bdc3464f14678934ca',
22     name: 'Default product',
23     description: 'product description',
24     price: 100
25   };
26 
27   beforeEach(async() => {
28     await Product.deleteMany();
29 
30     const product = new Product(defaultProduct);
31     product._id = '56cb91bdc3464f14678934ca';
32     return await product.save();
33   });
34 
35   afterEach(async() => await Product.deleteMany());
36 
37   describe('GET /products', () => {
38     it('should return a list of products', done => {
39       request.get('/products').end((err, res) => {
40         expect(res.body).to.eql([expectedProduct]);
41         done(err);
42       });
43     });
44   });
45 });

Executando os testes de integração novamente:

1 $ npm run test:integration

Devemos ter a seguinte resposta:

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

Nosso ciclo de TDD nos testes de integração está completo, refatoramos e adicionamos o comportamento esperado. Esse padrão onde começamos por testes de integração, depois criamos componentes internos como fizemos com controllers e models e utilizamos o teste de integração para válidar todo o comportamento, é conhecido como outside-in, termo esse que falaremos a seguir.

O código deste capitulo está disponível aqui.