Pytest

김준곤·2026년 4월 19일

업무 중 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/


1. Unit/Integration test

pytest 설명에 앞서, pytest가 SDLC(Software Development LifeCycle)내에 어떻게 사용되는지 제시한다. SDLC내 test는 결함을 초기 발견, 수정 비용을 최소화 하는 핵심 장치이며 그 중 Unit/ Integration test에서 pytest를 도입한다.

1.1. Unit test

소프트웨어의 가장 작은 단위(함수, 메서드, 클래스)가 의도한 대로 정확히 작동하는지 독립적으로 검증하는 단계.

  • 목적: 개별 로직의 정확성 확인 및 리팩토링 시 사이드 이펙트 방지.
  • 특징
    • 고립성(Isolation): 외부 의존성(DB, 네트워크, 파일 시스템)을 완전히 배제.
    • Mocking 사용: 외부 요소는 가짜 객체(Mock/Stub)로 대체하여 오직 해당 "단위"의 로직만 테스트.
    • 속도: 실행 속도가 매우 빠르며, 개발자가 코드를 수정할 때마다 즉시 실행.

1.2. Integration test

단위 테스트를 통과한 개별 모듈들을 결합했을 때, 이들이 서로 올바르게 상호작용하는지 검증하는 단계.

  • 목적: 모듈 간 인터페이스 오류, 데이터 흐름의 정합성, 외부 시스템(DB 등)과의 연동 확인.
  • 특징
    • 연결성: 두 개 이상의 클래스나 컴포넌트가 결합된 상태를 테스트.
    • 현실성: 실제 DB 세션이나 캐시 서버 등을 일부 연동하여 테스트하는 경우 있음.
    • 복잡성: 단위 테스트보다 설정이 복잡하고 실행 시간이 상대적으로 긺.

2. Pytest

test에 필요한 다양한 기능을 제공하는 python library

2.1. How to work

2.1.1. 설정 로드 (Initialization)

가장 먼저 프로젝트의 설정 파일들을 찾아 환경을 구성.

  • pytest.ini, pyproject.toml, setup.cfg 등의 파일을 확인하여 기본 옵션(addopts), 테스트 경로 등을 로드
  • 환경 변수와 플러그인을 로드.

2.1.2. 테스트 수집 (Collection)

가정 먼저 수행되는 핵심 단계로, 어떤 파일을 테스트할지 결정.

  • 탐색 범위: 명시적인 경로가 지정되지 않으면 현재 디렉토리와 그 하위 디렉토리를 모두 조회.
  • 재귀적 탐색: test_.py 또는 _test.py 형태의 이름을 가진 파일을 찾음.
  • 필터링: 파일 내부에서 test로 시작하는 함수나 클래스(단, Test로 시작하는 클래스 내의 test 메서드)를 수집.

2.1.3. 테스트 실행 (Execution)

수집된 각 테스트 유닛을 하나씩 실행.

  • Fixture 적용: 함수 실행 전후에 필요한 리소스(DB 연결, Mocking 등)를 주입.
  • 검증: 코드 내 assert 문을 평가. 실패 시 해당 시점의 로컬 변수 값을 캡처하여 상세한 에러 로그를 생성.

2.1.4. 결과 리포팅 (Reporting)

모든 테스트가 끝나면 요약 결과를 터미널에 출력.

2.2. Configuration

2.2.1. pytest.toml

  • 파일이 비어있더라도 가장 우선 적용되는 설정 파일 .pytest.toml와 같이 숨김 파일로 적용 가능
  • version 9.0에 추가됨
# pytest.toml or .pytest.toml
[pytest]
minversion = "9.0"
addopts = ["-ra", "-q"]
testpaths = [
    "tests",
    "integration",
]

2.2.2. pytest.ini

  • pytest.toml 및 .pytest.toml를 제외하고 가장 우선 적용되는 설정 파일
  • .pytest.ini와 같이 숨김 파일로 적용 가능
# pytest.ini or .pytest.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
    tests
    integration

2.2.3. pyproject.toml

현대 파이썬 프로젝트(PEP 518)의 표준 설정 파일.

# pyproject.toml
[tool.pytest]
minversion = "9.0"
addopts = ["-ra", "-q"]
testpaths = [
    "tests",
    "integration",
]

2.2.4. tox.ini

tox 프로젝트의 설정 파일이며, 파일 내 section이 존재하는 경우 pytest 설정을 저장하는 데 사용 가능

