Testcontainers for Python 시작하기

Testcontainers for Python 시작하기

이 가이드에서는 Testcontainers for Python을 이용해 실제 PostgreSQL 인스턴스로 Python 애플리케이션을 만들고 데이터베이스 상호작용을 테스트하는 방법을 배워요.

출처: 문서

본문

이 가이드를 통해 다음 내용을 배울 수 있어요.

  • PostgreSQL을 사용해 고객 데이터를 저장하는 Python 애플리케이션 만들기
  • 데이터베이스와 상호작용하기 위해 psycopg 사용하기
  • testcontainers-python과 pytest로 통합 테스트 작성하기
  • pytest 픽스처(fixture)로 컨테이너 수명주기 관리하기

사전 준비 (Prerequisites)

  • Python 3.10 이상
  • pip
  • Testcontainers가 지원하는 Docker 환경

참고: Testcontainers가 처음이라면 Testcontainers 개요를 방문해 알아보는 걸 권장해요.

Python 프로젝트 만들기

가상 환경과 함께 Python 프로젝트를 만들어요.

$ mkdir tc-python-demo
$ cd tc-python-demo
$ python3 -m venv venv
$ source venv/bin/activate

이 가이드는 Postgres 데이터베이스와 상호작용하기 위해 psycopg3, 테스트를 위해 pytest, 컨테이너에서 PostgreSQL 데이터베이스를 실행하기 위해 testcontainers-python을 사용해요.

의존성을 설치해요.

$ pip install "psycopg[binary]" pytest testcontainers[postgres]
$ pip freeze > requirements.txt

pip freeze 명령은 requirements.txt 파일을 생성해서, 다른 사람들이 pip install -r requirements.txt로 같은 패키지 버전을 설치할 수 있게 해요.

데이터베이스 헬퍼 만들기

데이터베이스 연결을 얻는 함수를 가진 db/connection.py 파일을 만들어요.

import os
import psycopg

def get_connection():
    host = os.getenv("DB_HOST", "localhost")
    port = os.getenv("DB_PORT", "5432")
    username = os.getenv("DB_USERNAME", "postgres")
    password = os.getenv("DB_PASSWORD", "postgres")
    database = os.getenv("DB_NAME", "postgres")
    return psycopg.connect(f"host={host} dbname={database} user={username} password={password} port={port}")

데이터베이스 연결 매개변수를 하드코딩하는 대신, 이 함수는 환경 변수를 사용해요. 이렇게 하면 코드를 바꾸지 않고도 여러 환경에서 애플리케이션을 실행할 수 있어요.

비즈니스 로직 만들기

customers/customers.py 파일을 만들고 Customer 클래스를 정의해요.

class Customer:
    def __init__(self, cust_id, name, email):
        self.id = cust_id
        self.name = name
        self.email = email

    def __str__(self):
        return f"Customer({self.id}, {self.name}, {self.email})"

customers 테이블을 만드는 create_table() 함수를 추가해요.

from db.connection import get_connection

def create_table():
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("""
                CREATE TABLE customers (
                    id serial PRIMARY KEY,
                    name varchar not null,
                    email varchar not null unique)
            """)
        conn.commit()

이 함수는 get_connection()으로 데이터베이스 연결을 얻고 customers 테이블을 만들어요. with 문은 작업이 끝나면 연결을 자동으로 닫아요.

나머지 CRUD 함수를 추가해요.

def create_customer(name, email):
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("INSERT INTO customers (name, email) VALUES (%s, %s)", (name, email))
        conn.commit()

def get_all_customers() -> list[Customer]:
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT * FROM customers")
            return [Customer(cid, name, email) for cid, name, email in cur]

def get_customer_by_email(email) -> Customer:
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT id, name, email FROM customers WHERE email = %s", (email,))
            (cid, name, email) = cur.fetchone()
            return Customer(cid, name, email)

def delete_all_customers():
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("DELETE FROM customers")
        conn.commit()

참고: 이 가이드를 단순하게 유지하기 위해 각 함수가 새 연결을 만들어요. 실제 애플리케이션에서는 연결 풀(connection pool)을 사용해 연결을 재사용하는 게 좋아요.

Testcontainers로 테스트 작성하기

Testcontainers로 PostgreSQL 컨테이너를 만들고 모든 테스트에 사용할 거예요. 각 테스트 전에 모든 고객 레코드를 삭제해서 테스트가 깨끗한 데이터베이스로 실행되게 해요.

pytest 픽스처 설정하기

