Escrevendo testes na prática
LidoNesta aula, você aprenderá a escrever testes unitários eficazes, usar mocks para isolar dependências, medir a cobertura de testes e testar código assíncrono, com exemplos práticos em JavaScript e boas práticas para testes de qualidade.
Escrever testes é uma habilidade essencial para qualquer desenvolvedor que queira entregar software confiável e fácil de manter. Nesta aula, vamos explorar na prática como escrever testes unitários, utilizar mocks para simular dependências, medir a cobertura de código e testar funções assíncronas. Você verá exemplos reais com JavaScript e Jest, um dos frameworks de teste mais populares, e aprenderá a aplicar esses conceitos em seus próprios projetos.
Testar não é apenas uma formalidade; é uma forma de documentar o comportamento esperado do código e de prevenir regressões. Ao dominar essas técnicas, você conseguirá criar suítes de teste robustas que aumentam a confiança no código e facilitam a colaboração em equipe.
Unitários
Testes unitários são a base da pirâmide de testes. Eles verificam o comportamento de uma unidade isolada de código, geralmente uma função ou método, de forma independente do restante do sistema. O objetivo é garantir que cada função faça exatamente o que se espera, com entradas e saídas previsíveis. Para escrever bons testes unitários, é importante seguir o padrão AAA (Arrange, Act, Assert): preparar os dados, executar a função e verificar o resultado.
Vamos considerar um exemplo simples: uma função que calcula o fatorial de um número. Escreveremos testes para os casos mais comuns, incluindo casos de borda, como 0 e 1, e números negativos.
// fatorial.js
function fatorial(n) {
if (n < 0) throw new Error('Número deve ser não negativo');
if (n === 0 || n === 1) return 1;
return n * fatorial(n - 1);
}
module.exports = fatorial;
// fatorial.test.js
const fatorial = require('./fatorial');
describe('Função fatorial', () => {
test('calcula fatorial de 0', () => {
expect(fatorial(0)).toBe(1);
});
test('calcula fatorial de 1', () => {
expect(fatorial(1)).toBe(1);
});
test('calcula fatorial de 5', () => {
expect(fatorial(5)).toBe(120);
});
test('lança erro para números negativos', () => {
expect(() => fatorial(-1)).toThrow('Número deve ser não negativo');
});
});
Nesse exemplo, usamos describe para agrupar testes relacionados e test para definir cada caso. O Jest fornece uma ampla variedade de matchers, como toBe para igualdade estrita e toThrow para verificar exceções. Ao rodar os testes com npm test, o Jest executa cada caso e reporta se passou ou falhou.
Mocks
Mocks são objetos simulados que imitam o comportamento de dependências reais. Eles são essenciais para isolar a unidade sob teste, evitando que fatores externos (como chamadas de API, acesso a banco de dados ou relógio do sistema) interfiram nos resultados. Com mocks, podemos controlar as respostas das dependências e verificar se elas foram chamadas corretamente.
O Jest oferece funções de mock (jest.fn()) e a capacidade de substituir módulos inteiros com jest.mock(). Vamos criar um exemplo em que uma função processUser depende de uma API externa para buscar dados de um usuário.
// api.js
async function fetchUser(id) {
const response = await fetch(`https://api.exemplo.com/users/${id}`);
return response.json();
}
module.exports = { fetchUser };
// processUser.js
const api = require('./api');
async function processUser(id) {
const user = await api.fetchUser(id);
return `Nome: ${user.name}, Email: ${user.email}`;
}
module.exports = { processUser };
// processUser.test.js
const { processUser } = require('./processUser');
const api = require('./api');
jest.mock('./api');
test('deve retornar uma string formatada com dados do usuário', async () => {
// Configura o mock para retornar um objeto fixo
api.fetchUser.mockResolvedValue({ name: 'Maria', email: 'maria@example.com' });
const result = await processUser(1);
expect(result).toBe('Nome: Maria, Email: maria@example.com');
// Verifica se fetchUser foi chamada com o argumento correto
expect(api.fetchUser).toHaveBeenCalledWith(1);
});
Nesse exemplo, jest.mock('./api') substitui o módulo api por versões mockadas automaticamente. Em seguida, usamos mockResolvedValue para definir o valor que a promessa deve resolver. Isso nos permite testar processUser sem fazer requisições reais, tornando o teste rápido e determinístico. Também verificamos que a dependência foi chamada com o argumento esperado usando toHaveBeenCalledWith.
Cobertura
Cobertura de testes é uma métrica que indica qual porcentagem do código está sendo exercitada pelos testes. Ela ajuda a identificar áreas não testadas e a avaliar a qualidade da suíte de testes. O Jest possui cobertura integrada e pode gerar relatórios detalhados, mostrando cobertura por arquivo, por função, por linha e por branch.
Para habilitar a cobertura, basta executar o Jest com a flag --coverage. Vamos ver um exemplo de relatório e como interpretá-lo.
npm test -- --coverage
O relatório exibirá algo como:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 80 | 50 | 75 | 80 |
fatorial.js | 100 | 100 | 100 | 100 |
api.js | 0 | 0 | 0 | 0 | 1-5
----------|---------|----------|---------|---------|-------------------
É importante lembrar que cobertura não é garantia de qualidade: ter 100% de cobertura não significa que os testes são bons. Porém, uma cobertura baixa indica lacunas que merecem atenção. Ferramentas de cobertura também podem ser integradas a pipelines de CI para garantir que a cobertura não caia abaixo de um limite definido.
Testando código assíncrono
JavaScript é assíncrono por natureza, e testar código assíncrono pode ser desafiador. O Jest oferece várias formas de lidar com isso: callbacks, promises e async/await. O mais recomendado é usar async/await nos testes, pois torna o código mais legível e evita problemas com callbacks aninhados.
Vamos criar um exemplo com uma função que busca dados de um usuário e usa async/await no teste.
// userService.js
const api = require('./api');
async function getUserInfo(id) {
const user = await api.fetchUser(id);
return user;
}
module.exports = { getUserInfo };
// userService.test.js
const { getUserInfo } = require('./userService');
const api = require('./api');
jest.mock('./api');
test('deve retornar dados do usuário', async () => {
const mockUser = { id: 1, name: 'João' };
api.fetchUser.mockResolvedValue(mockUser);
const user = await getUserInfo(1);
expect(user).toEqual(mockUser);
});
test('deve lidar com rejeição da promise', async () => {
api.fetchUser.mockRejectedValue(new Error('Falha na requisição'));
await expect(getUserInfo(1)).rejects.toThrow('Falha na requisição');
});
No primeiro teste, usamos async/await para esperar a resolução da promessa. No segundo, verificamos que a promessa é rejeitada com um erro específico usando .rejects.toThrow(). Também é possível testar com o padrão de promises usando .resolves e .rejects.
Outra situação comum é testar funções que usam setTimeout ou timers. O Jest permite usar jest.useFakeTimers() para controlar o tempo e avançar manualmente com jest.advanceTimersByTime(). Isso evita esperas reais e torna os testes mais rápidos.
// timer.js
function delay(callback, ms) {
setTimeout(callback, ms);
}
module.exports = { delay };
// timer.test.js
const { delay } = require('./timer');
jest.useFakeTimers();
test('deve chamar o callback após o tempo especificado', () => {
const callback = jest.fn();
delay(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
Boas práticas e observações finais
Escrever testes de qualidade requer prática e atenção a alguns princípios. Prefira testar comportamento em vez de implementação: isso torna os testes menos frágeis. Mantenha os testes independentes e isolados, evitando dependências entre eles. Use nomes descritivos para os testes, que expliquem o que está sendo verificado. E lembre-se de que mocks devem ser usados com moderação: se você mockar tudo, os testes podem não refletir a realidade.
Por fim, integre os testes ao seu fluxo de desenvolvimento, rodando-os frequentemente. Ferramentas como Jest oferecem modo watch, que executa os testes automaticamente a cada alteração, ajudando a detectar problemas rapidamente.
Referências
- Jest - Documentação oficial
- MDN - Módulos JavaScript
- MDN - Promises
- Jest - Mock Functions
- Jest - CLI Coverage
- Artigos sobre cobertura de testes
Exercícios
Escreva uma função
somaque receba dois números e retorne a soma. Crie testes unitários para essa função, incluindo casos com números negativos e zero.✓ Resposta:function soma(a, b) { return a + b; } // testes const soma = require('./soma'); test('soma números positivos', () => { expect(soma(2, 3)).toBe(5); }); test('soma com zero', () => { expect(soma(0, 5)).toBe(5); }); test('soma com negativos', () => { expect(soma(-1, -2)).toBe(-3); });Dada a função
getUserque faz uma chamada a uma API, escreva um teste usando mock para verificar se ela retorna o objeto esperado e se a API foi chamada com o ID correto.✓ Resposta:// getUser.js const api = require('./api'); async function getUser(id) { return api.fetchUser(id); } // getUser.test.js const { getUser } = require('./getUser'); const api = require('./api'); jest.mock('./api'); test('deve retornar o usuário e chamar a API com o ID', async () => { const mockUser = { id: 1, name: 'Ana' }; api.fetchUser.mockResolvedValue(mockUser); const user = await getUser(1); expect(user).toEqual(mockUser); expect(api.fetchUser).toHaveBeenCalledWith(1); });Execute o Jest com cobertura em um projeto simples e explique o que o relatório mostra. (Resposta em texto)
✓ Resposta:O relatório mostra a porcentagem de statements, branches, funções e linhas cobertas por arquivo. Por exemplo, se um arquivo tem 80% de cobertura de linhas, significa que 80% das linhas executáveis foram executadas durante os testes. Isso ajuda a identificar áreas não testadas.
Escreva um teste para uma função assíncrona que retorna uma promessa rejeitada. Use
async/awaite verifique a rejeição.✓ Resposta:// asyncFunc.js function asyncFunc() { return Promise.reject(new Error('Falha')); } // asyncFunc.test.js const asyncFunc = require('./asyncFunc'); test('deve rejeitar com erro', async () => { await expect(asyncFunc()).rejects.toThrow('Falha'); });Você tem uma função que usa
setTimeoutpara chamar um callback após 5 segundos. Escreva um teste usando timers falsos para verificar que o callback só é chamado após o tempo avançar.✓ Resposta:// delay.js function delay(callback, ms) { setTimeout(callback, ms); } // delay.test.js const { delay } = require('./delay'); jest.useFakeTimers(); test('callback só é chamado após o tempo', () => { const callback = jest.fn(); delay(callback, 5000); expect(callback).not.toHaveBeenCalled(); jest.advanceTimersByTime(5000); expect(callback).toHaveBeenCalledTimes(1); });