FastAPI 3. 테스트 코드

김우진·2026년 3월 15일

FastAPI

목록 보기
4/6
post-thumbnail

FastAPI 섹션 3. 테스트 코드


1. 테스트 코드란?

시스템의 품질과 신뢰성을 검증하기 위한 코드입니다.

장점:

  • 코드 변경 → 기능 점검 과정을 자동화하여 반복 작업을 줄이고 생산성 향상
  • 시스템에 대한 안정성 확신 → 유연한 코드 변경 및 리팩터링 가능

심화 키워드: Unit Test, Integration Test, Regression Test, TDD, BDD 등 다양한 테스트 종류와 방법론이 존재합니다.


2. PyTest란?

테스트 코드 작성을 위한 Python 라이브러리입니다.

기본 Unittest 대비 장점:

  • 간결한 문법 (assert 문)
  • 함수 단위 테스트 지원
  • fixture 지원 (테스트 데이터 관리)

3. PyTest 세팅

pip install pytest       # 테스트 라이브러리
pip install httpx        # TestClient가 내부적으로 사용하는 HTTP 클라이언트
pip install pytest-mock  # mocker 기능 사용 (Mocking)

파일 구조:

src/
└── test/          ← Python Package로 생성
    └── test_*.py  ← 파일명 반드시 test_ 로 시작해야 PyTest가 인식함

4. Fixture란?

테스트마다 반복되는 공통 준비 작업을 함수로 분리하는 기능입니다.

예를 들어 TestClient를 매 테스트마다 만들지 않고, fixture로 한 번만 정의하고 재사용합니다.

# conftest.py
import pytest
from fastapi.testclient import TestClient
from main import app

@pytest.fixture
def client():
    # 이 함수가 반환하는 값이 테스트 함수의 인자로 자동 주입됨
    return TestClient(app=app)

@pytest.fixture 로 정의한 함수는 테스트 함수의 인자 이름과 일치하면 자동으로 주입됩니다.


5. Mocking이란?

실제 DB나 외부 서비스에 의존하지 않고, 가짜 데이터를 반환하도록 함수를 대체하는 기술입니다.

  • 테스트가 DB 상태에 영향을 받지 않아서 독립적이고 빠른 테스트 가능
  • mocker.patch("경로.함수명", return_value=가짜데이터) 형태로 사용

6. Health Check 테스트

from database.orm import ToDo

def test_health_check(client):
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"ping": "pong"}

서버가 정상적으로 실행되고 있는지 확인하는 가장 기본적인 테스트입니다.


7. GET 전체 조회 테스트

def test_get_todos(client, mocker):
    # mocker.patch로 실제 DB 조회 함수를 가짜 데이터로 대체
    # "main.get_todos" : main.py 안의 get_todos 함수를 mock으로 교체
    mocker.patch("main.get_todos", return_value=[
        ToDo(id=1, contents="FastAPI Section 0", is_done=True),
        ToDo(id=2, contents="FastAPI Section 1", is_done=False),
    ])

    # order=ASC (기본값)
    response = client.get("/todos")
    assert response.status_code == 200
    assert response.json() == {
        "todos": [
            {"id": 1, "contents": "FastAPI Section 0", "is_done": True},
            {"id": 2, "contents": "FastAPI Section 1", "is_done": False},
        ]
    }

    # order=DESC (순서 반전)
    response = client.get("/todos?order=DESC")
    assert response.status_code == 200
    assert response.json() == {
        "todos": [
            {"id": 2, "contents": "FastAPI Section 1", "is_done": False},
            {"id": 1, "contents": "FastAPI Section 0", "is_done": True},
        ]
    }

mocker.patch로 대체한 데이터를 기준으로 DESC는 단순히 순서를 뒤집은 결과를 검증합니다.


8. GET 단일 조회 테스트

def test_get_todo(client, mocker):
    # --- 200 성공 케이스 ---
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=ToDo(id=1, contents="todo", is_done=True),
    )
    response = client.get("/todos/1")
    assert response.status_code == 200
    assert response.json() == {"id": 1, "contents": "todo", "is_done": True}

    # --- 404 실패 케이스 ---
    # return_value=None : 해당 id의 ToDo가 없는 상황을 mock으로 재현
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=None,
    )
    response = client.get("/todos/1")
    assert response.status_code == 404
    assert response.json() == {"detail": "ToDo Not Found"}

하나의 테스트 함수 안에서 성공(200)과 실패(404) 케이스를 모두 검증합니다.
return_value=None으로 설정하면 핸들러에서 HTTPException이 발생하는 경로를 테스트할 수 있습니다.