이 가이드는 설정과 정리 로직에 pytest 픽스처를 사용해요. 권장되는 방법은 파이널라이저(finalizer)를 사용해서 설정이 실패하더라도 정리가 실행되도록 보장하는 거예요.

@pytest.fixture
def setup(request):
    # setup code
    def cleanup():
        # teardown code
        request.addfinalizer(cleanup)
    return some_value

테스트 파일 만들기

pytest 자동 발견(auto-discovery)을 활성화하려면 빈 내용의 tests/__init__.py 파일을 만들어요. 그런 다음 픽스처와 함께 tests/test_customers.py를 만들어요.

import os
import pytest
from testcontainers.postgres import PostgresContainer
from customers import customers

postgres = PostgresContainer("postgres:16-alpine")

@pytest.fixture(scope="module", autouse=True)
def setup(request):
    postgres.start()

    def remove_container():
        postgres.stop()
    request.addfinalizer(remove_container)

    os.environ["DB_CONN"] = postgres.get_connection_url()
    os.environ["DB_HOST"] = postgres.get_container_host_ip()
    os.environ["DB_PORT"] = str(postgres.get_exposed_port(5432))
    os.environ["DB_USERNAME"] = postgres.username
    os.environ["DB_PASSWORD"] = postgres.password
    os.environ["DB_NAME"] = postgres.dbname

    customers.create_table()

@pytest.fixture(scope="function", autouse=True)
def setup_data():
    customers.delete_all_customers()

픽스처가 하는 일을 살펴보면,

  • setup 픽스처는 scope="module"이므로 파일의 모든 테스트에 대해 한 번 실행돼요. PostgreSQL 컨테이너를 시작하고, 연결 정보로 환경 변수를 설정하며, customers 테이블을 만들어요. 정리 함수는 모든 테스트가 끝난 후 컨테이너를 제거해요.
  • setup_data 픽스처는 scope="function"이므로 매 테스트 전에 실행돼요. 모든 레코드를 삭제해서 각 테스트가 깨끗한 데이터베이스를 갖게 해요.

테스트 작성하기

같은 파일에 테스트 함수를 추가해요.

def test_get_all_customers():
    customers.create_customer("Siva", "[email protected]")
    customers.create_customer("James", "[email protected]")

    customers_list = customers.get_all_customers()
    assert len(customers_list) == 2

def test_get_customer_by_email():
    customers.create_customer("John", "[email protected]")

    customer = customers.get_customer_by_email("[email protected]")
    assert customer.name == "John"
    assert customer.email == "[email protected]"

test_get_all_customers()는 고객 레코드 두 개를 삽입하고 모든 고객을 가져와 개수를 단언해요. test_get_customer_by_email()은 고객을 삽입하고 이메일로 조회해 상세 정보를 단언해요. setup_data가 각 테스트 전에 모든 레코드를 삭제하므로, 테스트는 어떤 순서로든 실행될 수 있어요.

테스트 실행과 다음 단계

pytest로 테스트를 실행해요.

$ pytest -v

다음과 비슷한 출력이 보여야 해요.

============================= test session starts ==============================
platform linux -- Python 3.13.x, pytest-9.x.x
collected 2 items

tests/test_customers.py::test_get_all_customers PASSED                    [ 50%]
tests/test_customers.py::test_get_customer_by_email PASSED                [100%]

============================== 2 passed in 1.90s ===============================

테스트는 목(mock) 대신 실제 PostgreSQL 데이터베이스에 대해 실행되므로 구현에 더 큰 확신을 줘요.

요약 (Summary)

Testcontainers for Python 라이브러리는 목(mock) 대신 프로덕션에서 쓰는 것과 같은 종류의 데이터베이스(Postgres)를 사용해 통합 테스트를 작성할 수 있게 도와줘요. 목을 쓰지 않고 실제 서비스와 대화하기 때문에, 코드를 리팩터링해도 애플리케이션이 예상대로 동작하는지 검증할 수 있답니다.

PostgreSQL 외에도 Testcontainers for Python은 많은 SQL 데이터베이스, NoSQL 데이터베이스, 메시징 큐 등을 위한 모듈을 제공해요. 테스트에 필요한 어떤 컨테이너화된 의존성이든 Testcontainers로 실행할 수 있어요.

Testcontainers에 대해 더 알아보고 싶다면 Testcontainers 개요를 방문해요.

더 읽어보기 (Further reading)

  • testcontainers-python 문서
  • Testcontainers for Go 시작하기
  • Testcontainers for Java 시작하기
  • Testcontainers for Node.js 시작하기

더 알아보기 (Learn more)