DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Testes de integração em Node.js com Mocha/Chai

logotech

# 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
Enter fullscreen mode Exit fullscreen mode

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\"]
}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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 bloco describe. Ú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 de chai.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 bloco describe terem terminado. Ideal para liberar recursos globais (ex: fechar a conexão com o banco de dados, parar o servidor de teste). No nosso exemplo, usamos server.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
Enter fullscreen mode Exit fullscreen mode

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)