| name | python-testing |
| description | Fixture, Parametrization, Mocking, 커버리지 분석, 비동기 테스트 및 테스트 조직을 포함하여 pytest를 사용한 Python 테스트 모범 사례. Python 테스트를 작성하거나 개선할 때 사용하세요.
|
| metadata | {"origin":"ECC","globs":["**/*.py","**/*.pyi"]} |
Python 테스트 (Python Testing)
이 스킬은 주요 테스트 프레임워크로 pytest를 사용하여 포괄적인 Python 테스트 패턴을 제공합니다.
테스트 프레임워크
강력한 기능과 깔끔한 구문을 가진 pytest를 테스트 프레임워크로 사용하세요.
기본 테스트 구조
def test_user_creation():
"""유효한 데이터로 사용자가 생성되는지 테스트"""
user = User(name="Alice", email="alice@example.com")
assert user.name == "Alice"
assert user.email == "alice@example.com"
assert user.is_active is True
테스트 발견 (Discovery)
pytest는 다음 규칙에 따라 테스트를 자동으로 찾습니다:
- 파일:
test_*.py 또는 *_test.py
- 함수:
test_*
- 클래스:
Test* (__init__ 메서드 제외)
- 메서드:
test_*
픽스처 (Fixtures)
픽스처는 재사용 가능한 테스트 설정(setup) 및 정리(teardown)를 제공합니다:
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def db_session():
"""테스트를 위한 데이터베이스 세션 제공"""
engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)
session = Session()
Base.metadata.create_all(engine)
yield session
session.close()
def test_user_repository(db_session):
"""db_session 픽스처를 사용하는 테스트"""
repo = UserRepository(db_session)
user = repo.create(name="Alice", email="alice@example.com")
assert user.id is not None
픽스처 범위 (Scopes)
@pytest.fixture(scope="function")
def user():
return User(name="Alice")
@pytest.fixture(scope="class")
def database():
db = Database()
db.connect()
yield db
db.disconnect()
@pytest.fixture(scope="module")
def app():
return create_app()
@pytest.fixture(scope="session")
def config():
return load_config()
픽스처 의존성
@pytest.fixture
def database():
db = Database()
db.connect()
yield db
db.disconnect()
@pytest.fixture
def user_repository(database):
"""database 픽스처에 의존하는 픽스처"""
return UserRepository(database)
def test_create_user(user_repository):
user = user_repository.create(name="Alice")
assert user.id is not None
매개변수화 (Parametrization)
@pytest.mark.parametrize를 사용하여 여러 입력을 테스트하세요:
import pytest
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("invalid-email", False),
("", False),
("user@", False),
("@example.com", False),
])
def test_email_validation(email, expected):
result = validate_email(email)
assert result == expected
여러 매개변수
@pytest.mark.parametrize("name,age,valid", [
("Alice", 25, True),
("Bob", 17, False),
("", 25, False),
("Charlie", -1, False),
])
def test_user_validation(name, age, valid):
result = validate_user(name, age)
assert result == valid
ID와 함께 매개변수화
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
], ids=["lowercase", "another_lowercase"])
def test_uppercase(input, expected):
assert input.upper() == expected
테스트 마커 (Test Markers)
테스트 분류 및 선택적 실행을 위해 마커를 사용하세요:
import pytest
@pytest.mark.unit
def test_calculate_total():
"""빠른 단위 테스트"""
assert calculate_total([1, 2, 3]) == 6
@pytest.mark.integration
def test_database_connection():
"""느린 통합 테스트"""
db = Database()
assert db.connect() is True
@pytest.mark.slow
def test_large_dataset():
"""매우 느린 테스트"""
process_million_records()
@pytest.mark.skip(reason="아직 구현되지 않음")
def test_future_feature():
pass
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Python 3.10 이상 필요")
def test_new_syntax():
pass
특정 마커 실행:
pytest -m unit
pytest -m "not slow"
pytest -m "unit or integration"
모킹 (Mocking)
unittest.mock 사용
from unittest.mock import Mock, patch, MagicMock
def test_user_service_with_mock():
"""모크 리포지토리를 사용한 테스트"""
mock_repo = Mock()
mock_repo.find_by_id.return_value = User(id="1", name="Alice")
service = UserService(mock_repo)
user = service.get_user("1")
assert user.name == "Alice"
mock_repo.find_by_id.assert_called_once_with("1")
@patch('myapp.services.EmailService')
def test_send_notification(mock_email_service):
"""패치된 의존성을 사용한 테스트"""
service = NotificationService()
service.send("user@example.com", "Hello")
mock_email_service.send.assert_called_once()
pytest-mock 플러그인
def test_with_mocker(mocker):
"""pytest-mock 플러그인 사용"""
mock_repo = mocker.Mock()
mock_repo.find_by_id.return_value = User(id="1", name="Alice")
service = UserService(mock_repo)
user = service.get_user("1")
assert user.name == "Alice"
커버리지 분석 (Coverage Analysis)
기본 커버리지
pytest --cov=src --cov-report=term-missing
HTML 커버리지 보고서
pytest --cov=src --cov-report=html
open htmlcov/index.html
커버리지 구성
[tool.pytest.ini_options]
addopts = """
--cov=src
--cov-report=term-missing
--cov-report=html
--cov-fail-under=80
"""
브랜치 커버리지
pytest --cov=src --cov-branch
비동기 테스트 (Async Testing)
비동기 함수 테스트
import pytest
@pytest.mark.asyncio
async def test_async_fetch_user():
"""비동기 함수 테스트"""
user = await fetch_user("1")
assert user.name == "Alice"
@pytest.fixture
async def async_client():
"""비동기 픽스처"""
client = AsyncClient()
await client.connect()
yield client
await client.disconnect()
@pytest.mark.asyncio
async def test_with_async_fixture(async_client):
result = await async_client.get("/users/1")
assert result.status == 200
테스트 조직 (Test Organization)
디렉토리 구조
tests/
├── unit/
│ ├── test_models.py
│ ├── test_services.py
│ └── test_utils.py
├── integration/
│ ├── test_database.py
│ └── test_api.py
├── conftest.py # 공유 픽스처
└── pytest.ini # 구성 파일
conftest.py
import pytest
@pytest.fixture(scope="session")
def app():
"""모든 테스트에서 사용 가능한 애플리케이션 픽스처"""
return create_app()
@pytest.fixture
def client(app):
"""테스트 클라이언트 픽스처"""
return app.test_client()
def pytest_configure(config):
"""커스텀 마커 등록"""
config.addinivalue_line("markers", "unit: Unit tests")
config.addinivalue_line("markers", "integration: Integration tests")
config.addinivalue_line("markers", "slow: Slow tests")
단언문 (Assertions)
기본 단언문
def test_assertions():
assert value == expected
assert value != other
assert value > 0
assert value in collection
assert isinstance(value, str)
더 나은 에러 메시지를 제공하는 pytest 단언문
def test_with_context():
"""pytest는 상세한 단언문 내부 검사를 제공합니다"""
result = calculate_total([1, 2, 3])
expected = 6
assert result == expected
커스텀 단언 메시지
def test_with_message():
result = process_data(input_data)
assert result.is_valid, f"유효한 결과를 기대했지만 에러 발생: {result.errors}"
근사치 비교
import pytest
def test_float_comparison():
result = 0.1 + 0.2
assert result == pytest.approx(0.3)
assert result == pytest.approx(0.3, abs=1e-9)
예외 테스트 (Exception Testing)
import pytest
def test_raises_exception():
"""함수가 예상된 예외를 발생시키는지 테스트"""
with pytest.raises(ValueError):
validate_age(-1)
def test_exception_message():
"""예외 메시지 테스트"""
with pytest.raises(ValueError, match="Age must be positive"):
validate_age(-1)
def test_exception_details():
"""예외 캡처 및 검사"""
with pytest.raises(ValidationError) as exc_info:
validate_user(name="", age=-1)
assert "name" in exc_info.value.errors
assert "age" in exc_info.value.errors
테스트 헬퍼 (Test Helpers)
def assert_user_equal(actual, expected):
"""커스텀 단언 헬퍼"""
assert actual.id == expected.id
assert actual.name == expected.name
assert actual.email == expected.email
def create_test_user(**kwargs):
"""테스트 데이터 팩토리"""
defaults = {
"name": "Test User",
"email": "test@example.com",
"age": 25,
}
defaults.update(kwargs)
return User(**defaults)
속성 기반 테스트 (Property-Based Testing)
hypothesis 라이브러리를 사용한 속성 기반 테스트:
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
"""덧셈의 교환 법칙 테스트"""
assert a + b == b + a
@given(st.lists(st.integers()))
def test_sort_idempotent(lst):
"""두 번 정렬해도 결과가 같은지 테스트 (멱등성)"""
sorted_once = sorted(lst)
sorted_twice = sorted(sorted_once)
assert sorted_once == sorted_twice
모범 사례
- 테스트당 하나의 단언 (가능한 경우)
- 설명적인 테스트 이름 사용 - 무엇이 테스트되는지 기술
- 준비-실행-단언(Arrange-Act-Assert) 패턴 - 명확한 테스트 구조
- 설정을 위해 픽스처 사용 - 중복 방지
- 외부 의존성 모킹 - 테스트 속도 및 격리 유지
- 엣지 케이스 테스트 - 빈 입력, None, 경계값 등
- parametrize 사용 - 여러 시나리오를 효율적으로 테스트
- 테스트 독립성 유지 - 테스트 간 상태 공유 금지
테스트 실행
pytest
pytest tests/test_user.py
pytest tests/test_user.py::test_create_user
pytest -v
pytest -s
pytest -n auto
pytest --lf
pytest --ff
이 스킬을 사용하는 시점
- 새로운 Python 테스트를 작성할 때
- 테스트 커버리지를 개선할 때
- pytest 인프라를 설정할 때
- 간헐적으로 실패하는(flaky) 테스트를 디버깅할 때
- 통합 테스트를 구현할 때
- 비동기 Python 코드를 테스트할 때