컨테이너 안에서 테스트 실행하기

Docker로 테스트를 격리하는 3가지 패턴(이미지 직접 실행·Compose 통합·멀티 스테이지), 서비스 healthcheck 대기, 커버리지 보고서 추출, GitHub Actions 연동 완성 예제를 다룹니다.

· 4 min read · PALDYN Team

지난 글에서 build-push-action으로 이미지를 빌드하고 레지스트리에 푸시하는 방법을 다뤘다. 이미지를 만들기 전에 테스트가 통과해야 한다. 컨테이너 안에서 테스트를 실행하면 CI 환경과 로컬 환경이 완전히 동일해지고, 외부 의존성(DB, Redis 등)도 컨테이너로 함께 구동해 격리된 상태에서 통합 테스트가 가능하다.

3가지 테스트 패턴

컨테이너 테스트 3가지 패턴

패턴을 선택하는 기준은 외부 의존성 존재 여부prod 이미지 분리 필요성이다.

패턴 1: 빌드된 이미지로 직접 실행

# 이미지 빌드 (테스트 의존성 포함)
docker build -t myapp:test .

# 테스트 실행
docker run --rm myapp:test pytest tests/unit/ -v

# 환경 변수 주입
docker run --rm \
  -e DATABASE_URL=sqlite:///:memory: \
  myapp:test \
  pytest tests/ --tb=short

유닛 테스트처럼 외부 서비스가 없어도 되는 경우에 가장 단순하다.

패턴 2: Compose로 통합 테스트

Compose 통합 테스트 구성

# compose.test.yml
services:
  app:
    build: .
    command: pytest tests/ -v --cov=app --cov-report=xml
    environment:
      DATABASE_URL: postgresql://test:test@db:5432/testdb
      REDIS_URL: redis://redis:6379
    volumes:
      - ./coverage:/app/coverage   # 보고서 추출
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      retries: 5
# 테스트 실행
docker compose -f compose.test.yml up \
  --build \
  --abort-on-container-exit \
  --exit-code-from app

# 결과 코드 저장 후 정리
EXIT_CODE=$?
docker compose -f compose.test.yml down -v
exit $EXIT_CODE

패턴 3: 멀티 스테이지 Dockerfile

# Dockerfile
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/

# 테스트 스테이지
FROM base AS test
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY tests/ ./tests/
RUN pytest tests/unit/ -v

# 프로덕션 스테이지 (테스트 통과 후에만 도달)
FROM base AS production
CMD ["gunicorn", "src.main:app"]
# 테스트 스테이지까지만 빌드 (테스트 실패 시 중단)
docker build --target test -t myapp:test .

# 프로덕션 이미지 빌드 (테스트 통과 필요)
docker build --target production -t myapp:prod .

테스트 코드와 개발 의존성이 프로덕션 이미지에 포함되지 않는다.

커버리지 보고서 추출

# 볼륨 마운트로 호스트에 보고서 추출
docker run --rm \
  -v $(pwd)/coverage:/app/coverage \
  myapp:test \
  pytest tests/ --cov=app --cov-report=xml:coverage/coverage.xml

# GitHub Actions에서 업로드
- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/coverage.xml

GitHub Actions 완성 예제

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run unit tests
        run: |
          docker build --target test -t myapp:test .

      - name: Run integration tests
        run: |
          docker compose -f compose.test.yml up \
            --build --abort-on-container-exit \
            --exit-code-from app
          docker compose -f compose.test.yml down -v

      - name: Build production image
        if: success()
        run: docker build --target production -t myapp:prod .

테스트용 환경 변수 관리

# compose.test.yml — env_file 사용
services:
  app:
    env_file:
      - .env.test     # 테스트 전용 설정
    build: .
# .env.test
DATABASE_URL=postgresql://test:test@db:5432/testdb
SECRET_KEY=test-secret-key-not-for-production
ENVIRONMENT=test

.env.test는 실제 시크릿 없이 테스트 전용 더미 값만 포함한다. .gitignore가 아니라 저장소에 커밋해도 무방하다.

병렬 테스트 분할 (pytest-xdist)

# 컨테이너 안에서 병렬 실행
docker run --rm myapp:test \
  pytest tests/ -n auto --dist=loadscope

-n auto는 CPU 코어 수만큼 워커를 생성한다. 테스트 수가 많을 때 큰 효과가 있다.


지난 글: docker/build-push-action 완전 정복

다음 글: Docker로 개발 환경 구성하기


읽어주셔서 감사합니다. 😊