Ramos da InformáticaMundoJSUm guia completo para Testcontainers no Node.js

Um guia completo para Testcontainers no Node.js

-

O Testcontainers é uma biblioteca que fornece instâncias leves e descartáveis de bancos de dados, message brokers, navegadores ou qualquer coisa que possa rodar em um contêiner Docker. É ideal para testes de integração. Ela ajuda você a evitar os altos custos de manter ambientes compartilhados de staging e teste, elimina a bagunça de mocks que imitam mal o comportamento real e resolve o problema do “na minha máquina funciona”, garantindo que os testes rodem contra serviços reais, com a mesma configuração, tanto localmente quanto na pipeline de CI. O Testcontainers gerencia o ciclo de vida do contêiner: inicia antes dos testes, para depois. É determinístico, isolado e reproduzível.

Como eu usava antes

Antes do Testcontainers, a abordagem padrão era mockar dependências. Mockar um driver de banco de dados ou um cliente Redis funciona para testes unitários, mas é frágil para testes de integração. Você testa o mock, não o comportamento real. Já vi muitas vezes testes passarem com mocks e falharem em produção por diferenças sutis de serialização, timeouts ou semântica de comandos. Rodar um banco de dados compartilhado também é problemático: estado sujo entre execuções, conflitos de schema e a necessidade de limpar bases manualmente.

O que é o Testcontainers

O Testcontainers é um pacote que oferece APIs para iniciar e interagir com contêineres Docker diretamente do seu código de teste. Cada módulo encapsula um serviço. Por exemplo, o módulo PostgreSqlContainer expõe métodos para obter a string de conexão, usuário, senha, etc., já configurados. Você não precisa lidar com scripts shell ou arquivos docker-compose para subir dependências de teste.

Instalação

Instale o pacote principal:
npm install -D testcontainers
Depois, adicione o módulo específico do serviço que você precisa:
npm install -D @testcontainers/postgresql
Módulos existem para PostgreSQL, MySQL, Redis, Kafka, Elasticsearch, MongoDB, entre outros. Você também pode criar contêineres genéricos com GenericContainer.

Primeiro exemplo: PostgreSQL

Aqui está um teste simples que sobe um PostgreSQL, cria uma tabela e insere dados:
const { PostgreSqlContainer } = require('@testcontainers/postgresql'); const { Client } = require('pg'); describe('PostgreSQL Testcontainers', () => { let container; let client; beforeAll(async () => { container = await new PostgreSqlContainer() .withDatabase('testdb') .withUsername('testuser') .withPassword('testpass') .start(); client = new Client({ host: container.getHost(), port: container.getPort(), database: container.getDatabase(), user: container.getUsername(), password: container.getPassword(), }); await client.connect(); }); afterAll(async () => { await client.end(); await container.stop(); }); it('should create a table and insert a row', async () => { await client.query(` CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(50) ) `); await client.query(`INSERT INTO users (name) VALUES ('Alice')`); const res = await client.query(`SELECT * FROM users`); expect(res.rows).toHaveLength(1); expect(res.rows[0].name).toBe('Alice'); }); });
O ciclo é sempre o mesmo: inicia o contêiner, usa as credenciais expostas, executa o teste, derruba tudo. Nada de sujeira acumulada.

Configuração avançada

Pool de contêineres quentes

Por padrão, cada suíte de testes sobe e derruba um contêiner novo. Isso é seguro, mas em suítes muito grandes o cold start acumulado pode ser proibitivo. Para desenvolvimento local, use o withReuse:
container = await new PostgreSqlContainer() .withReuse() .withLabel('reuse-id', 'my-postgres') .start();
O contêiner fica rodando até ser explicitamente parado. Mas em CI, withReuse pode vazar estado entre pipelines diferentes. Para resolver isso com segurança, implemente um sistema de pool de contêineres quentes com tag por branch:
const branch = process.env.CI_COMMIT_BRANCH || 'local'; const reuseId = `postgres-${branch}-${require('crypto').randomBytes(4).toString('hex')}`; container = await new PostgreSqlContainer() .withReuse() .withLabel('branch', branch) .withLabel('reuse-id', reuseId) .start();
Associado a um cronjob que roda docker container prune --filter "label=branch=<branch>" após o merge, você mantém um pool de contêineres quentes por branch ativa. É a diferença entre esperar 10 segundos e 200 milissegundos para obter uma conexão de banco.

Wait strategies

Por padrão, o Testcontainers espera que a porta do serviço esteja disponível. Mas muitos serviços expõem a porta antes de estarem realmente prontos. Por isso, use wait strategies explícitas:
const { Wait } = require('testcontainers'); container = await new PostgreSqlContainer() .withWaitStrategy( Wait.forLogMessage('database system is ready to accept connections') ) .start();
Isso evita falhas intermitentes no início dos testes.

