4단계
testcontainers
30 분
testcontainers
"테스트용 mock DB" 가 아니라 실제 PostgreSQL 컨테이너 를 테스트 안에서 띄움. 프로덕션과 99% 동일 환경.
1. 왜 mock SQLite 말고 testcontainers?
- SQLite ≠ PostgreSQL — JSONB · TIMESTAMPTZ · 배열 · FK CASCADE 등 동작 차이
- 실제 마이그레이션 검증 — CREATE TABLE 이 실제로 돈다
- 격리 — 각 테스트가 독립 컨테이너 또는 트랜잭션 롤백
단점: 첫 실행 시 이미지 pull 30s · 테스트당 13s 오버헤드. 통합 테스트 전용.
2. 설치 (Node + vitest)
pnpm add -D testcontainers pg
3. 기본 패턴
// tests/integration/db.integration.test.ts
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { Pool } from "pg";
let container: StartedPostgreSqlContainer;
let pool: Pool;
beforeAll(async () => {
container = await new PostgreSqlContainer("postgres:15-alpine")
.withDatabase("test")
.withUsername("test")
.withPassword("test")
.start();
pool = new Pool({
host: container.getHost(),
port: container.getPort(),
database: "test",
user: "test",
password: "test",
});
// 마이그레이션 실행
await pool.query(await fs.readFile("sql/001_create.sql", "utf-8"));
}, 60_000);
afterAll(async () => {
await pool.end();
await container.stop();
});
test("insert + select", async () => {
await pool.query("INSERT INTO users (email) VALUES ($1)", ["a@b.c"]);
const r = await pool.query("SELECT count(*)::int AS n FROM users");
expect(r.rows[0].n).toBe(1);
});
4. 파이썬 (pytest)
uv add --dev testcontainers pytest-asyncio asyncpg
# tests/conftest.py
import pytest
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def postgres_url():
with PostgresContainer("postgres:15-alpine") as pg:
yield pg.get_connection_url()
@pytest.fixture
async def db(postgres_url):
import asyncpg
pool = await asyncpg.create_pool(postgres_url)
async with pool.acquire() as conn:
yield conn
await pool.close()
5. 마이그레이션 · 시드 멱등
async function migrate(pool: Pool) {
const files = [
"sql/001_create_users.sql",
"sql/002_create_posts.sql",
];
for (const f of files) {
const sql = await fs.readFile(f, "utf-8");
await pool.query(sql);
}
}
SQL = SSOT 정책이 있다면 그 파일을 그대로 실행. 별도 테스트용 schema 금지.
6. 테스트 격리 전략
세 가지 옵션.
| 옵션 | 속도 | 격리 | 구현 |
|---|---|---|---|
| 컨테이너 per 테스트 | 느림 (3s/test) | 완벽 | 거의 안 씀 |
| 컨테이너 1개 + 트랜잭션 롤백 | 빠름 | 높음 | 추천 |
| 컨테이너 1개 + TRUNCATE | 중간 | 중간 | AUTO_INCREMENT 초기화 편리 |
beforeEach(async () => {
await pool.query("BEGIN");
});
afterEach(async () => {
await pool.query("ROLLBACK");
});
트랜잭션 롤백은 FK 제약 · 트리거 사이드 이펙트까지 모두 되돌림.
7. CI 에서
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
services: # Docker 사용 가능
docker:
image: docker:dind
options: --privileged
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm test:integration
GitHub Actions 는 Docker 내장. docker-compose 없이 testcontainers 동작.
8. 자주 걸리는 자리
- timeout 60s 부족 — 첫 이미지 pull 시 길어짐. 2 분 권장
- 포트 충돌 — testcontainers 가 자동 할당. 하드코딩 금지
- 롤백 후 시퀀스 리셋 안 됨 —
BEGIN; ... ROLLBACK;로는 sequence 유지.TRUNCATE ... RESTART IDENTITY필요 - 병렬 테스트 → 같은 컨테이너 공유 — 트랜잭션 롤백 조합 or 컨테이너 분리
9. Kafka · Redis · Elasticsearch
testcontainers 는 PG 뿐 아니라 모든 주요 인프라 지원.
import { KafkaContainer } from "@testcontainers/kafka";
import { RedisContainer } from "@testcontainers/redis";
const kafka = await new KafkaContainer("confluentinc/cp-kafka:7.5.0").start();
const redis = await new RedisContainer("redis:7-alpine").start();
하고픈 말
"mock DB 가 가짜라서 프로덕션에서만 버그 발견" 이 testcontainers 도입의 주된 이유. 오버헤드를 감수할 만한 수익이 있습니다.
Next
- 05-playwright-e2e