시스템의 품질과 신뢰성을 검증하기 위한 코드입니다.
장점:
코드 변경 → 기능 점검 과정을 자동화하여 반복 작업을 줄이고 생산성 향상심화 키워드: Unit Test, Integration Test, Regression Test, TDD, BDD 등 다양한 테스트 종류와 방법론이 존재합니다.
테스트 코드 작성을 위한 Python 라이브러리입니다.
기본 Unittest 대비 장점:
assert 문)pip install pytest # 테스트 라이브러리
pip install httpx # TestClient가 내부적으로 사용하는 HTTP 클라이언트
pip install pytest-mock # mocker 기능 사용 (Mocking)
파일 구조:
src/
└── test/ ← Python Package로 생성
└── test_*.py ← 파일명 반드시 test_ 로 시작해야 PyTest가 인식함
테스트마다 반복되는 공통 준비 작업을 함수로 분리하는 기능입니다.
예를 들어 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로 정의한 함수는 테스트 함수의 인자 이름과 일치하면 자동으로 주입됩니다.
실제 DB나 외부 서비스에 의존하지 않고, 가짜 데이터를 반환하도록 함수를 대체하는 기술입니다.
mocker.patch("경로.함수명", return_value=가짜데이터) 형태로 사용from database.orm import ToDo
def test_health_check(client):
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"ping": "pong"}
서버가 정상적으로 실행되고 있는지 확인하는 가장 기본적인 테스트입니다.
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는 단순히 순서를 뒤집은 결과를 검증합니다.
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이 발생하는 경로를 테스트할 수 있습니다.
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.patchvsmocker.spy차이:
mocker.patch: 함수를 완전히 가짜로 교체 (실제 함수 실행 안 함)mocker.spy: 함수를 그대로 실행하되 호출 결과를 추적 (spy_return으로 확인 가능)
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()이 실행되는 로직을 테스트할 수 있습니다.
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()검증을 하지 않습니다.
테스트 함수 실행
↓
fixture(client) 자동 주입 → TestClient 생성
↓
mocker.patch로 DB 함수를 가짜 데이터로 대체
↓
client.get/post/patch/delete 로 API 호출
↓
response.status_code / response.json() 검증
↓
assert 통과 → 테스트 성공 ✅ / 실패 → 테스트 실패 ❌
| 개념 | 설명 |
|---|---|
@pytest.fixture | 테스트에서 공통으로 쓰는 객체를 한 번만 정의하고 재사용 |
mocker.patch | 함수를 가짜로 완전히 교체 (실제 실행 안 함) |
mocker.spy | 함수를 실행하면서 결과를 추적 |
mocker.patch.object | 특정 클래스의 메서드를 mock으로 교체 |
assert_called_once_with() | mock 함수가 정확히 1번 호출됐는지 검증 |
spy_return | spy로 추적한 함수의 반환값 확인 |