[Python 시리즈 11편] pytest로 테스트 자동화하기

English version

pytest로 테스트 자동화하기

프로젝트 의존성과 환경을 정리했다면 이제 코드를 깨지 않도록 테스트를 자동화할 차례입니다. pytest는 간결한 문법과 확장성으로 사실상 표준 도구입니다. 이번 글에서는 "테스트 파일 만들기 → 픽스처로 중복 줄이기 → 모킹과 커버리지로 확장" 순서로 난도를 천천히 높입니다.

이번 글에서 새로 나오는 용어

  1. pytest: Python용 테스트 프레임워크로 assert만으로도 깔끔한 테스트를 작성하게 도와주는 도구
  2. 픽스처(Fixture): 테스트가 시작되기 전에 공통 데이터나 환경을 준비해 함수 인자로 주입하는 구성 요소
  3. 모킹(Mock): 실제 외부 API나 느린 작업을 가짜 객체로 바꿔 테스트를 빠르고 안전하게 만드는 방법
  4. 커버리지(Coverage): 테스트가 실제 코드 줄 중 얼마나 실행했는지 퍼센트로 보여주는 지표

개념 정리

학습 메모

  • 소요 시간: 60~70분
  • 준비물: 함수·클래스 구조, requests 기반 코드베이스, uv 환경
  • 학습 목표: pytest 설치 후 픽스처·모킹·커버리지 흐름을 한 번에 실행하기
  • 테스트는 기능 단위를 증명하는 자동 검증입니다.
  • 픽스처(fixture)는 테스트 전에 필요한 데이터·환경을 준비하는 함수입니다.
  • 모킹(mocking)은 외부 API나 파일처럼 느린 의존성을 가짜로 바꾸는 기법입니다.
  • 커버리지(coverage)는 테스트가 실제 코드 라인을 얼마나 실행했는지 비율로 알려줍니다.
  • 고급 섹션은 "Optional"로 표시했으니 우선 핵심 섹션만 통과해도 충분합니다.

코드로 이해하기

설치와 기본 실행 (Core)

uv add --dev pytest pytest-cov
uv run pytest

프로젝트 루트에서 tests/ 디렉터리를 만들고 test_*.py 파일을 추가하면 자동으로 인식됩니다.

첫 번째 테스트 (Core)

# tests/test_app.py
from mealbot.meal import Meal


def test_to_markdown_formats_bullets():
    meal = Meal(menu=["돈까스", "샐러드"])
    assert meal.to_markdown() == "- 돈까스\n- 샐러드"

단순한 assert 구문만으로 기대값을 비교할 수 있고, 실패 시 diff를 자세히 보여줍니다.

픽스처로 공통 준비 구간 추출 (Core)

반복되는 준비 코드를 함수로 빼 놓으면 테스트가 길어지지 않습니다. 처음에는 딕셔너리 하나를 반환하는 정도로 가볍게 시작하세요.



@pytest.fixture
def menu_payload():
    return {"menu": ["비빔밥", "미역국"], "calories": 640}


def test_from_json(menu_payload):
    meal = Meal.from_json(json.dumps(menu_payload, ensure_ascii=False))
    assert meal.calories == 640

픽스처는 테스트 함수 인자 이름으로 자동 주입되므로 재사용과 가독성이 높습니다.

🧪 픽스처란? 테스트 실행 전에 필요한 데이터나 환경을 준비하는 함수입니다. @pytest.fixture 데코레이터를 붙이면 테스트 인자로 바로 주입할 수 있습니다.

외부 API 모킹 (Core → Plus)

이제 외부 API를 흉내 내며 테스트를 더 안전하게 만드는 단계입니다. "진짜 요청 대신 가짜 응답을 돌려준다"는 생각만 잡으면 됩니다.

from unittest.mock import Mock


def test_fetch_meal_calls_request(monkeypatch):
    mock_response = Mock()
    mock_response.json.return_value = {"menu": ["카레"]}
    mock_response.raise_for_status.return_value = None

    def fake_get(url, timeout):
        assert "api" in url
        return mock_response

    monkeypatch.setattr("mealbot.fetcher.requests.get", fake_get)

    data = fetch_meal("demo-school")
    assert data["menu"] == ["카레"]

monkeypatch 픽스처는 런타임 중 함수를 교체해 네트워크 호출을 차단했습니다. 이 패턴은 외부 서비스나 환경 변수를 모킹할 때도 유용합니다.

🔧 monkeypatch 용도 테스트 중 모듈 속성이나 환경 변수를 일시적으로 바꾸는 pytest 내장 픽스처입니다. 테스트가 끝나면 원래 상태로 복원됩니다.

매개변수화로 사례 확장 (Optional)

한 함수가 여러 입력을 받아야 할 때는 @pytest.mark.parametrize가 편합니다. 아직 테스트가 많지 않다면 이 부분은 나중에 다시 읽어도 괜찮습니다.



@pytest.mark.parametrize(
    "calories,expected",
    [
        (0, "정보 없음"),
        (800, "800 kcal"),
    ],
)
def test_calorie_label(calories, expected):
    assert format_calorie_label(calories) == expected

하나의 테스트 함수로 여러 입력을 검증해 케이스 누락을 줄입니다.

커버리지와 CI 연동 (Optional)

커버리지는 학습 막바지에 보는 보너스입니다. 필수는 아니지만, 테스트가 어떤 줄을 놓쳤는지 눈으로 확인하고 싶은 순간에 큰 도움이 됩니다.

uv run pytest --cov=mealbot --cov-report=term-missing

term-missing 옵션을 사용하면 테스트되지 않은 라인을 즉시 확인할 수 있습니다. GitHub Actions에서는 pytest --cov 결과를 업로드해 PR에 자동 코멘트를 달 수 있습니다.

왜 중요할까

pytest 흐름을 익히면 리팩터링이나 배포 전에 안전망을 확보할 수 있습니다. 픽스처와 모킹이 있으면 외부 API, 파일 시스템, 환경 변수를 제어한 상태에서 빠르게 검증할 수 있습니다. 커버리지 보고서를 보면 눈에 띄지 않던 빈틈도 한눈에 발견할 수 있습니다.

실습

  • 따라 하기: uv run pytest를 실행해 첫 테스트를 통과시키고 실패 시 출력되는 diff를 캡처합니다.
  • 확장하기: @pytest.mark.parametrize와 커버리지 옵션을 동시에 적용해 입력 케이스를 늘리고 리포트를 확인합니다.
  • 디버깅: monkeypatch로 잘못된 값을 반환하게 만들어 테스트를 실패시킨 뒤 픽스처를 수정해 통과시키며 원인 분석 문장을 남깁니다.
  • 완료 기준: 픽스처·모킹·커버리지 명령을 연속으로 실행해 녹색 체크와 보고서를 모두 확인했을 때입니다.

마무리

테스트는 기능을 작은 단위로 증명해 주고, 리팩터링이나 배포 전에 신뢰도를 제공합니다. 다음 편에서는 지금까지 만든 도구들을 묶어 간단한 CLI 자동화 앱을 완성하고, 언어 자체의 심화 주제로 넘어갈 준비를 마치겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요