Loading...
Loading...
02-reusable-code-python/testing/pytest_patterns.py
"""
pytest 패턴 - fixture, conftest, parametrize, mock 패턴 모음
@source GitHub-커뮤니티
@extracted 2026-02-16
@version 1.0.0
의존성:
- pytest (필수, pip install pytest)
- pytest-asyncio (권장, pip install pytest-asyncio)
- httpx (권장, 테스트 클라이언트용)
사용법:
이 파일은 패턴 레퍼런스입니다.
conftest.py에 필요한 fixture를 복사하여 사용하세요.
"""
# ============================================
# conftest.py 패턴
# ============================================
"""
# conftest.py 예제
import asyncio
from collections.abc import AsyncGenerator, Generator
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from app.main import app
from app.database import engine, get_db_session
# 1. 이벤트 루프 (세션 범위)
@pytest.fixture(scope="session")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
loop = asyncio.new_event_loop()
yield loop
loop.close()
# 2. 비동기 테스트 클라이언트
@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
) as ac:
yield ac
# 3. DB 세션 (테스트별 트랜잭션 롤백)
@pytest_asyncio.fixture
async def db_session():
async with engine.begin() as conn:
session = get_db_session(conn)
yield session
await conn.rollback() # 테스트 후 롤백
# 4. 인증된 클라이언트
@pytest_asyncio.fixture
async def auth_client(client: AsyncClient) -> AsyncClient:
# 테스트 사용자 생성 + 토큰 발급
response = await client.post("/api/auth/login", json={
"email": "test@test.com",
"password": "testpass123",
})
token = response.json()["token"]
client.headers["Authorization"] = f"Bearer {token}"
return client
# 5. 팩토리 fixture (동적 데이터 생성)
@pytest.fixture
def user_factory():
counter = 0
def _create(**overrides):
nonlocal counter
counter += 1
defaults = {
"name": f"테스트유저{counter}",
"email": f"test{counter}@test.com",
"role": "user",
}
defaults.update(overrides)
return defaults
return _create
"""
# ============================================
# parametrize 패턴
# ============================================
"""
# 기본 parametrize
@pytest.mark.parametrize(
"input_value, expected",
[
("hello", "HELLO"),
("world", "WORLD"),
("한글", "한글"),
],
)
def test_uppercase(input_value: str, expected: str):
assert input_value.upper() == expected
# ID 지정 (테스트 이름에 표시)
@pytest.mark.parametrize(
"email, is_valid",
[
pytest.param("user@test.com", True, id="valid-email"),
pytest.param("invalid", False, id="no-at-sign"),
pytest.param("@test.com", False, id="no-local-part"),
pytest.param("user@", False, id="no-domain"),
],
)
def test_email_validation(email: str, is_valid: bool):
assert validate_email(email) == is_valid
# 여러 parametrize 조합
@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"])
@pytest.mark.parametrize("auth", [True, False])
async def test_api_methods(client, method, auth):
headers = {"Authorization": "Bearer token"} if auth else {}
response = await client.request(method, "/api/resource", headers=headers)
if not auth:
assert response.status_code == 401
"""
# ============================================
# Mock 패턴
# ============================================
"""
from unittest.mock import AsyncMock, MagicMock, patch
# 1. 함수 mock
def test_with_mock():
with patch("app.services.send_email") as mock_send:
mock_send.return_value = True
result = process_order(order)
mock_send.assert_called_once_with(
to="user@test.com",
subject="주문 확인",
)
# 2. 비동기 함수 mock
async def test_async_mock():
with patch("app.services.fetch_user", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = {"id": "1", "name": "홍길동"}
result = await get_user_profile("1")
assert result["name"] == "홍길동"
# 3. 외부 API mock (httpx)
async def test_external_api(client):
with patch("httpx.AsyncClient.get", new_callable=AsyncMock) as mock_get:
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"data": "mocked"},
)
response = await client.get("/api/proxy/external")
assert response.status_code == 200
# 4. 환경변수 mock
def test_with_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key")
monkeypatch.setenv("DEBUG", "true")
# 환경변수가 설정된 상태에서 테스트
# 5. datetime mock
def test_with_frozen_time():
from unittest.mock import patch
from datetime import datetime
fixed_now = datetime(2026, 2, 16, 12, 0, 0)
with patch("app.utils.datetime") as mock_dt:
mock_dt.now.return_value = fixed_now
result = get_current_timestamp()
assert result == fixed_now
"""
# ============================================
# 테스트 유틸리티 함수
# ============================================
def assert_dict_contains(actual: dict, expected: dict) -> None:
"""딕셔너리가 기대 키-값을 포함하는지 확인
Args:
actual: 실제 딕셔너리
expected: 포함해야 하는 키-값
Example:
response_data = {"id": "1", "name": "홍길동", "email": "hong@test.com"}
assert_dict_contains(response_data, {"name": "홍길동", "email": "hong@test.com"})
"""
for key, value in expected.items():
assert key in actual, f"키 '{key}'가 딕셔너리에 없습니다"
assert actual[key] == value, (
f"키 '{key}': 기대값 {value!r}, 실제값 {actual[key]!r}"
)
def assert_status_and_json(response, expected_status: int, expected_data: dict | None = None):
"""HTTP 응답 상태 코드와 JSON 검증
Args:
response: httpx/TestClient 응답
expected_status: 기대 HTTP 상태 코드
expected_data: 기대 JSON 데이터 (부분 매칭)
Example:
response = await client.get("/api/users/1")
assert_status_and_json(response, 200, {"name": "홍길동"})
"""
assert response.status_code == expected_status, (
f"상태 코드: 기대 {expected_status}, 실제 {response.status_code}. "
f"응답: {response.text}"
)
if expected_data is not None:
data = response.json()
assert_dict_contains(data, expected_data)
async def create_test_user(
client,
*,
name: str = "테스트유저",
email: str = "test@test.com",
password: str = "testpass123",
) -> dict:
"""테스트용 사용자 생성 헬퍼
Args:
client: httpx AsyncClient
name: 사용자 이름
email: 이메일
password: 비밀번호
Returns:
생성된 사용자 데이터
Example:
user = await create_test_user(client, name="관리자", email="admin@test.com")
"""
response = await client.post("/api/users", json={
"name": name,
"email": email,
"password": password,
})
assert response.status_code in (200, 201), f"사용자 생성 실패: {response.text}"
return response.json()