← Back to list

testcontainers-usage
by IvanTorresEdge
⭐ 0🍴 1📅 Jan 13, 2026
SKILL.md
name: testcontainers-usage description: Docker-based testing with testcontainers. Use when running tests with real databases.
Testcontainers Usage Skill
This skill covers testcontainers for isolated integration testing with Docker.
When to Use
Use this skill when:
- Need isolated database per test run
- Testing against real database
- Running CI/CD pipeline tests
- Testing database-specific features
Core Principle
REAL DEPENDENCIES - Test against real databases in containers. No mocking database behavior.
Installation
npm install -D @testcontainers/postgresql testcontainers
Basic Setup
// tests/setup/containers.ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';
let container: StartedPostgreSqlContainer;
let prisma: PrismaClient;
export async function startDatabase(): Promise<{
container: StartedPostgreSqlContainer;
prisma: PrismaClient;
connectionString: string;
}> {
container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('testdb')
.withUsername('test')
.withPassword('test')
.start();
const connectionString = container.getConnectionUri();
// Set environment variable for Prisma
process.env.DATABASE_URL = connectionString;
// Run migrations
execSync('npx prisma migrate deploy', {
env: { ...process.env, DATABASE_URL: connectionString },
stdio: 'inherit',
});
// Create Prisma client
prisma = new PrismaClient({
datasources: {
db: { url: connectionString },
},
});
await prisma.$connect();
return { container, prisma, connectionString };
}
export async function stopDatabase(): Promise<void> {
if (prisma) {
await prisma.$disconnect();
}
if (container) {
await container.stop();
}
}
export function getPrisma(): PrismaClient {
return prisma;
}
Vitest Configuration
// vitest.container.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['**/*.container.test.ts'],
testTimeout: 120000,
hookTimeout: 60000,
pool: 'forks',
poolOptions: {
forks: {
singleFork: true, // Run serially for container tests
},
},
globalSetup: './tests/setup/container-setup.ts',
},
});
Global Setup
// tests/setup/container-setup.ts
import { startDatabase, stopDatabase } from './containers';
export async function setup(): Promise<void> {
console.log('Starting test database container...');
await startDatabase();
console.log('Test database ready');
}
export async function teardown(): Promise<void> {
console.log('Stopping test database container...');
await stopDatabase();
console.log('Container stopped');
}
Per-Test Container
// tests/setup/per-test-container.ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';
export async function withDatabase<T>(
testFn: (prisma: PrismaClient) => Promise<T>
): Promise<T> {
const container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('testdb')
.withUsername('test')
.withPassword('test')
.start();
const connectionString = container.getConnectionUri();
// Run migrations
execSync('npx prisma migrate deploy', {
env: { ...process.env, DATABASE_URL: connectionString },
stdio: 'pipe',
});
const prisma = new PrismaClient({
datasources: { db: { url: connectionString } },
});
try {
await prisma.$connect();
return await testFn(prisma);
} finally {
await prisma.$disconnect();
await container.stop();
}
}
Integration Test Example
// src/services/__tests__/user.container.test.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { startDatabase, stopDatabase, getPrisma } from '../../../tests/setup/containers';
import { UserService } from '../user';
describe('UserService with testcontainers', () => {
let prisma: PrismaClient;
let userService: UserService;
beforeAll(async () => {
const db = await startDatabase();
prisma = db.prisma;
userService = new UserService(prisma);
}, 120000); // 2 minute timeout for container startup
afterAll(async () => {
await stopDatabase();
});
beforeEach(async () => {
// Clean up between tests
await prisma.user.deleteMany();
});
it('creates a user', async () => {
const user = await userService.create({
email: 'test@example.com',
name: 'Test User',
password: 'password123',
});
expect(user.id).toBeDefined();
expect(user.email).toBe('test@example.com');
});
it('finds user by email', async () => {
await userService.create({
email: 'find@example.com',
name: 'Find Me',
password: 'password123',
});
const found = await userService.findByEmail('find@example.com');
expect(found).not.toBeNull();
expect(found?.name).toBe('Find Me');
});
it('returns null for non-existent user', async () => {
const found = await userService.findByEmail('nonexistent@example.com');
expect(found).toBeNull();
});
it('updates user', async () => {
const user = await userService.create({
email: 'update@example.com',
name: 'Original Name',
password: 'password123',
});
const updated = await userService.update(user.id, { name: 'New Name' });
expect(updated.name).toBe('New Name');
});
it('deletes user', async () => {
const user = await userService.create({
email: 'delete@example.com',
name: 'Delete Me',
password: 'password123',
});
await userService.delete(user.id);
const found = await userService.findById(user.id);
expect(found).toBeNull();
});
});
Multiple Containers
// tests/setup/multi-containers.ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { GenericContainer, StartedTestContainer } from 'testcontainers';
interface TestContainers {
postgres: StartedPostgreSqlContainer;
redis: StartedTestContainer;
}
export async function startContainers(): Promise<TestContainers> {
// Start containers in parallel
const [postgres, redis] = await Promise.all([
new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('testdb')
.start(),
new GenericContainer('redis:7-alpine')
.withExposedPorts(6379)
.start(),
]);
// Set environment variables
process.env.DATABASE_URL = postgres.getConnectionUri();
process.env.REDIS_URL = `redis://${redis.getHost()}:${redis.getMappedPort(6379)}`;
return { postgres, redis };
}
export async function stopContainers(containers: TestContainers): Promise<void> {
await Promise.all([
containers.postgres.stop(),
containers.redis.stop(),
]);
}
Reusable Container
// tests/setup/reusable-container.ts
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
let reusableContainer: StartedPostgreSqlContainer | null = null;
export async function getReusableContainer(): Promise<StartedPostgreSqlContainer> {
if (!reusableContainer) {
reusableContainer = await new PostgreSqlContainer('postgres:16-alpine')
.withReuse()
.start();
}
return reusableContainer;
}
// Note: Reusable containers persist between test runs
// Use for faster local development
// Don't use in CI - start fresh containers there
Test with App
// src/routes/__tests__/users.container.test.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import supertest from 'supertest';
import { FastifyInstance } from 'fastify';
import { PrismaClient } from '@prisma/client';
import { buildApp } from '../../app';
import { startDatabase, stopDatabase } from '../../../tests/setup/containers';
describe('Users API with real database', () => {
let app: FastifyInstance;
let prisma: PrismaClient;
beforeAll(async () => {
const db = await startDatabase();
prisma = db.prisma;
app = await buildApp();
await app.ready();
}, 120000);
afterAll(async () => {
await app.close();
await stopDatabase();
});
beforeEach(async () => {
await prisma.user.deleteMany();
});
it('creates and retrieves user', async () => {
// Create
const createResponse = await supertest(app.server)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'Test User',
password: 'Password123!',
})
.expect(201);
const userId = createResponse.body.id;
// Retrieve
const getResponse = await supertest(app.server)
.get(`/api/users/${userId}`)
.expect(200);
expect(getResponse.body.email).toBe('test@example.com');
});
});
CI Configuration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
container-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- name: Run container tests
run: npm run test:containers
Best Practices
- Single fork mode - Run container tests serially
- Adequate timeouts - Container startup takes time
- Clean between tests - Delete data, not container
- Reuse for dev - Speed up local development
- Fresh for CI - Always start new in CI
- Parallel containers - Start multiple services together
Notes
- Requires Docker to be running
- First run downloads images (slow)
- Reusable containers speed up local dev
- Container logs available for debugging
Score
Total Score
60/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon