소프트웨어 개발/백엔드

📚[FastAPI] 6장. 테스트 및 디버깅: Pytest 활용과 품질 보증

브라더댄 2025. 1. 26. 14:06
728x90
반응형
SMALL

안녕하세요! 이번 글에서는 FastAPI 애플리케이션의 테스트(Test)디버깅(Debugging) 전략을 살펴보겠습니다. 이전 장들에서 RESTful API, 인증, DB 연동 등 백엔드의 주요 기능들을 구현했는데요, 제대로 작동하는지 신뢰성을 확보하기 위해서는 자동화된 테스트가 필수입니다.

특히 Python 에서 많이 사용되는 Pytest와 FastAPI 자체 기능인 TestClient를 통해, API 통합 테스트부터 단위(Unit) 테스트까지 체계적으로 커버하는 방법을 소개합니다. 또한 디버깅 기법, 로깅(Logging) 설정 등 품질을 높이는 다양한 팁도 함께 알아보겠습니다.


6.1. 테스트의 중요성

6.1.1. 왜 테스트가 필요한가?

  • 신뢰성(Confidence) 확보: 코드 수정이나 리팩토링 후에도 기존 기능이 잘 동작함을 자동으로 확인할 수 있습니다.
  • 회귀(Regression) 방지: 과거에 해결된 버그가 재발하지 않도록 모니터링할 수 있습니다.
  • 개발 속도 향상: 수동 테스트 시간을 크게 절약할 수 있으며, CI/CD와 결합해 배포 파이프라인을 자동화할 수 있습니다.

6.1.2. 테스트 방식

  • 단위 테스트(Unit Test): 함수나 메서드 단위로 가장 작은 단위를 검증
  • 통합 테스트(Integration Test): 여러 컴포넌트(DB, 외부 서비스 등)가 조합되었을 때의 동작 확인
  • 엔드 투 엔드(E2E) 테스트: 실제 사용자 시나리오를 끝에서 끝까지 시뮬레이션

6.2. Pytest 환경 설정

6.2.1. Poetry로 Pytest 설치

poetry add --group dev pytest pytest-cov
  • pytest: Python 테스트 프레임워크
  • pytest-cov: 코드 커버리지 측정 도구

TIP: 별도 개발 의존성 그룹(--group dev)을 지정하면, 프로덕션 빌드 시 불필요한 패키지를 제외할 수 있습니다.

6.2.2. 디렉토리 구조 예시

프로젝트 루트 혹은 app/와 같은 위치에 tests/ 폴더를 두고, 모듈별 또는 기능별로 테스트 파일을 분리합니다.

app/
├── api/
│   └── ...
├── core/
│   └── ...
├── tests/
│   ├── test_user_api.py
│   ├── test_auth.py
│   ├── conftest.py
│   └── ...
└── main.py
  • conftest.py: Pytest에서 공통 Fixture나 설정을 정의하는 파일

6.2.3. Pytest 실행 방법

# 기본 실행
poetry run pytest

# 특정 파일만 실행
poetry run pytest tests/test_user_api.py

# 커버리지 보고
poetry run pytest --cov=app --cov-report=html

6.3. 기본 테스트 코드 구조

아래는 Pytest에서 흔히 사용하는 테스트 코드의 기본 틀입니다.

def test_something():
    # 1. 준비(Arrange): 필요한 데이터 혹은 환경 세팅
    input_value = 10
    # 2. 실행(Act): 실제 로직 실행
    result = some_function(input_value)
    # 3. 검증(Assert): 기대 결과와 일치하는지 확인
    assert result == 20
  • 함수명이 test_로 시작하거나 _test로 끝나야 Pytest가 자동으로 인식합니다.
  • assert를 통해 결과를 검증하며, 실패 시 테스트가 깨지고 이유를 보여줍니다.

6.4. FastAPI TestClient 활용

6.4.1. TestClient란?

