Testcontainers: Real Database Integration Testing for Node.js
Testcontainers is a Node.js library that spins up real Docker containers — PostgreSQL, Redis, MongoDB, Kafka, and more — for use inside your test suite. It solves the fundamental problem with mocked databases: the mock passes, but the real query fails in production. With Testcontainers, each test run uses a fresh, real container that starts before tests and stops when they finish.
Why Real Containers Beat Mocks
Mocks simulate a database by returning hardcoded values. They cannot catch: SQL syntax errors, constraint violations, index performance, serialization differences between pg-node and raw Postgres, or Redis EXPIRE behaviour. A mock-based test that passes can coexist with a production bug for months.
Testcontainers runs the same Docker image you use in production. Your queries, migrations, and ORM code run against the real engine. If the test passes, it works. If it fails, it fails now — not in production at 3am.
Installation
npm install --save-dev testcontainers
# Requires Docker to be running locally or in CI
docker --version # verify Docker is available PostgreSQL Container with Jest
// tests/user.integration.test.ts
import { PostgreSqlContainer } from 'testcontainers';
import { Pool } from 'pg';
let container: PostgreSqlContainer;
let pool: Pool;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16')
.withDatabase('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();
pool = new Pool({ connectionString: container.getConnectionUri() });
await pool.query('CREATE TABLE users (id SERIAL, name TEXT)');
}, 60_000); // allow 60s for image pull
afterAll(async () => {
await pool.end();
await container.stop();
});
it('inserts and retrieves a user', async () => {
await pool.query("INSERT INTO users (name) VALUES ('Alice')");
const result = await pool.query('SELECT name FROM users');
expect(result.rows[0].name).toBe('Alice');
}); Redis Container for Cache Tests
import { RedisContainer } from 'testcontainers';
import { createClient } from 'redis';
let container: RedisContainer;
let client: ReturnType<typeof createClient>;
beforeAll(async () => {
container = await new RedisContainer('redis:7').start();
client = createClient({ url: container.getConnectionUrl() });
await client.connect();
}, 30_000);
afterAll(async () => {
await client.quit();
await container.stop();
});
it('sets and gets a key with TTL', async () => {
await client.set('session:abc', 'user123', { EX: 300 });
const val = await client.get('session:abc');
expect(val).toBe('user123');
const ttl = await client.ttl('session:abc');
expect(ttl).toBeGreaterThan(0);
}); Container Lifecycle and Parallel Isolation
// Start multiple containers in parallel:
const [pg, redis] = await Promise.all([
new PostgreSqlContainer().start(),
new RedisContainer().start(),
]);
// Each container gets a random host port — no conflicts
console.log(pg.getMappedPort(5432)); // e.g., 54321
console.log(redis.getMappedPort(6379)); // e.g., 63791 Using with Vitest
// vitest.config.ts
export default defineConfig({
test: {
testTimeout: 60_000, // containers need startup time
hookTimeout: 60_000,
pool: 'forks', // required for Docker socket access in Vitest
}
}); Testcontainers vs docker-compose for Tests
| Aspect | Testcontainers | docker-compose |
|---|---|---|
| Setup | In test code (TypeScript) | Separate YAML file |
| Lifecycle | Auto start/stop per suite | Manual up/down commands |
| Isolation | Per-test or per-suite | Shared across all tests |
| Port conflicts | No — random ports | Yes — fixed ports |
| CI complexity | Just run npm test | Requires compose pre-step |
Related Products
on GumroadDeveloper Productivity Bundle
Stop rebuilding the same setup every project. 51 VSCode snippets, 40 Git aliases, 5 Docker Compose stacks, 5 GitHub Actions workflows, 5 README templates, and a dotfiles kit. Set up a professional dev environment in under an hour.
Buy on Gumroad →AI Prompt Engineering Toolkit
58 structured prompt templates for Claude, ChatGPT, and Gemini — organized by use case. Code Review, Debugging, Documentation, Architecture, and 10 model-specific tips. Stop writing prompts from scratch.
Buy on Gumroad →Developer Cheat Sheet Bundle
10 print-ready cheat sheets for the tools developers use every day: Git, Docker, Linux CLI, SQL, Regex, HTTP Status codes, VS Code shortcuts, Bash scripting, jq/JSON, and Markdown.
Buy on Gumroad →You Might Also Need
JSON Formatter & Validator
Format, beautify, and validate JSON online. Paste your JSON to instantly pretty-print it with 2-space, 4-space, or tab indentation. Minify JSON too. Free, runs in your browser.
Open free tool →
Regex Tester
Free regex tester. Test regular expressions against multiple strings with expected match/no-match assertions — see instant pass/fail results. Built-in presets for email, URL, IPv4, and phone. Pro: unl
Open free tool →
Base64 Encoder & Decoder
Free Base64 Encoder & Decoder. Convert text, images, and any file to Base64 and back. Supports URL-safe Base64, image data URIs, and file download from Base64. Runs 100% in your browser — no data sent
Open free tool →
More Free Tools
Related Articles
curl 深度使用手冊:API 測試從入門到進階
curl 是每位開發者必備的 CLI 工具。本文涵蓋 GET/POST/PUT/DELETE 請求、認證、header 設定、檔案上傳、除錯技巧,以及 REST API 測試的完整實戰範例。
Best AI Code Review Tools 2026: CodeRabbit, Sourcery, DeepCode & Bito Compared
Compare top AI code review tools for 2026: CodeRabbit, Sourcery, DeepCode, and Bito. Features, pricing, GitHub/GitLab integration, and choosing the right tool for your team.
AI Code Review Tools 2026: GitHub Copilot, CodeRabbit, Qodo Merge Ranked
Compare AI code review tools for 2026: GitHub Copilot code review, CodeRabbit, Qodo Merge, Sourcery, and PR-Agent. Pricing, accuracy, and GitHub integration.
AI Coding Tools Comparison 2026: GitHub Copilot vs Cursor vs Windsurf vs Cline
Comprehensive comparison of the best AI coding tools in 2026: GitHub Copilot, Cursor, Windsurf, and Cline. Pricing, features, model support, and real-world benchmarks.
AI Pair Programming in 2026: GitHub Copilot vs Cursor vs Claude Code
Master AI pair programming in 2026. Compare GitHub Copilot, Cursor, and Claude Code for autocomplete, chat, and agentic workflows. Includes setup guides and productivity tips.
Enjoying this tool? Get more like it.
Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.
Unlock AI-Powered Dev Tools
- ⚡ AI Code Review, Doc Generator & SQL Builder
- ⚡ All premium templates & early access
- ⚡ Member discounts on Gumroad products
$9/mo after trial · Cancel anytime
Want the full toolkit?
Get DevPlaybook Pro
Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.
MIT licensed · Instant download · No subscription
See what's included → Browse all deals →Frequently Asked Questions
What is the difference between unit tests with mocks and integration tests with Testcontainers?▼
Unit tests with mocks replace real dependencies (databases, caches, queues) with fake objects that return predefined responses. This makes them fast and isolated, but they can pass even when the real integration is broken. Testcontainers runs a real Docker container — a real PostgreSQL, a real Redis — so your integration test exercises the actual SQL queries, real connection pooling, real Redis serialization. If something breaks in production, a Testcontainers test will catch it; a mock often will not.
How do I use Testcontainers with Jest?▼
Install testcontainers and the relevant module: npm install --save-dev testcontainers. In your Jest test file, use beforeAll to start the container and store the connection URL, then pass that URL to your database client. Use afterAll to call container.stop(). Set testTimeout in jest.config to at least 30000ms because container startup takes time. Using --runInBand prevents parallel containers from conflicting on the same host port.
How does Testcontainers handle container cleanup?▼
By default, Testcontainers registers a JVM (or Node process) shutdown hook that stops containers when the test process exits. If you call container.stop() in afterAll, cleanup is explicit and reliable. Testcontainers also supports a Ryuk sidecar container (enabled by default) that automatically removes any containers that were not explicitly stopped — useful if a test process crashes mid-run. To disable Ryuk, set TESTCONTAINERS_RYUK_DISABLED=true.
How do I run Testcontainers in GitHub Actions CI?▼
GitHub Actions runners include Docker by default on ubuntu-latest. No extra setup is needed — just include Testcontainers in your test suite and run npm test in the workflow. The Docker daemon is already running. For self-hosted runners, ensure the runner user has Docker socket access. If you see socket permission errors, add the runner user to the docker group. Testcontainers Cloud is an alternative that offloads container orchestration to a remote daemon.
Can Testcontainers run multiple containers per test suite?▼
Yes. You can start multiple containers independently in separate beforeAll blocks or use the Network API to create containers that communicate with each other on a shared Docker network. A common pattern is a PostgreSQL container + a Redis container started in parallel with Promise.all([pgContainer.start(), redisContainer.start()]) to minimise startup time. Each container gets its own dynamically assigned host port to avoid conflicts.
How does Testcontainers Cloud differ from local Docker?▼
Local Docker runs containers on your machine or CI runner. Testcontainers Cloud (by AtomicJar, now part of Docker) offloads container execution to a remote cloud daemon. This is useful for teams with slow local machines, strict CI environments where Docker is unavailable, or when you want to avoid Docker-in-Docker complexity in Kubernetes-based CI. The API is identical — you change only an environment variable, not your test code.