# Dominando Testes de API com Mocha e Chai: Um Guia Completo para Backend
No universo do desenvolvimento backend, a confiabilidade e a robustez das APIs são pilares fundamentais. Garantir que seus endpoints respondam corretamente, mesmo sob condições adversas, é uma tarefa que exige atenção e ferramentas adequadas. Este artigo mergulha no mundo dos testes automatizados, focando em como configurar e utilizar Mocha e Chai, duas bibliotecas poderosas e amplamente adotadas no ecossistema Node.js, para testar seus endpoints de API. Abordaremos também as estratégias essenciais de setup e teardown de ambientes de teste, garantindo que seus testes sejam isolados, repetíveis e confiáveis.
A Importância dos Testes de API
APIs são a espinha dorsal da comunicação entre diferentes sistemas e serviços. Uma API com falhas pode levar a inconsistências de dados, interrupções de serviço e uma experiência frustrante para o usuário final. Testes de API automatizados nos permitem:
- Detectar Bugs Precocemente: Identificar problemas na lógica de negócios, validação de entrada e tratamento de erros antes que cheguem em produção.
- Garantir a Consistência: Assegurar que as respostas da API permaneçam previsíveis e corretas ao longo do tempo, especialmente após refatorações ou novas implementações.
- Facilitar a Refatoração: Proporcionar uma rede de segurança que permite modificar o código com confiança, sabendo que os testes indicarão se algo foi quebrado.
- Documentar o Comportamento: Os testes servem como uma forma de documentação executável, demonstrando como a API deve se comportar.
Configurando o Ambiente de Teste: Mocha e Chai
Mocha é um framework de testes JavaScript flexível que roda em Node.js e no navegador, permitindo testes assíncronos e relatórios de teste detalhados. Chai é uma biblioteca de asserções (assertion library) que pode ser usada com Mocha, oferecendo uma sintaxe expressiva para verificar se os resultados dos seus testes atendem às expectativas.
Instalação
Primeiro, vamos inicializar um projeto Node.js (se ainda não tiver um) e instalar Mocha e Chai como dependências de desenvolvimento:
npm init -y
npm install --save-dev mocha chai @types/mocha @types/chai ts-node typescript
Precisaremos também configurar o TypeScript para nosso projeto. Crie um arquivo tsconfig.json na raiz do projeto com o seguinte conteúdo:
{
\"compilerOptions\": {
\"target\": \"ES2016\",
\"module\": \"CommonJS\",
\"outDir\": \"./dist\",
\"rootDir\": \"./src\",
\"strict\": true,
\"esModuleInterop\": true,
\"skipLibCheck\": true,
\"forceConsistentCasingInFileNames\": true,
\"moduleResolution\": \"node\",
\"types\": [\"mocha\", \"chai\"]
},
\"include\": [\"src/**/*.ts\"],
\"exclude\": [\"node_modules\"]
}
Agora, vamos criar um diretório src para nosso código e um subdiretório test para nossos testes.
Estrutura de Diretórios
my-api-project/
├── src/
│ └── server.ts # Seu código de servidor
├── test/
│ └── api.test.ts # Seus testes de API
├── tsconfig.json
├── package.json
└── node_modules/
Configurando Mocha
Você pode configurar Mocha através de um arquivo mocha.opts no diretório test/ ou diretamente no seu package.json. Para este guia, usaremos o package.json. Adicione o seguinte script:
// package.json
\"scripts\": {
\"test\": \"mocha -r ts-node/register src/test/**/*.ts"
}
Este script instrui o Mocha a usar ts-node para transpilar e executar os arquivos TypeScript na pasta src/test/.
Testando Endpoints da API
Vamos supor que temos um servidor Express simples com um endpoint /users que retorna uma lista de usuários.
Exemplo de Servidor (src/server.ts)
import express, { Request, Response } from 'express';
const app = express();
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
// Middleware para parsear JSON
app.use(express.json());
// Dados de exemplo
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
// Endpoint GET /users
app.get('/users', (req: Request, res: Response) => {
res.status(200).json(users);
});
// Endpoint POST /users
app.post('/users', (req: Request, res: Response) => {
const newUser = req.body;
if (!newUser.name || !newUser.email) {
return res.status(400).json({ message: 'Name and email are required' });
}
const id = users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1;
users.push({ ...newUser, id });
res.status(201).json({ ...newUser, id });
});
// Endpoint GET /users/:id
app.get('/users/:id', (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const user = users.find(u => u.id === id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
});
// Endpoint DELETE /users/:id
app.delete('/users/:id', (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const initialLength = users.length;
users = users.filter(u => u.id !== id);
if (users.length < initialLength) {
res.status(204).send(); // No Content
} else {
res.status(404).json({ message: 'User not found' });
}
});
const server = app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
export { app, server }; // Exporta app e server para testes
Exemplo de Testes (src/test/api.test.ts)
Para interagir com nossa API em testes, usaremos a biblioteca supertest, que fornece um wrapper HTTP de alto nível para testar frameworks como Mocha.
npm install --save-dev supertest
Agora, vamos escrever os testes:
// src/test/api.test.ts
import chai from 'chai';
import chaiHttp from 'chai-http';
import { app, server } from '../server'; // Importa a instância do servidor e a aplicação Express
// Configura o chai para usar o chai-http
chai.use(chaiHttp);
const expect = chai.expect;
describe('API User Endpoints', () => {
// Variáveis globais para o describe block
let request: chai.SuperTest<chai.Test>;
// --- Setup e Teardown ---
// beforeEach: Executa antes de cada teste (it block)
beforeEach(() => {
// Cria uma nova instância de requisição para cada teste
// Isso garante que os testes sejam isolados
request = chai.request(app);
});
// after: Executa uma vez após todos os testes em 'describe' terem terminado
after((done) => {
// Fecha o servidor após a execução de todos os testes
server.close(() => {
console.log('Server closed after all tests');
done(); // Chama done() para indicar que o teardown está completo
});
});
// --- Testes ---
it('should GET all users', async () => {
const res = await request.get('/users');
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
expect(res.body.length).to.be.greaterThan(0);
expect(res.body[0]).to.have.property('id');
expect(res.body[0]).to.have.property('name');
expect(res.body[0]).to.have.property('email');
});
it('should POST a new user', async () => {
const newUser = { name: 'Charlie', email: 'charlie@example.com' };
const res = await request.post('/users').send(newUser);
expect(res).to.have.status(201);
expect(res.body).to.be.an('object');
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('Charlie');
expect(res.body.email).to.equal('charlie@example.com');
// Verificação adicional: buscar o usuário recém-criado para confirmar
const getRes = await request.get(`/users/${res.body.id}`);
expect(getRes).to.have.status(200);
expect(getRes.body.name).to.equal('Charlie');
});
it('should return 400 if name or email is missing during POST', async () => {
const incompleteUser = { name: 'David' }; // Email faltando
const res = await request.post('/users').send(incompleteUser);
expect(res).to.have.status(400);
expect(res.body).to.have.property('message', 'Name and email are required');
});
it('should GET a specific user by ID', async () => {
// Supondo que Alice (id=1) exista
const res = await request.get('/users/1');
expect(res).to.have.status(200);
expect(res.body).to.be.an('object');
expect(res.body.id).to.equal(1);
expect(res.body.name).to.equal('Alice');
});
it('should return 404 if user is not found by ID', async () => {
const nonExistentId = 999;
const res = await request.get(`/users/${nonExistentId}`);
expect(res).to.have.status(404);
expect(res.body).to.have.property('message', 'User not found');
});
it('should DELETE a user by ID', async () => {
// Precisamos de um usuário para deletar. Vamos criar um primeiro.
const createUserRes = await request.post('/users').send({ name: 'Eve', email: 'eve@example.com' });
expect(createUserRes).to.have.status(201);
const userIdToDelete = createUserRes.body.id;
// Agora, deleta o usuário
const deleteRes = await request.delete(`/users/${userIdToDelete}`);
expect(deleteRes).to.have.status(204); // Status No Content para DELETE bem-sucedido
// Verificação: tentar buscar o usuário deletado
const getRes = await request.get(`/users/${userIdToDelete}`);
expect(getRes).to.have.status(404);
expect(getRes.body).to.have.property('message', 'User not found');
});
it('should return 404 if trying to delete a non-existent user', async () => {
const nonExistentId = 999;
const res = await request.delete(`/users/${nonExistentId}`);
expect(res).to.have.status(404);
expect(res.body).to.have.property('message', 'User not found');
});
});
// Lembre-se de exportar o server para que o 'after' hook possa fechá-lo
export { request };
Setup e Teardown de Ambientes de Teste
Estratégias eficazes de setup e teardown são cruciais para garantir que seus testes sejam confiáveis e não interfiram uns nos outros.
-
before: Executa uma vez antes de todos os testes em um blocodescribe. Útil para inicializar recursos que serão compartilhados por todos os testes (ex: conectar a um banco de dados de teste). -
beforeEach: Executa antes de cada teste individual (it). Essencial para garantir que cada teste comece com um estado limpo e previsível. No nosso exemplo, recriamos a instância dechai.request(app)para isolar cada teste. Se estivéssemos usando um banco de dados, aqui seria o local para limpar tabelas ou resetar dados. -
after: Executa uma vez após todos os testes em um blocodescribeterem terminado. Ideal para liberar recursos globais (ex: fechar a conexão com o banco de dados, parar o servidor de teste). No nosso exemplo, usamosserver.close()para garantir que o processo de teste termine corretamente. -
afterEach: Executa após cada teste individual (it). Útil para limpar recursos criados especificamente por um teste (ex: deletar um arquivo criado, remover um registro específico do banco de dados).
No exemplo acima, usamos beforeEach para isolar os testes e after para fechar o servidor. Para testes mais complexos envolvendo bancos de dados, você pode precisar de before para configurar o banco e afterEach para limpar os dados criados por cada teste, garantindo a idempotência.
Executando os Testes
Simplesmente execute o comando definido no seu package.json:
npm test
Você deverá ver os resultados dos seus testes no console.
Conclusão
Dominar testes de API com Mocha e Chai é um passo fundamental para qualquer desenvolvedor backend sério sobre a qualidade e a manutenibilidade do seu código. Ao implementar estratégias robustas de setup e teardown, você garante que seus testes sejam uma representação confiável do comportamento da sua API, permitindo que você inove e refatore com segurança. Lembre-se, testes não são um luxo, mas uma necessidade para construir aplicações backend resilientes e de alta performance.
Top comments (0)