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.