Testes no Node.js com Jest
1. Introdução ao Jest e Configuração Inicial
Jest é um framework de testes desenvolvido pelo Facebook, amplamente utilizado no ecossistema JavaScript e React. Sua popularidade vem da facilidade de configuração, velocidade de execução e recursos integrados como mocking, cobertura de código e suporte nativo a async/await.
No contexto Node.js + React, Jest se destaca por funcionar perfeitamente tanto para testes de backend (APIs, serviços, lógica de negócios) quanto para testes de frontend (componentes React, hooks, utilitários).
Instalação e Configuração Básica
npm install --save-dev jest
Crie o arquivo jest.config.js na raiz do projeto:
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
clearMocks: true,
collectCoverageFrom: ['src/**/*.js', '!src/**/*.test.js']
};
Adicione os scripts no package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Estrutura de Pastas Recomendada
src/
__tests__/
unit/
math.test.js
integration/
api.test.js
utils/
math.js
routes/
users.js
2. Escrevendo Testes Unitários com Jest
A sintaxe fundamental do Jest é composta por describe, it/test e expect. Os matchers são funções que permitem verificar valores de diferentes formas.
// src/utils/math.js
function sum(a, b) {
return a + b;
}
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
async function fetchData() {
return Promise.resolve({ id: 1, name: 'John' });
}
module.exports = { sum, divide, fetchData };
// src/__tests__/unit/math.test.js
const { sum, divide, fetchData } = require('../utils/math');
describe('Math utilities', () => {
describe('sum', () => {
it('should add two positive numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(sum(-1, 5)).toBe(4);
});
});
describe('divide', () => {
it('should divide two numbers correctly', () => {
expect(divide(10, 2)).toBe(5);
});
it('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
describe('fetchData', () => {
it('should return user data asynchronously', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1, name: 'John' });
});
});
});
Matchers essenciais:
- toBe - comparação estrita (===)
- toEqual - comparação profunda de objetos
- toContain - verifica se array contém elemento
- toThrow - verifica se função lança exceção
3. Mocking e Isolamento de Dependências
Mocks são essenciais para isolar a unidade sendo testada de suas dependências externas como bancos de dados, APIs e serviços.
// src/services/userService.js
const database = require('./database');
async function getUser(id) {
const user = await database.findUser(id);
if (!user) throw new Error('User not found');
return user;
}
module.exports = { getUser };
// src/__tests__/unit/userService.test.js
const { getUser } = require('../services/userService');
const database = require('../services/database');
jest.mock('../services/database');
describe('User Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return user when found', async () => {
const mockUser = { id: 1, name: 'Alice' };
database.findUser.mockResolvedValue(mockUser);
const user = await getUser(1);
expect(user).toEqual(mockUser);
expect(database.findUser).toHaveBeenCalledWith(1);
});
it('should throw error when user not found', async () => {
database.findUser.mockResolvedValue(null);
await expect(getUser(999)).rejects.toThrow('User not found');
});
});
Usando jest.spyOn() para espiar métodos existentes:
const logger = require('../utils/logger');
test('should log error message', () => {
const spy = jest.spyOn(logger, 'error').mockImplementation(() => {});
// código que chama logger.error
expect(spy).toHaveBeenCalledWith('Erro crítico');
spy.mockRestore();
});
4. Testes em Rotas HTTP com Supertest
Supertest permite testar servidores HTTP sem precisar iniciá-los em uma porta real.
npm install --save-dev supertest
// src/routes/users.js
const express = require('express');
const router = express.Router();
router.get('/users/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (error) {
res.status(404).json({ error: error.message });
}
});
router.post('/users', async (req, res) => {
const newUser = await createUser(req.body);
res.status(201).json(newUser);
});
module.exports = router;
// src/__tests__/integration/users.test.js
const request = require('supertest');
const app = require('../../app');
jest.mock('../../services/database');
describe('Users API', () => {
describe('GET /users/:id', () => {
it('should return user when exists', async () => {
database.findUser.mockResolvedValue({ id: 1, name: 'Alice' });
const response = await request(app)
.get('/users/1')
.expect(200);
expect(response.body).toEqual({ id: 1, name: 'Alice' });
});
it('should return 404 when user not found', async () => {
database.findUser.mockResolvedValue(null);
const response = await request(app)
.get('/users/999')
.expect(404);
expect(response.body).toEqual({ error: 'User not found' });
});
});
describe('POST /users', () => {
it('should create user and return 201', async () => {
const newUser = { name: 'Bob', email: 'bob@test.com' };
database.createUser.mockResolvedValue({ id: 2, ...newUser });
const response = await request(app)
.post('/users')
.send(newUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Bob');
});
});
});
5. Testes com Banco de Dados e Operações Assíncronas
Para testar operações reais de banco de dados, use bancos em memória como SQLite ou MongoDB Memory Server.
npm install --save-dev mongodb-memory-server
// src/__tests__/integration/database.test.js
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await User.deleteMany({});
});
describe('User Model', () => {
it('should create and save user successfully', async () => {
const user = new User({ name: 'Alice', email: 'alice@test.com' });
const savedUser = await user.save();
expect(savedUser._id).toBeDefined();
expect(savedUser.name).toBe('Alice');
});
});
Usando timers falsos para testar operações com tempo:
jest.useFakeTimers();
it('should call callback after 1 second', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
6. Cobertura de Código e Relatórios
Ative o relatório de cobertura com a flag --coverage:
jest --coverage
Configure thresholds no jest.config.js:
module.exports = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
coverageReporters: ['html', 'lcov', 'text']
};
Isso garante que o pipeline falhe se a cobertura ficar abaixo de 80%.
7. Testes em Componentes React (com Jest e Testing Library)
npm install --save-dev @testing-library/react @testing-library/jest-dom
// src/components/Counter.jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
// src/__tests__/components/Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '../../components/Counter';
describe('Counter Component', () => {
it('should render initial count as 0', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('Count: 0');
});
it('should increment count when button is clicked', () => {
render(<Counter />);
const button = screen.getByText('Increment');
fireEvent.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('Count: 1');
});
});
Testando hooks customizados com renderHook:
import { renderHook, act } from '@testing-library/react';
import useCounter from '../../hooks/useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
8. Boas Práticas e Padrões Avançados
Padrão AAA (Arrange, Act, Assert)
test('should calculate discount correctly', () => {
// Arrange
const price = 100;
const discount = 0.1;
// Act
const finalPrice = calculateDiscount(price, discount);
// Assert
expect(finalPrice).toBe(90);
});
Testes Parametrizados
describe.each([
[1, 1, 2],
[-1, 1, 0],
[0, 0, 0],
[2.5, 3.5, 6]
])('sum(%i, %i)', (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(sum(a, b)).toBe(expected);
});
});
Snapshot Testing
test('should match snapshot', () => {
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
});
Para atualizar snapshots: jest --updateSnapshot
Referências
- Documentação Oficial do Jest — Guia completo de instalação, configuração e todas as funcionalidades do Jest
- Testing Library para React — Documentação oficial da React Testing Library com exemplos práticos
- Supertest no GitHub — Biblioteca para testar servidores HTTP Node.js com sintaxe fluente
- MongoDB Memory Server — Solução para testes com MongoDB em memória, sem dependências externas
- Jest Cheat Sheet — Folha de consulta rápida com todos os matchers e funcionalidades do Jest
- Testes Assíncronos com Jest — Tutorial em português sobre testes assíncronos e mocking no Jest