Voltar para Artigos
Back-end5 min de leitura

Testes E2E no Node.js: Supertest, Banco de Testes Isolado e Testcontainers

Como montar um ambiente de testes E2E robusto no Express: criação dinâmica de schemas de banco de dados por suite, autenticação via factories, Supertest para roteamento real e o uso de Testcontainers para dependências.

12 de agosto de 2026

Testes Unitários garantem que a regra de negócio funciona, mas são cegos para o mundo externo. Se o arquivo de rotas esquecer de injetar um middleware, ou se a string de conexão do TypeORM estiver errada, a aplicação passa em todos os testes unitários e quebra no deploy. Os Testes End-to-End (E2E) existem para validar o fluxo completo: request HTTP → Roteamento → Middleware (Autenticação/Validação) → Controller → Use Case → Banco de Dados Real → Response.

Isolamento de Banco de Dados por Teste

O maior erro em testes E2E é usar um banco de dados compartilhado. Testes rodando em paralelo inserem dados que afetam as queries uns dos outros, causando os famosos "Flaky Tests" (testes que às vezes passam, às vezes falham). A solução é criar um Schema ou Database isolado para cada suíte (arquivo) de teste e derrubá-lo no final.

setupE2E.ts
import { execSync } from 'child_process';
import { randomUUID } from 'crypto';
import { dataSource } from '../typeorm/data-source';

// Esse arquivo é configurado no jest.config.ts (setupFilesAfterEnv)
// Ele roda antes de cada arquivo de teste (.spec.ts)

const schemaId = `test_${randomUUID().replace(/-/g, '')}`;

beforeAll(async () => {
  // Altera a URL de conexão do TypeORM para usar o novo schema
  const url = new URL(process.env.DATABASE_URL!);
  url.searchParams.set('schema', schemaId);
  process.env.DATABASE_URL = url.toString();

  await dataSource.initialize();
  // Cria o schema isolado no PostgreSQL
  await dataSource.query(`CREATE SCHEMA IF NOT EXISTS ${schemaId}`);
  
  // Roda as migrations exclusivas para este schema
  execSync('npm run typeorm migration:run', { env: process.env });
});

afterAll(async () => {
  // Limpeza absoluta: apaga o schema inteiro no final
  await dataSource.query(`DROP SCHEMA IF EXISTS ${schemaId} CASCADE`);
  await dataSource.destroy();
});

Supertest: Chamadas HTTP sem subir porta

O schema isolado por UUID garante que suites rodando em paralelo (com --maxWorkers=4 do Jest) nunca compartilhem dados. O DROP SCHEMA CASCADE no afterAll limpa tudo sem deixar lixo no banco de testes. Esse padrão também funciona com SQLite em memória para testes ainda mais rápidos, mas o PostgreSQL isolado garante que você esteja testando o comportamento real do banco em produção.

O supertest recebe a instância nativa do Express (app) e permite disparar requisições diretamente pela memória, sem precisar alocar a porta local :3333 e sem problemas de concorrência de portas rodando testes em paralelo.

Um teste E2E bem escrito verifica três camadas: o status HTTP (201, 422, 404), a estrutura da resposta (campos presentes, campos ausentes como password), e o efeito no banco (registro foi realmente criado). Verificar apenas o status HTTP não é suficiente — você pode retornar 201 sem ter salvo nada. Verificar apenas o banco não garante que a rota e o middleware estão funcionando.

CreateUserController.e2e-spec.ts
import request from 'supertest';
import { app } from '../../../infra/http/app';
import { dataSource } from '../../../infra/typeorm/data-source';

describe('CreateUserController (E2E)', () => {
  it('should create a new user and return 201', async () => {
    // 1. Arrange & Act
    const response = await request(app)
      .post('/users')
      .send({
        name: 'Jane Doe',
        email: 'jane@example.com',
        password: 'secure_password',
      });

    // 2. Assert HTTP Response
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe('jane@example.com');
    expect(response.body).not.toHaveProperty('password');

    // 3. Assert Database Effect (Verifica se realmente salvou)
    const userInDb = await dataSource.query(
      `SELECT * FROM users WHERE email = 'jane@example.com'`
    );
    expect(userInDb).toHaveLength(1);
  });

  it('should return 422 if Zod validation fails', async () => {
    const response = await request(app).post('/users').send({
      name: 'Jane',
      email: 'invalid-email', // Vai ser barrado no middleware
      password: '123', // Muito curta
    });

    expect(response.status).toBe(422);
    expect(response.body.message).toContain('Validação');
  });
});

Testcontainers: Se sua API depende do Redis, Kafka ou MongoDB, não obrigue o dev a instalar e gerenciar esses serviços manualmente para rodar os testes. Use a biblioteca testcontainers-node para subir containers Docker efêmeros programaticamente no beforeAll global e injetar as portas dinâmicas nas variáveis de ambiente.

Conclusão

A Pirâmide de Testes ensina: tenha muitos testes unitários (rápidos e focados) e poucos E2E (lentos e abrangentes). Cubra 100% dos fluxos alternativos e de erros nos Testes Unitários com Repositórios em Memória. Use os Testes E2E apenas para os 'Caminhos Felizes' principais (Login, Checkout, Cadastro) para garantir que toda a fiação (Banco, Express, Middlewares, DI) está conectada corretamente.