FastAPI는 fastapi.testclient.TestClient 클래스를 제공하여, 가상 서버 형태로 API 호출을 테스트할 수 있습니다. 실제 서버를 띄우지 않아도 되므로, 빠르고 간편하게 통합 테스트를 수행할 수 있습니다.

# tests/test_user_api.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_user():
    response = client.post(
        "/api/v1/users",
        json={"email": "test@example.com", "password": "123456"}
    )
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"
  • client.post(), client.get() 등 HTTP 메서드별 함수를 호출해 라우터에 요청을 보냅니다.
  • response.status_code, response.json() 등을 통해 응답을 검증합니다.

6.4.2. 인증 토큰 테스트 예시

사용자 인증이 필요한 엔드포인트라면, JWT 토큰을 발급받아 headers에 넣고 다시 요청해야 합니다.

def test_user_me():
    # 1) 로그인 -> 토큰 획득
    login_resp = client.post(
        "/api/v1/auth/login",
        data={"username": "test@example.com", "password": "123456"}
    )
    assert login_resp.status_code == 200
    token_data = login_resp.json()
    access_token = token_data["access_token"]

    # 2) 보호된 엔드포인트 호출
    resp = client.get(
        "/api/v1/users/me",
        headers={"Authorization": f"Bearer {access_token}"}
    )
    assert resp.status_code == 200
    user_data = resp.json()
    assert user_data["email"] == "test@example.com"

6.5. Mocking & Fixture

6.5.1. 왜 Mocking이 필요한가?

  • 외부 API, DB, 메시지 큐 같은 의존성을 직접 호출하면 테스트 속도가 느려지고, 실패 요인이 늘어날 수 있습니다.
  • Mock 객체로 외부 의존성을 가짜로 대체하여, 특정 상황(예: 에러 응답)을 재현하고 테스트를 격리(Isolation)할 수 있습니다.

6.5.2. Pytest Fixture

  • Fixture는 반복적으로 사용되는 리소스(예: DB 세션, 임시 디렉토리)를 자동으로 준비하고 해제하는 메커니즘입니다.
  • conftest.py에서 공통 Fixture를 정의하면, 여러 테스트 파일에서 쉽게 재사용 가능합니다.
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db.base import Base

TEST_DB_URL = "sqlite:///:memory:"

@pytest.fixture(scope="session")
def db_engine():
    engine = create_engine(TEST_DB_URL)
    Base.metadata.create_all(bind=engine)
    yield engine
    engine.dispose()

@pytest.fixture()
def db_session(db_engine):
    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine)
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()
  • 이렇게 하면, 테스트 함수에서 db_session을 매개변수로 주입(Depends)받아 DB 연산을 수행할 수 있습니다.

6.6. 코드 커버리지(Coverage)

6.6.1. 커버리지 측정

**pytest-cov**를 사용하면 테스트가 코드 베이스의 어느 부분까지 커버하는지를 확인할 수 있습니다.

poetry run pytest --cov=app --cov-report=term-missing
  • --cov=app: app 디렉토리 내의 파일들을 커버리지 대상으로 삼음
  • --cov-report=term-missing: 터미널에 함수별 미커버 부분이 표시됨
  • --cov-report=html: HTML 리포트를 생성하여 브라우저로 상세 분석 가능

6.6.2. 커버리지 목표

  • 이상적으로는 테스트 커버리지가 80~90% 이상이 되도록 노력하지만, 수치 자체에만 집중하기보다는 중요 로직 위주로 충분히 검증하는 것이 핵심입니다.
  • DB 연동, 예외 처리, 인증 로직 등 치명적 기능은 반드시 테스트로 커버해주세요.

6.7. 디버깅 및 로깅(Logging)

6.7.1. 디버깅 기법

  1. IDE 디버거 (VS Code, PyCharm 등)
    • 브레이크포인트를 걸고 코드를 한 줄씩 실행하여 변수 상태 확인
  2. 프린트(Logging)
    • 간단한 상황에선 print()가 빠르고 유용
  3. Remote Debugging
    • Docker 컨테이너나 원격 서버에서 PyCharm/VS Code 원격 디버깅 기능을 설정해 디버그 가능