9. POST 생성 테스트

def test_create_todo(client, mocker):
    # mocker.spy: 함수를 대체하지 않고 호출 결과를 추적(감시)하는 기능
    # ToDo.create 메서드가 어떤 값으로 호출됐는지 확인하기 위해 사용
    create_spy = mocker.spy(ToDo, "create")

    # create_todo(DB 저장 함수)는 mock으로 대체 → 실제 DB 저장 없이 결과만 반환
    mocker.patch(
        "main.create_todo",
        return_value=ToDo(id=1, contents="todo", is_done=True),
    )

    body = {
        "contents": "test",
        "is_done": False,
    }
    response = client.post("/todos", json=body)

    # spy_return: ToDo.create가 반환한 객체 (DB 저장 전 상태)
    # DB 저장 전이라 id는 None이어야 함
    assert create_spy.spy_return.id is None
    assert create_spy.spy_return.contents == "test"
    assert create_spy.spy_return.is_done is False

    # DB 저장 후 응답은 mock이 반환한 값 기준
    assert response.status_code == 201
    assert response.json() == {"id": 1, "contents": "todo", "is_done": True}

mocker.patch vs mocker.spy 차이:

  • mocker.patch : 함수를 완전히 가짜로 교체 (실제 함수 실행 안 함)
  • mocker.spy : 함수를 그대로 실행하되 호출 결과를 추적 (spy_return으로 확인 가능)

10. PATCH 수정 테스트

def test_update_todo(client, mocker):
    # --- 200 성공 케이스 ---
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=ToDo(id=1, contents="todo", is_done=True),
    )

    # mocker.patch.object: 특정 클래스의 메서드를 mock으로 교체
    # ToDo.undone() 이 실제로 호출됐는지 검증하기 위해 spy처럼 활용
    undone = mocker.patch.object(ToDo, "undone")

    mocker.patch(
        "main.update_todo",
        return_value=ToDo(id=1, contents="todo", is_done=False),
    )

    response = client.patch("/todos/1", json={"is_done": False})

    # is_done=False를 보냈으므로 undone()이 정확히 한 번 호출됐어야 함
    undone.assert_called_once_with()
    assert response.status_code == 200
    assert response.json() == {"id": 1, "contents": "todo", "is_done": False}

    # --- 404 실패 케이스 ---
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=None,
    )
    response = client.patch("/todos/1", json={"is_done": True})
    assert response.status_code == 404
    assert response.json() == {"detail": "ToDo Not Found"}

undone.assert_called_once_with() : mock으로 교체된 undone 메서드가 정확히 1번 호출됐는지 검증합니다.
이를 통해 is_done=False일 때 undone()이 실행되는 로직을 테스트할 수 있습니다.


11. DELETE 삭제 테스트

def test_delete_todo(client, mocker):
    # --- 204 성공 케이스 ---
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=ToDo(id=1, contents="todo", is_done=True),
    )
    # delete_todo는 반환값이 없으므로 return_value=None
    mocker.patch("main.delete_todo", return_value=None)

    response = client.delete("/todos/1")
    assert response.status_code == 204
    # 204 No Content: 성공했지만 응답 본문이 없으므로 json() 검증 없음

    # --- 404 실패 케이스 ---
    mocker.patch(
        "main.get_todo_by_todo_id",
        return_value=None,
    )
    response = client.delete("/todos/1")
    assert response.status_code == 404
    assert response.json() == {"detail": "ToDo Not Found"}

DELETE 성공 시 상태코드는 204 No Content로, 응답 본문이 없기 때문에 response.json() 검증을 하지 않습니다.


12. 전체 흐름 정리

테스트 함수 실행
     ↓
fixture(client) 자동 주입 → TestClient 생성
     ↓
mocker.patch로 DB 함수를 가짜 데이터로 대체
     ↓
client.get/post/patch/delete 로 API 호출
     ↓
response.status_code / response.json() 검증
     ↓
assert 통과 → 테스트 성공 ✅ / 실패 → 테스트 실패 ❌

13. 핵심 개념 요약

개념설명
@pytest.fixture테스트에서 공통으로 쓰는 객체를 한 번만 정의하고 재사용
mocker.patch함수를 가짜로 완전히 교체 (실제 실행 안 함)
mocker.spy함수를 실행하면서 결과를 추적
mocker.patch.object특정 클래스의 메서드를 mock으로 교체
assert_called_once_with()mock 함수가 정확히 1번 호출됐는지 검증
spy_returnspy로 추적한 함수의 반환값 확인

0개의 댓글