Testes paralelos com Jest

Se você usa --maxWorkers > 1 no Jest, precisa garantir que portas e aliases de rede não colidam entre workers diferentes. O Testcontainers resolve portas aleatórias automaticamente. Porém, aliases de rede são estáticos — dois workers tentando criar o mesmo alias na mesma rede Docker vão conflitar. A solução é tornar os aliases únicos por worker:
const workerId = process.env.JEST_WORKER_ID || '0'; const networkAlias = `db-${workerId}`; const pgContainer = await new PostgreSqlContainer() .withNetwork(network) .withNetworkAliases(networkAlias) .start();
Assim, cada worker do Jest tem seu próprio namespace na rede Docker, mantendo total isolamento mesmo em execução paralela.

Rede customizada

Se você precisa que múltiplos contêineres se comuniquem, crie uma rede Docker compartilhada:
const { Network } = require('testcontainers'); const network = await new Network().start(); const pgContainer = await new PostgreSqlContainer() .withNetwork(network) .withNetworkAliases('db') .start();

Variáveis de ambiente

Qualquer opção extra pode ser injetada via withEnvironment:
container = await new GenericContainer('redis') .withEnvironment({ REDIS_PASSWORD: 'secret' }) .start();

Integração com ORMs: migrations programáticas

Em vez de confiar em um banco pré-configurado ou scripts externos, suba o schema programaticamente dentro do beforeAll. Isso garante que o schema esteja sempre sincronizado com a versão do código sob teste. Exemplo com Knex:
const knex = require('knex'); beforeAll(async () => { container = await new PostgreSqlContainer().start(); const db = knex({ client: 'pg', connection: { host: container.getHost(), port: container.getPort(), user: container.getUsername(), password: container.getPassword(), database: container.getDatabase(), }, }); await db.migrate.latest(); await db.seed.run(); // injete essa instância nos testes global.__db__ = db; });
Se usa TypeORM, faça await dataSource.runMigrations(). Se usa Prisma, prisma migrate deploy seguido de prisma db seed. O ponto central é: o ciclo de vida do schema é parte do teste, não um pré-requisito externo. Isso fecha a porta para o clássico “esqueci de rodar a migration no ambiente de teste”.

Snapshots determinísticos com browser-container

Testcontainers não serve só para bancos. O módulo browser-container permite rodar navegadores reais — Chromium, Firefox — dentro de contêineres, com total isolamento. Combine isso com jest-image-snapshot e você terá testes visuais determinísticos, sem depender de fontes, resoluções de tela ou configurações do sistema operacional do runner:
const { BrowserContainer } = require('@testcontainers/browser'); const { toMatchImageSnapshot } = require('jest-image-snapshot'); expect.extend({ toMatchImageSnapshot }); let browserContainer; let browser; beforeAll(async () => { browserContainer = await new BrowserContainer() .withImage('chromium') .start(); const wsEndpoint = browserContainer.getWebSocketUrl(); browser = await chromium.connect(wsEndpoint); }); afterAll(async () => { await browser.close(); await browserContainer.stop(); }); it('renders the dashboard correctly', async () => { const page = await browser.newPage(); await page.goto('http://localhost:3000/dashboard'); const screenshot = await page.screenshot(); expect(screenshot).toMatchImageSnapshot({ failureThreshold: 0.01, failureThresholdType: 'percent', }); });
Como o contêiner do navegador é imutável e idêntico em qualquer máquina, os snapshots são perfeitamente reproduzíveis entre desenvolvedores e CI. Chega de testes de screenshot que “quebram misteriosamente” porque alguém atualizou o Chrome local.

Teste real: app Node.js com PostgreSQL e Redis

Suponha um app Express que depende de PostgreSQL e Redis. Seu teste de integração sobe ambos os contêineres, aplica as migrations, popula dados e faz requisições HTTP reais contra o app:
beforeAll(async () => { pgContainer = await new PostgreSqlContainer().start(); redisContainer = await new GenericContainer('redis:7') .withExposedPorts(6379) .start(); process.env.DATABASE_URL = `postgresql://${pgContainer.getUsername()}:${pgContainer.getPassword()}@${pgContainer.getHost()}:${pgContainer.getPort()}/${pgContainer.getDatabase()}`; process.env.REDIS_URL = `redis://${redisContainer.getHost()}:${redisContainer.getMappedPort(6379)}`; // Rode as migrations e inicie o servidor... const app = require('../app'); server = app.listen(0); });
Isso lhe dá confiança de que o sistema todo funciona, não apenas partes isoladas.

Testcontainers na CI

No GitHub Actions, por exemplo, você só precisa garantir que o Docker esteja disponível — ele já vem pré-instalado nos runners ubuntu-latest. Nenhuma configuração especial é necessária. Um passo típico:
- name: Run tests run: npm test
Sem services extras no workflow, sem docker-compose. Tudo está no código.

Limitações e cuidados

  • Performance: Subir um contêiner é rápido, mas não instantâneo. O pool de contêineres quentes com tag por branch resolve isso para suítes grandes.
  • Sobrecarga de recursos: Vários contêineres simultâneos podem pesar em CI. Monitore o consumo de memória e ajuste o maxWorkers de acordo.
  • Docker em Docker (DinD): Se você usa contêineres para rodar seus testes (ex.: runners customizados), pode precisar configurar o socket do Docker ou usar DinD, mas em runners padrão funciona sem problemas.
  • Limpeza em paralelo: Mesmo com afterAll, se um worker do Jest for morto abruptamente, contêineres podem ficar órfãos. Combine aliases únicos por worker com um globalTeardown que faça a limpeza como rede de segurança.
  • Snapshots visuais: Contêineres de navegador são pesados. Reserve-os para testes de regressão visual crítica e execute-os em um job separado do CI.

Conclusão

O Testcontainers é uma ferramenta que considero indispensável para testes de integração em Node.js. Ele remove a desculpa de que “testes de integração são difíceis de configurar” e aproxima o ambiente de teste da produção real. As estratégias que discutimos — pool de contêineres quentes, migrations programáticas, aliases únicos para paralelismo e snapshots determinísticos via browser-container — são o salto de maturidade que separa um teste que “funciona” de um teste que escala em times grandes e pipelines complexas. Se você ainda mocka banco de dados ou depende de instâncias compartilhadas para testes, experimente o Testcontainers. E quando o fizer, vá direto para essas práticas avançadas. A qualidade, a previsibilidade e a velocidade do seu pipeline de testes vão melhorar significativamente. Continue aprendendo Sincronizando PostgreSQL e Turbopuffer com Puffgres – A integração de inteligência artificial em aplicações modernas — seja para buscas semânticas, sistemas de Geração Aumentada por Recuperação (RAG) ou motores de recomendação — introduziu um desafio arquitetural complexo: a sincronização de dados. Como manter seu banco de dados relacional primário e seu banco de dados vetorial perfeitamente alinhados sem comprometer a performance ou introduzir inconsistências

3 Técnicas para Reduzir Consumo de Tokens no Claude Code e Codex

Biscuit PostgreSQL: Indexação LIKE de Alta Performance

🚀 Apoio Independente de um sonho

Ajude a manter o código livre e o sonho vivo de viver de produzir conteúdos para você!

Escrever tutoriais profundos e sem paywalls exige tempo e dedicação. Meu grande sonho é me dedicar 100% a produzir conteúdos, criar ferramentas open-source, abrir uma comunidade ativa e, em breve, lançar um canal no YouTube.

Se este artigo te poupou horas de trabalho, considere enviar um "Pix Livre" de qualquer valor para apoiar esta jornada. Cada incentivo me aproxima de viver exclusivamente para a nossa comunidade dev! 💚

QR Code Pix Livre - Ramos da Informática
Escaneie com o app do seu banco ☕
Ramos Souza J
Ramos Souza Jhttps://ramosdainformatica.com.br/sobre/
Ramos de Souza Janones é Senior FullStack Engineer na ReDraw, com mais de 26 anos de trajetória no desenvolvimento de software. Especialista em arquiteturas escaláveis com React e TypeScript, sua jornada percorreu desde o Clipper até o ecossistema moderno de IA e microsserviços. Com passagens por grandes players como Wipro (Bradesco PIX), Ramos também atuou na Fiocruz em um projeto estratégico para o Ministério da Saúde, desenvolvendo o sistema de acompanhamento da saúde da mulher para a prevenção do câncer de colo, do monitoramento na infância à maturidade. Unindo visão técnica profunda, liderança e foco em performance, ele é o criador do portal Ramos da Informática, onde compartilha conhecimento sobre desenvolvimento Full Stack e as tendências de IA aplicadas à engenharia de software.

Mais recentes

PostgreSQL: Como Dar Acesso Seguro para DBA Terceirizado

Aprenda a conceder acesso seguro e temporário para DBA terceirizado no PostgreSQL. Guia completo com pgaudit, privilégios granulares, expiração...

Do Prompt ao Protótipo: Modelagem 3D com Text-to-CAD e Agentes de IA

Descubra como a modelagem 3D com Text-to-CAD e agentes de IA está transformando o design mecânico. Tutorial completo de instalação, skills...

ShadScan: Gere Código shadcn/ui a Partir de Prints

Descubra como o ShadScan usa IA para transformar prints de componentes UI em código shadcn/ui e Tailwind CSS pronto...

Package.json Linter: Como Validar com ESLint Plugin

Aprenda a usar o eslint-plugin-package-json para validar e padronizar seu package.json automaticamente. Evite erros de publicação NPM, inconsistências em...
E-Zine Dev

Evolua para Sênior

Estratégias de Node.js, arquitetura Limpa e IA que nunca publicamos no blog. Junte-se a +10.000 devs.

Assinar Gratuitamente Zero spam. Cancele quando quiser.
Masterclass Online

Automação de Busca de Vagas Tech com n8n e IA

Construa o seu próprio recrutador autônomo. Aprenda a varrer a internet, ler requisitos com LLMs e receber as vagas com match perfeito no seu Telegram ou Slack.

  • Workflows visuais com n8n
  • Filtros inteligentes de stack com IA
  • Alertas de vagas em tempo real na nuvem

Com o Especialista

Ramos de Souza Janones

Engenheiro Full Stack Sênior

Garantir Minha Vaga ➔ Inscrições via Sympla

Masterclass Ensina Profissionais de TI a Automatizar a Busca por Vagas Usando IA e n8n

Encontrar a vaga ideal no mercado de tecnologia não precisa mais ser um processo manual, repetitivo e exaustivo. Uma...

Sincronizando PostgreSQL e Turbopuffer com Puffgres

Guia Definitivo: Sincronizando PostgreSQL e Turbopuffer com Puffgres para Buscas Vetoriais Avançadas A integração de inteligência artificial em aplicações modernas...

Mais Lidos

State of AI 2026: A Maturidade da Inteligência Artificial

A inteligência artificial deixou definitivamente o território das experimentações...

Do Prompt ao Protótipo: Modelagem 3D com Text-to-CAD e Agentes de IA

Descubra como a modelagem 3D com Text-to-CAD e agentes de...

Guia Definitivo de PHP: Funcionalidades e Uso

PHP é uma linguagem interpretada livre, usada originalmente apenas...

Placa de vídeo da NVIDIA RTX 4070 TI: Alto desempenho

Nova placa de vídeo da NVIDIA, RTX 4070 TI...
E-Zine Dev

Evolua para Sênior

Estratégias de Node.js, arquitetura Limpa e IA que nunca publicamos no blog. Junte-se a +10.000 devs.

Assinar Gratuitamente Zero spam. Cancele quando quiser.
✨ Utilitários Dev

Ferramentas Práticas

Sem enrolação para adiantar o seu dia a dia.

Carreira Internacional

JOB NA GRINGA

Meta de Salário Remoto
U$ 5.000/mês

O mapa completo para programadores do Brasil conquistarem contratos internacionais e mudarem de vida financeira.

  • Vagas exclusivas semanais: Membros acessam vagas com 7 dias de antecedência.
  • Workshops e lives gravadas: Buscar vagas não é óbvio. Nós te mostraremos como.
  • 498 Portais de vagas: Que contratam Brasileiros direto na sua dashboard.
  • Mentorias com Recrutadores: Encontros semanais ao vivo com Erika Linares.
  • Inglês diário com foco em conversação: Treine para entrevistas num ambiente sem julgamentos.
  • Suporte pós-contratação: Contabilidade e recebimento legal com a menor taxa.
Garantir Minha Vaga

Inscrição segura via Hotmart

🚀 Apoio Independente

Ajude a manter o código livre e o sonho vivo!

Escrever tutoriais profundos exige tempo. Meu sonho é viver 100% produzindo conteúdo de qualidade para você!

Escaneie com o app do banco para enviar um "Pix Livre"

Se este conteúdo te ajudou, fortaleça essa jornada. Cada incentivo conta! 💚

Você vai gostarrelacionados
Continue aprendendo

E-Zine Dev Ramos

Quer dominar arquitetura e IA?

Junte-se a +10.000 profissionais. Receba semanalmente estratégias de Node.js, React e IA que nunca publicamos no blog.

Assinar Gratuitamente Zero spam. Cancele quando quiser.

O Sonho é Possível com seu Apoio!

Meu grande sonho é dedicar 100% do meu tempo a criar ferramentas open-source e tutoriais profundos para você. Se este conteúdo te ajudou, fortaleça essa jornada enviando um Pix Livre de qualquer valor. 💚

QR Code Pix Livre

Escaneie com o app do banco ☕