Entendendo como utilizar o Mocking nativo do test runner do node em testes unitários.
- Node@20.9.0
- sinon@15.2.0: para os fake timers.
- c8@8.0.1: para criar relatório de test coverage.
Para baixar as dependências e rodar o servidor:
npm ci --silent
npm start
Para rodar os testes automatizados: npm test
ou npm test:dev
Alguns truques que aprendi durante o projeto...
O módulo node:crypto
está sendo utilizado para gerar UUIDs dos TODOs. Esse pacote funciona dependendo do sistema operacional, se o SO cair ou estiver com algum defeito, então o pacote não funcionará como o esperado. Então, dentre dos testes, fazemos a sua substituição antes de todos os testes rodarem, e atribuiremos novamente o seu papel original.
// todoService.test.js
import crypto from "node:crypto";
// ...
before(() => {
const DEFAULT_ID = "0001";
crypto.randomUUID = () => DEFAULT_ID;
});
after(async () => {
crypto.randomUUID = (await import("node:crypto")).randomUUID;
});
Mas é mais fácil substituir o pacote por outros de terceiros.
O método deepStrictEqual
compara se os dados são iguais e do mesmo tipo. Objetos atribuídos como tipos diferentes darão erro. Um objeto Todo
não será igual ao objeto que contém todas as propriedades de Todo
.
{
error: {
+ data: Todo {
- data: {
id: '0001',
...
message: 'invalid data'
}
}
Para corrigir isso, adicionamos JSON.stringify()
para o resultado e o esperado, ao invés de assert.deepStrictEqual(result, expected)
, como a seguir:
// todoService.test.js
assert.deepStrictEqual(JSON.stringify(result), JSON.stringify(expected))
Outras alternativas seriam instalar o sinon.js ou o chai.
Sinon é uma biblioteca de testes dedicado em spies, stubs e mocks que contém Fake Timers, essencial para trabalharmos com datas em node. TODO
O comando node --experimental-test-coverage --test
faz o test coverage do projeto. Porém, é recomendado instalar pacotes de terceiros por ser uma funcionalidade experimental do node, como o c8@8.0.1
.
A técnica dependency inversion permite o desacoplar componentes um do outro.
Ao invés do service ou controller depender diretamente das implementações do repository, o que é algo ruim num ambiente de testes pois é necessário subir o banco de dados para testar:
flowchart BT
A[TodoService] --> B[TodoRepo]
Fazemos com que tanto o service quanto o repository dependam da interface do repositório:
flowchart BT
A[TodoService] --> B{{InterfaceTodoRepo}}
C[TodoRepo] --> B
Os principais ganhos são a testabilidade, pois não depende de subir um banco de dados para os testes, desacoplamento, substituibilidade (Liskov Substitution Principle), flexibilidade (Open Closed Principle) e inversão de controle (Dependency Inversion Principle).