업무 중 FastAPI endpoint test를 작성할 일이 생겨서, lamp up 개념으로 python의 test framework를 조사하게 되었다.
공식 문서와 AI를 적극 활용해보았고, 내용 중 생략되거나 틀린 내용이 있을 수 있다. 피드백은 언제든 환영이다.
참고로 endpoint test이기 때문에 순수 pytest framework만을 사용한 것은 아니다. FastAPI에서 제시하는 TestClient 객체를 활용하여 API request를 생성하고, 응답 값을 확인하는 식의 코드를 작성해야 할 것이다. 따라서 아래 문서를 참고하여 진행하였다. unit test라면 넘어가도 되는 내용이다.
https://fastapi.tiangolo.com/reference/testclient/
pytest 설명에 앞서, pytest가 SDLC(Software Development LifeCycle)내에 어떻게 사용되는지 제시한다. SDLC내 test는 결함을 초기 발견, 수정 비용을 최소화 하는 핵심 장치이며 그 중 Unit/ Integration test에서 pytest를 도입한다.
소프트웨어의 가장 작은 단위(함수, 메서드, 클래스)가 의도한 대로 정확히 작동하는지 독립적으로 검증하는 단계.
단위 테스트를 통과한 개별 모듈들을 결합했을 때, 이들이 서로 올바르게 상호작용하는지 검증하는 단계.
test에 필요한 다양한 기능을 제공하는 python library
가장 먼저 프로젝트의 설정 파일들을 찾아 환경을 구성.
가정 먼저 수행되는 핵심 단계로, 어떤 파일을 테스트할지 결정.
수집된 각 테스트 유닛을 하나씩 실행.
모든 테스트가 끝나면 요약 결과를 터미널에 출력.
pytest.toml.pytest.toml와 같이 숨김 파일로 적용 가능# pytest.toml or .pytest.toml
[pytest]
minversion = "9.0"
addopts = ["-ra", "-q"]
testpaths = [
"tests",
"integration",
]
pytest.ini.pytest.ini와 같이 숨김 파일로 적용 가능# pytest.ini or .pytest.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
tests
integration
pyproject.toml현대 파이썬 프로젝트(PEP 518)의 표준 설정 파일.
# pyproject.toml
[tool.pytest]
minversion = "9.0"
addopts = ["-ra", "-q"]
testpaths = [
"tests",
"integration",
]
tox.initox 프로젝트의 설정 파일이며, 파일 내 section이 존재하는 경우 pytest 설정을 저장하는 데 사용 가능
# tox.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
tests
integration
setup.cfg범용 구성 파일로, setuptools에서 사용되었으며, section이 있는 경우 pytest 구성에도 사용 가능한 옵션
# setup.cfg
[tool:pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
tests
integration
conftest.pypytest의 환경 설정을 모듈화하고 자동화하는 전역 설정 파일, 본문 2.1.2. 단계에서 로드된다.
.py)에서 공통으로 사용하는 Fixture를 정의.conftest.py는 상위 디렉터리의 conftest.py를 상속 받으며 test 파일에서 가까운 순으로 설정이 우선 적용# example
project/
├── conftest.py (A) -> fixture "db_url" = "prod_db" # <- 그 이후에 해당(상위) 파일이 적용
└── tests/
├── conftest.py (B) -> fixture "db_url" = "test_db" # <- 가장 가까운 conftest.py가 우선 적용
└── test_api.py # <- 해당 파일이 test file.
@pytest.fixture@pytest.fixture를 찾음.yield가 있다면 그 직전까지의 코드가 수행.return 또는 yield 뒤의 값)이 테스트 함수의 인자로 전달.test_*)이 실행.yield 이후의 코드가 실행되어 리소스를 정리.| Scope | Description |
|---|---|
| function | (기본값) 각 테스트 함수마다 매번 실행됨. |
| class | 테스트 클래스 내의 메서드들이 공유함. |
| module | 하나의 .py 파일 내의 모든 테스트가 공유함. |
| package | 패키지(디렉토리) 단위로 공유함. |
| session | 전체 테스트 실행 기간 동안 단 한 번만 실행됨. (가장 빠름) |
아래 예시 코드처럼 fixture 함수가 작성되었을 때, yield 키워드를 기준으로 이전에는 테스트에 필요한 환경 설정(setup), 이후에는 설정한 내용 정리(teardown/cleanup)이 동작
@pytest.fixture(scope="function")
def init_db():
# Setup
db = db_connect()
yield db
db.close()
def test_db_insert(init_db):
ret = init_db.insert()
assert ret
여러 개의 입력 데이터 세트를 사용하여 테스트할 수 있는 Decorator로 한 함수를 재활용하여 여러 Case를 테스트 할 수 있도록 한다.
테스트를 무조건 건너뜀. 미구현 기능이나 잠시 비활성화해야 할 테스트에 사용한다.
@pytest.mark.skip(reason="아직 기능 구현 중임")
def test_not_ready():
pass
특정 조건이 참일 때만 테스트를 건너뛴다. (예: 특정 OS, 특정 라이브러리 버전 등)
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="리눅스 환경에서만 돌아가는 로직임")
def test_linux_only():
pass
테스트가 실패할 것을 예상, 버그가 있음을 알고 있지만 아직 수정하지 않았을 때 사용한다.
XFAIL(기대했던 실패)로 표시되고 전체 결과는 Pass로 처리.XPASS(기대 안 했는데 성공)로 표시.import pytest
# 아직 구현되지 않은 기능 (예: 랭킹 시스템의 특정 계산 로직)
def calculate_rank_bonus(score):
# 아직 로직이 작성되지 않아 에러가 발생한다고 가정
raise NotImplementedError("보너스 계산 로직 미구현")
class TestGameLogic:
# 1. 가장 기본적인 사용법
@pytest.mark.xfail(reason="보너스 점수 시스템이 아직 기획 단계임")
def test_bonus_calculation(self):
assert calculate_rank_bonus(100) == 10
# 2. 특정 조건에서만 실패가 예상될 때 (condition)
@pytest.mark.xfail(raises=ZeroDivisionError, reason="0으로 나누기 버그 수정 중")
def test_division_logic(self):
# 이 테스트는 ZeroDivisionError가 발생하면 XFAIL로 깔끔하게 넘어감
result = 1 / 0
assert result == 1
# 3. strict 모드 (실패해야만 하는 테스트)
# 만약 테스트가 '성공'해버리면 오히려 Fail을 발생시켜 알려줌
@pytest.mark.xfail(strict=True, reason="이 로직은 반드시 실패해야 함. 성공하면 태그 삭제 필요.")
def test_must_fail(self):
# 만약 누군가 내부 로직을 고쳐서 이게 성공(True)하게 되면,
# pytest는 이 테스트를 '실패(FAILED)'로 간주하여 개발자에게 알림
assert True
테스트 함수 내에서 직접 인자로 사용할 필요는 없지만, 실행은 반드시 되어야 하는 Fixture가 있을 때 사용한다.
클래스 단위로 적용할 때 편리하다.
@pytest.mark.usefixtures("clean_db")
class TestUserAPI:
def test_create(self):
# clean_db 피스처를 인자로 받지 않아도 자동으로 실행됨
pass
pytest-asyncio 플러그인을 설치했을 때 사용 가능. 비동기(async def) 테스트 함수를 실행하기 위해 필수적이다.
@pytest.mark.asyncio
async def test_async_logic():
result = await call_async_api()
assert result == "success"