codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›테스트 전략과 품질 게이트›4단계

4단계

testcontainers

0회 조회

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

← 3단계

pytest · fixture · parametrize

5단계 →

Playwright E2E