6.7.2. Python 로깅 설정

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)

def some_function():
    logger.info("Function is called")
    ...
  • FastAPI + Uvicorn을 사용할 때는 Uvicorn의 로깅 설정을 커스텀하거나, Python logging 모듈을 사용하는 방식을 병행할 수 있습니다.
  • 로그 레벨(DEBUG, INFO, WARNING, ERROR)을 적절히 설정해 개발/운영 환경에서 필요한 정보만 추적하도록 조정하세요.

6.8. 실제 활용 사례

6.8.1. 전자상거래(E-Commerce)

  • 결제 로직 테스트 시, 실제 결제 API(외부 PG사)를 호출하지 않고 Mock 객체를 사용해 가짜 승인 응답을 주고받음
  • 할인 정책이나 쿠폰 코드 등 조건이 복잡한 로직을 단위 테스트로 커버
  • CI 단계에서 pytest --cov를 실행해 커버리지를 측정, PR 머지 전 자동 검증

6.8.2. 대규모 사내 ERP 시스템

  • 각 모듈(인사, 재무, 영업 등)이 서로 연동될 때, 통합 테스트로 데이터가 일관성 있게 흐르는지 확인
  • 디버깅 시, 개발 환경에서는 상세 로그를 남기고, 운영 환경에서는 최소 WARN급 이상만 출력
  • SentryDatadog 등의 모니터링 툴과 연결해 예외 발생 시 Slack 알림 또는 이슈 트래커에 자동 등록

6.8.3. 실시간 서비스(채팅, 게임 등)

  • REST API 뿐 아니라 WebSocket 통신, 비동기 작업(Celery) 등도 별도 테스트 케이스로 작성
  • Mocking으로 외부 서비스(예: 메시지 브로커, Redis) 의존성을 격리
  • 단위 테스트에서는 메시지 전송 로직, 이벤트 처리 로직이 의도대로 동작하는지 검증

모범 사례

  1. 테스트 코드의 가독성: Arrange-Act-Assert 패턴을 따르고, 테스트 함수/클래스 이름을 명확히
  2. 지속적 통합(CI): GitHub Actions, GitLab CI, Jenkins 등으로 Pull Request 시 자동으로 테스트 실행
  3. 비결정적 요소 제거: 타임아웃, 랜덤 값, 외부 의존성은 Mock/Fixture로 통제해 테스트 안정성 확보
  4. 로깅 레벨: 개발 및 운영 환경에서 다른 로깅 레벨을 사용해 성능과 추적력을 모두 잡기

6.9. 마무리

이번 6장에서는 테스트 및 디버깅을 통해 FastAPI 프로젝트의 품질을 보증하는 과정을 다뤘습니다. 주요 내용은 다음과 같습니다:

  1. Pytest 설치 및 환경 설정, 기본 구조
  2. FastAPI TestClient를 활용한 API 통합 테스트
  3. Mocking과 Fixture로 외부 의존성을 제어
  4. 코드 커버리지 측정과 최적화 전략
  5. 디버깅 기법(IDE 브레이크포인트, 로깅) 소개

다음 장에서는 더 나아가 **배포(Docker, CI/CD 등)**나 비동기 처리(Celery) 관련 내용이 이어질 텐데요, 탄탄한 테스트 환경을 갖추고 있어야 프로덕션 변경 시에도 안심하고 배포할 수 있습니다. 이번 장에서 배운 방법들로 자동화된 테스트 체계를 마련해, 팀 전체의 생산성과 서비스 신뢰도를 높이시길 바랍니다.

이상으로 6장을 마무리합니다. 도움이 되셨다면 좋아요와 댓글 남겨주시고, 추가적인 질문이나 더 다루었으면 하는 주제가 있다면 언제든 말씀해주세요. 다음 글에서 뵙겠습니다!

728x90
반응형
LIST