# tox.ini
[pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
    tests
    integration

2.2.5. setup.cfg

범용 구성 파일로, setuptools에서 사용되었으며, section이 있는 경우 pytest 구성에도 사용 가능한 옵션

# setup.cfg
[tool:pytest]
minversion = 6.0
addopts = -ra -q
testpaths =
    tests
    integration

2.3. conftest.py

pytest의 환경 설정을 모듈화하고 자동화하는 전역 설정 파일, 본문 2.1.2. 단계에서 로드된다.

  • Fixture의 공유: 여러 테스트 파일(.py)에서 공통으로 사용하는 Fixture를 정의.
  • Hook 함수 구현: pytest의 기본 동작(테스트 수집, 리포팅 등)을 수정하거나 확장.
  • 외부 플러그인 로드: 특정 플러그인을 프로젝트 전역에 적용.
  • 디렉토리별 환경 설정: 각 디렉토리마다 conftest.py를 두어 해당 폴더 내 테스트에만 적용되는 규칙 정의.
    • 하위 디렉터리에 있는 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.

3. Decorator 사용 예시

3.1. @pytest.fixture

  • 테스트에 필요한 데이터, 상태, 혹은 외부 의존성(DB 연결, Mock 객체 등) 준비 및 테스트가 끝난 뒤 이를 정리할 수 있도록 함수에 사용하는 Decorator.
  • Decorator가 정의된 함수는 함수 명을 사용하여 인자 전달 가능하다.

3.1.1. LifeCycle

  1. 발견(Discovery): pytest가 테스트 함수를 실행하기 전, 매개변수 이름을 확인하고 동일한 이름을 가진 @pytest.fixture를 찾음.
  2. 실행(Setup): Fixture 함수가 실행. yield가 있다면 그 직전까지의 코드가 수행.
  3. 주입(Injection): Fixture의 반환값(return 또는 yield 뒤의 값)이 테스트 함수의 인자로 전달.
  4. 테스트 수행: 실제 테스트 로직(test_*)이 실행.
  5. 정리(Teardown): 테스트가 종료되면(성공/실패 무관), yield 이후의 코드가 실행되어 리소스를 정리.

3.1.2. Scope option

ScopeDescription
function(기본값) 각 테스트 함수마다 매번 실행됨.
class테스트 클래스 내의 메서드들이 공유함.
module하나의 .py 파일 내의 모든 테스트가 공유함.
package패키지(디렉토리) 단위로 공유함.
session전체 테스트 실행 기간 동안 단 한 번만 실행됨. (가장 빠름)

3.1.3. Setup/Teardown

아래 예시 코드처럼 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

3.2. @pytest.mark.parametrize

여러 개의 입력 데이터 세트를 사용하여 테스트할 수 있는 Decorator로 한 함수를 재활용하여 여러 Case를 테스트 할 수 있도록 한다.

3.3. @pytest.mark.skip

테스트를 무조건 건너뜀. 미구현 기능이나 잠시 비활성화해야 할 테스트에 사용한다.

@pytest.mark.skip(reason="아직 기능 구현 중임")
def test_not_ready():
  pass

3.4. @pytest.mark.skipif

특정 조건이 참일 때만 테스트를 건너뛴다. (예: 특정 OS, 특정 라이브러리 버전 등)

import sys

@pytest.mark.skipif(sys.platform == "win32", reason="리눅스 환경에서만 돌아가는 로직임")
def test_linux_only():
  pass

3.5. @pytest.mark.xfail

테스트가 실패할 것을 예상, 버그가 있음을 알고 있지만 아직 수정하지 않았을 때 사용한다.

  • 실패하면 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

3.6. @pytest.mark.usefixtures

테스트 함수 내에서 직접 인자로 사용할 필요는 없지만, 실행은 반드시 되어야 하는 Fixture가 있을 때 사용한다.
클래스 단위로 적용할 때 편리하다.

@pytest.mark.usefixtures("clean_db")
class TestUserAPI:
  def test_create(self):
      # clean_db 피스처를 인자로 받지 않아도 자동으로 실행됨
      pass

3.7. @pytest.mark.asyncio

pytest-asyncio 플러그인을 설치했을 때 사용 가능. 비동기(async def) 테스트 함수를 실행하기 위해 필수적이다.

@pytest.mark.asyncio
async def test_async_logic():
    result = await call_async_api()
    assert result == "success"

4. Ref.

profile
System Architecture를 목표하는 Engineer

0개의 댓글