FastAPI 테스트 - 단위 테스트, 통합 테스트, E2E 테스트, TDD
pytest로 FastAPI 애플리케이션의 단위 테스트, 통합 테스트, E2E 테스트를 작성하는 방법과 TDD 개발 방식을 이해한다.
왜 테스트를 작성하는가
코드를 수정할 때마다 직접 API를 호출해서 결과를 눈으로 확인하는 방식은 한계가 있다.
- 기능이 늘어날수록 확인해야 할 케이스가 기하급수적으로 증가한다
- "이 부분을 고쳤더니 저 부분이 깨졌다"는 상황을 코드 배포 전에 잡지 못한다
- 팀원이 코드를 이해할 때 테스트가 살아있는 문서 역할을 한다
테스트는 코드 품질을 유지하는 안전망이다.
테스트의 종류
E2E 테스트 실제 브라우저/HTTP 클라이언트로 전체 흐름 검증
▲
통합 테스트 여러 계층(라우터 + 서비스 + DB)을 묶어서 검증
▲
단위 테스트 함수/클래스 하나를 독립적으로 검증
| 구분 | 범위 | 속도 | 신뢰도 |
|---|---|---|---|
| 단위 테스트 | 함수 하나 | 매우 빠름 | 부분적 |
| 통합 테스트 | 라우터 + DB | 중간 | 높음 |
| E2E 테스트 | 전체 시스템 | 느림 | 가장 높음 |
Spring Boot로 치면 @SpringBootTest, @WebMvcTest, JUnit5와 같은 역할을 FastAPI에서는 pytest + TestClient가 담당한다.
설치
pip install pytest pytest-asyncio httpx
pytest— 파이썬 표준 테스트 프레임워크pytest-asyncio— async 함수 테스트 지원httpx— FastAPI TestClient가 내부적으로 사용하는 HTTP 클라이언트
테스트할 앱 구조
5편에서 만든 유저 API를 기반으로 테스트를 작성한다.
app/
├── main.py
├── database.py
└── users/
├── router.py
├── service.py
├── repository.py
├── models.py
├── schemas.py
└── dependencies.py
tests/
├── conftest.py
├── users/
│ ├── test_service.py ← 단위 테스트
│ ├── test_router.py ← 통합 테스트
│ └── test_flow.py ← E2E 테스트
└── e2e/
└── conftest.py
1. 단위 테스트 (Unit Test)
단위 테스트는 외부 의존성 없이 함수 하나만 테스트한다. DB, 네트워크, 외부 서비스를 실제로 사용하지 않고 Mock으로 대체한다.
서비스 계층 테스트
# app/users/service.py
from app.users.repository import UserRepository
from app.users.schemas import UserCreate, UserResponse
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def create_user(self, data: UserCreate) -> UserResponse:
if self.repo.find_by_email(data.email):
raise ValueError("이미 존재하는 이메일입니다")
return self.repo.save(data)
def get_user(self, user_id: int) -> UserResponse:
user = self.repo.find_by_id(user_id)
if not user:
raise ValueError(f"유저 {user_id}를 찾을 수 없습니다")
return user
# tests/users/test_service.py
import pytest
from unittest.mock import MagicMock
from app.users.service import UserService
from app.users.schemas import UserCreate, UserResponse
@pytest.fixture
def mock_repo():
return MagicMock()
@pytest.fixture
def service(mock_repo):
return UserService(repo=mock_repo)
def test_create_user_성공(service, mock_repo):
# Given: 이메일 중복 없음
mock_repo.find_by_email.return_value = None
mock_repo.save.return_value = UserResponse(id=1, name="홍길동", email="hong@test.com")
data = UserCreate(name="홍길동", email="hong@test.com")
# When
result = service.create_user(data)
# Then
assert result.id == 1
assert result.email == "hong@test.com"
mock_repo.save.assert_called_once_with(data)
def test_create_user_이메일_중복(service, mock_repo):
# Given: 이미 존재하는 이메일
mock_repo.find_by_email.return_value = UserResponse(id=1, name="기존유저", email="hong@test.com")
data = UserCreate(name="홍길동", email="hong@test.com")
# When / Then
with pytest.raises(ValueError, match="이미 존재하는 이메일"):
service.create_user(data)
def test_get_user_존재하지_않음(service, mock_repo):
# Given
mock_repo.find_by_id.return_value = None
# When / Then
with pytest.raises(ValueError, match="찾을 수 없습니다"):
service.get_user(999)
핵심 개념: Given / When / Then
def test_무언가():
# Given: 사전 조건 준비
mock_repo.find_by_email.return_value = None
# When: 실제로 테스트할 동작
result = service.create_user(data)
# Then: 결과 검증
assert result.id == 1
Spring의 JUnit과 동일한 패턴이다. 테스트의 의도를 명확히 드러낸다.
Mock이란?
mock_repo = MagicMock()
# 특정 메서드가 특정 값을 리턴하도록 설정
mock_repo.find_by_email.return_value = None
# 해당 메서드가 실제로 호출됐는지 검증
mock_repo.save.assert_called_once_with(data)
실제 DB를 붙이지 않고 "DB가 이런 값을 반환했다"고 가정하고 서비스 로직만 검증한다.
2. 통합 테스트 (Integration Test)
통합 테스트는 라우터 + 서비스 + DB를 묶어서 검증한다. FastAPI의 TestClient를 사용하면 실제 HTTP 요청을 보내는 것처럼 테스트할 수 있다.
TestClient 기본 사용법
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_루트():
response = client.get("/")
assert response.status_code == 200
TestClient는 실제 서버를 띄우지 않는다. 내부적으로 HTTP 요청을 흉내내어 FastAPI 앱에 직접 전달한다.
테스트용 DB 분리
통합 테스트에서는 실제 운영 DB를 건드리면 안 된다. 테스트 전용 DB를 따로 쓴다.
# tests/conftest.py ← pytest가 자동으로 읽는 설정 파일
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
# 테스트용 인메모리 SQLite DB
TEST_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(bind=engine)
@pytest.fixture(autouse=True)
def setup_db():
"""각 테스트 전에 테이블 생성, 후에 삭제"""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def db():
session = TestingSessionLocal()
try:
yield session
finally:
session.close()
@pytest.fixture
def client(db):
"""운영 DB 대신 테스트 DB를 주입한 TestClient"""
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
conftest.py는 pytest의 전역 fixture 파일이다. Spring의 @TestConfiguration과 비슷한 역할이다.
라우터 통합 테스트
# tests/users/test_router.py
import pytest
def test_유저_생성(client):
response = client.post("/users", json={
"name": "홍길동",
"email": "hong@test.com"
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "홍길동"
assert data["email"] == "hong@test.com"
assert "id" in data
def test_유저_생성_이메일_중복(client):
client.post("/users", json={"name": "기존유저", "email": "hong@test.com"})
response = client.post("/users", json={"name": "홍길동", "email": "hong@test.com"})
assert response.status_code == 409
def test_유저_조회(client):
created = client.post("/users", json={"name": "홍길동", "email": "hong@test.com"}).json()
user_id = created["id"]
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
assert response.json()["name"] == "홍길동"
def test_유저_조회_없는_ID(client):
response = client.get("/users/999")
assert response.status_code == 404
def test_유저_목록(client):
client.post("/users", json={"name": "유저1", "email": "user1@test.com"})
client.post("/users", json={"name": "유저2", "email": "user2@test.com"})
response = client.get("/users")
assert response.status_code == 200
assert len(response.json()) == 2
def test_유저_삭제(client):
created = client.post("/users", json={"name": "홍길동", "email": "hong@test.com"}).json()
user_id = created["id"]
response = client.delete(f"/users/{user_id}")
assert response.status_code == 204
assert client.get(f"/users/{user_id}").status_code == 404
통합 테스트는 실제 DB에 데이터를 넣고 빼면서 전체 흐름을 검증한다. Mock을 쓰지 않으니 실제 동작에 가깝다.
3. E2E 테스트 (End-to-End Test)
E2E 테스트는 실제 서버를 띄우고 실제 HTTP 요청을 보내서 사용자 시나리오 전체를 검증한다.
pytest-asyncio로 비동기 E2E 테스트
pip install pytest-asyncio httpx
# tests/users/test_flow.py
import pytest
import httpx
BASE_URL = "http://localhost:8000"
@pytest.mark.asyncio
async def test_유저_전체_CRUD_시나리오():
async with httpx.AsyncClient(base_url=BASE_URL) as client:
# 1. 유저 생성
create_resp = await client.post("/users", json={
"name": "홍길동",
"email": "hong@test.com"
})
assert create_resp.status_code == 201
user_id = create_resp.json()["id"]
# 2. 생성된 유저 조회
get_resp = await client.get(f"/users/{user_id}")
assert get_resp.status_code == 200
assert get_resp.json()["name"] == "홍길동"
# 3. 유저 목록에 포함됐는지 확인
list_resp = await client.get("/users")
assert any(u["id"] == user_id for u in list_resp.json())
# 4. 유저 삭제
delete_resp = await client.delete(f"/users/{user_id}")
assert delete_resp.status_code == 204
# 5. 삭제 후 조회 시 404
not_found_resp = await client.get(f"/users/{user_id}")
assert not_found_resp.status_code == 404
E2E 테스트는 실제 서버가 실행 중이어야 한다. CI/CD 파이프라인에서는 테스트 서버를 자동으로 띄운 후 실행한다.
conftest.py로 테스트 서버 자동 시작
# tests/e2e/conftest.py
import pytest
import subprocess
import time
@pytest.fixture(scope="session", autouse=True)
def start_server():
"""E2E 테스트 전에 서버를 자동으로 시작"""
proc = subprocess.Popen(["uvicorn", "app.main:app", "--port", "8001"])
time.sleep(2) # 서버 기동 대기
yield
proc.terminate()
3-1. Depends 의존성 테스트
실제 FastAPI 앱에서는 Depends()가 곳곳에 쓰인다. 테스트에서는 이 의존성을 override해서 실제 DB, 실제 인증 서버 없이도 테스트할 수 있다.
dependency_overrides 원리
# 운영 코드
@router.get("/users/me")
def get_me(current_user: User = Depends(get_current_user)):
return current_user
# 테스트에서 get_current_user를 가짜로 교체
app.dependency_overrides[get_current_user] = lambda: fake_user
app.dependency_overrides는 딕셔너리다. 키는 원래 의존성 함수, 값은 대체할 함수. 테스트가 끝나면 반드시 clear()해야 한다.
인증 의존성 override
JWT 토큰을 검증하는 get_current_user를 테스트에서 교체하는 예시다.
# app/auth/dependencies.py
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.users.models import User
from app.auth.jwt import decode_token
security = HTTPBearer()
def get_current_user_id(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> int:
return decode_token(credentials.credentials)
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.auth.dependencies import get_current_user_id
@pytest.fixture
def fake_user_id():
return 1
@pytest.fixture
def authenticated_client(fake_user_id):
"""인증된 유저로 요청하는 TestClient"""
app.dependency_overrides[get_current_user_id] = lambda: fake_user_id
yield TestClient(app)
app.dependency_overrides.clear()
# tests/users/test_router.py
def test_내_정보_조회_인증됨(authenticated_client, fake_user_id):
response = authenticated_client.get("/users/me")
assert response.status_code == 200
assert response.json()["user_id"] == fake_user_id
def test_내_정보_조회_미인증():
# override 없이 그냥 TestClient 사용 → 실제 인증 로직 실행 → 401
client = TestClient(app)
response = client.get("/users/me")
assert response.status_code == 401
실제 JWT 토큰 없이도 "인증된 상태"를 만들어 테스트할 수 있다.
서비스 의존성 override
DB 없이 서비스 레이어를 Mock으로 대체해서 라우터만 테스트한다.
# app/users/router.py
from app.users.service import UserService, get_user_service
@router.get("/users/{user_id}")
def get_user(
user_id: int,
service: UserService = Depends(get_user_service)
):
return service.get_user(user_id)
# tests/users/test_router.py
from unittest.mock import MagicMock
from app.users.service import get_user_service
from app.users.schemas import UserResponse
def test_유저_조회_서비스_Mock():
mock_service = MagicMock()
mock_service.get_user.return_value = UserResponse(
id=1, name="홍길동", email="hong@test.com"
)
app.dependency_overrides[get_user_service] = lambda: mock_service
response = TestClient(app).get("/users/1")
assert response.status_code == 200
assert response.json()["name"] == "홍길동"
mock_service.get_user.assert_called_once_with(1)
app.dependency_overrides.clear()
자동 정리
여러 테스트에서 override를 쓰다 보면 clear() 빠뜨리기 쉽다. autouse fixture로 자동화한다.
# tests/conftest.py
@pytest.fixture(autouse=True)
def clear_dependency_overrides():
"""모든 테스트 후 의존성 override 초기화"""
yield
app.dependency_overrides.clear()
이렇게 하면 각 테스트에서 clear()를 직접 호출하지 않아도 된다.
4. TDD (Test Driven Development)
TDD는 테스트를 먼저 작성하고, 그 테스트를 통과시키는 코드를 나중에 작성하는 개발 방식이다.
Red → 실패하는 테스트 작성
Green → 테스트가 통과하는 최소한의 코드 작성
Refactor → 코드 정리
TDD 실전 예시: 비밀번호 검증 기능
Step 1 - Red: 실패하는 테스트 먼저 작성
# tests/users/test_password.py
import pytest
from app.users.utils import validate_password # 아직 없는 파일
def test_비밀번호_8자_이상():
assert validate_password("short") is False
assert validate_password("longpassword") is True
def test_비밀번호_대문자_포함():
assert validate_password("alllowercase1!") is False
assert validate_password("HasUpper1!") is True
def test_비밀번호_숫자_포함():
assert validate_password("NoNumbers!") is False
assert validate_password("HasNumber1!") is True
def test_비밀번호_특수문자_포함():
assert validate_password("NoSpecial1") is False
assert validate_password("HasSpecial1!") is True
지금 이 테스트를 실행하면 ImportError로 실패한다. 아직 validate_password가 없으니 당연하다.
pytest tests/users/test_password.py
# ERROR: cannot import name 'validate_password'
Step 2 - Green: 테스트를 통과시키는 코드 작성
# app/users/utils.py
import re
def validate_password(password: str) -> bool:
if len(password) < 8:
return False
if not re.search(r'[A-Z]', password):
return False
if not re.search(r'[0-9]', password):
return False
if not re.search(r'[!@#$%^&*]', password):
return False
return True
pytest tests/users/test_password.py
# 4 passed ✓
Step 3 - Refactor: 코드 정리
# app/users/utils.py
import re
RULES = [
(r'.{8,}', "8자 이상"),
(r'[A-Z]', "대문자 포함"),
(r'[0-9]', "숫자 포함"),
(r'[!@#$%^&*]', "특수문자 포함"),
]
def validate_password(password: str) -> bool:
return all(re.search(pattern, password) for pattern, _ in RULES)
pytest tests/users/test_password.py
# 4 passed ✓ — 리팩터링 후에도 테스트 통과
TDD로 API 엔드포인트 개발하기
요구사항: 유저 검색 API를 추가한다 (GET /users?name=홍)
# Step 1: 테스트 먼저 작성
# tests/users/test_router.py
def test_이름으로_유저_검색(client):
client.post("/users", json={"name": "홍길동", "email": "hong@test.com"})
client.post("/users", json={"name": "홍판서", "email": "hong2@test.com"})
client.post("/users", json={"name": "이몽룡", "email": "lee@test.com"})
response = client.get("/users?name=홍")
assert response.status_code == 200
results = response.json()
assert len(results) == 2
assert all("홍" in u["name"] for u in results)
def test_검색_결과_없음(client):
response = client.get("/users?name=없는이름")
assert response.status_code == 200
assert response.json() == []
pytest tests/users/test_router.py::test_이름으로_유저_검색
# FAILED — /users?name=홍 이 아직 구현 안 됨
# Step 2: 구현
# app/users/router.py
from typing import Optional
@router.get("/users")
def get_users(name: Optional[str] = None, service: UserService = Depends(get_user_service)):
return service.get_users(name_filter=name)
# app/users/service.py
def get_users(self, name_filter: Optional[str] = None) -> list[UserResponse]:
return self.repo.find_all(name_filter=name_filter)
pytest tests/users/test_router.py
# 2 passed ✓
테스트 실행
# 전체 테스트 실행
pytest
# 특정 디렉토리만
pytest tests/users/
# 특정 파일만
pytest tests/users/test_service.py
# 특정 테스트 함수만
pytest tests/users/test_service.py::test_create_user_성공
# 상세 출력
pytest -v
# 실패 시 즉시 중단
pytest -x
# 커버리지 측정
pip install pytest-cov
pytest --cov=app --cov-report=term-missing
테스트 커버리지
pytest --cov=app --cov-report=term-missing
# 출력 예시
Name Stmts Miss Cover
-------------------------------------------------
app/users/router.py 25 2 92%
app/users/service.py 18 1 94%
app/users/repository.py 20 0 100%
app/main.py 8 0 100%
-------------------------------------------------
TOTAL 71 3 96%
커버리지 100%가 목표가 아니다. 핵심 비즈니스 로직과 경계 조건에 집중하는 것이 더 중요하다.
전략 정리
| 상황 | 추천 방식 |
|---|---|
| 비즈니스 로직 검증 | 단위 테스트 + Mock |
| API 동작 검증 | 통합 테스트 + 테스트 DB |
| 사용자 시나리오 검증 | E2E 테스트 |
| 새 기능 개발 | TDD (테스트 먼저) |
| 버그 수정 | 버그를 재현하는 테스트 먼저 작성 후 수정 |
테스트를 처음 도입할 때는 통합 테스트부터 시작하는 것이 효과적이다. 코드 변경 없이 기존 동작을 검증하는 안전망을 먼저 구축한 후, 단위 테스트와 TDD를 점진적으로 적용한다.
심화 - conftest.py 동작 원리
pytest는 어떻게 동작하나
pytest를 실행하면 두 가지를 자동으로 찾는다.
1. conftest.py → fixture 정의 파일
2. test_*.py → 테스트 함수 파일
conftest.py 는 pytest가 약속된 파일명으로 자동으로 읽는다. 별도로 import하지 않아도 된다.
@pytest.fixture 원리
테스트 함수가 필요한 사전 준비물을 만들어주는 함수다.
@pytest.fixture
def db():
session = TestingSessionLocal()
yield session # 테스트에 session 전달
session.close() # 테스트 끝난 후 정리
파라미터 이름으로 요청하면 pytest가 자동으로 실행해서 주입한다.
def test_something(db): # "db" fixture 찾아서 자동 주입
result = db.query(...)
fixture가 다른 fixture를 의존하는 것도 가능하다.
@pytest.fixture
def client(db): # db fixture를 주입받음
...
pytest가 의존 관계를 보고 자동으로 순서를 결정한다.
db fixture 먼저 실행
↓
client fixture 실행 (db 주입됨)
↓
테스트 함수 실행 (client 주입됨)
yield 문법 원리
yield의 핵심은 일시정지 개념이다.
def get_db():
db = SessionLocal()
yield db # 여기서 멈추고 db 전달
# FastAPI가 라우터 실행
db.close() # 라우터 끝나면 여기서 재개
return이었으면 db.close() 호출할 타이밍이 없다. yield 덕분에 전달 + 나중에 정리가 한 함수 안에서 가능하다.
pytest fixture도 같은 원리다.
@pytest.fixture
def db():
session = TestingSessionLocal()
yield session # 테스트에 session 전달 (일시정지)
session.close() # 테스트 끝나면 재개
next()는 누가 호출하나
yield를 쓰면 generator 함수가 된다. generator를 실제로 실행시키려면 next()를 호출해야 한다.
pytest fixture의 경우
# pytest 내부 (우리가 볼 일 없는 코드)
gen = db() # generator 생성
value = next(gen) # yield 전까지 실행, session 받음
# 테스트 실행
next(gen) # yield 이후 실행, session.close()
FastAPI Depends의 경우
# FastAPI 내부
gen = get_db()
value = next(gen) # yield 전까지 실행, db 받음
# 라우터 실행
next(gen) # yield 이후 실행, db.close()
우리는 규칙대로 yield 쓰고 Depends 등록만 하면 된다. next() 호출은 프레임워크가 알아서 한다.
autouse=True 원리
pytest는 테스트 실행 전에 fixture 목록을 스캔한다.
@pytest.fixture(autouse=True)
def setup_db():
Base.metadata.create_all(bind=engine) # 테스트 전
yield
Base.metadata.drop_all(bind=engine) # 테스트 후
autouse=True 가 있으면 테스트 함수가 요청하지 않아도 pytest가 강제로 실행시킨다.
test_todo_create 실행 전 → create_all()
test_todo_create 실행
test_todo_create 실행 후 → drop_all()
test_get_todo 실행 전 → create_all()
test_get_todo 실행
test_get_todo 실행 후 → drop_all()
... 반복
매 테스트마다 깨끗한 DB에서 시작하기 때문에 테스트 간 데이터 오염이 없다.
fixture 실행 순서와 정리 순서
conftest.py 기준 전체 실행 순서다.
[전처리]
create_all() # setup_db yield 앞
session = Session() # db yield 앞
TestClient 생성 # client yield 앞
[테스트 함수 실행]
[후처리 - 역순]
dependency_overrides.clear() # client yield 뒤
session.close() # db yield 뒤
drop_all() # setup_db yield 뒤
후처리는 전처리의 역순으로 실행된다.
override_get_db에 yield가 필요한 이유
@pytest.fixture
def client(db):
def override_get_db():
yield db # FastAPI용 generator
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app) # pytest fixture용 yield
app.dependency_overrides.clear()
override_get_db 안의 yield와 client fixture의 yield는 역할이 다르다.
override_get_db의 yield → FastAPI가 next() 호출 (DB 주입용)
client의 yield → pytest가 next() 호출 (fixture용)
FastAPI Depends는 generator 함수를 기대한다. return db로 쓰면 FastAPI가 generator로 인식하지 못한다.
미들웨어 패턴
yield를 이용한 전처리/후처리 구조는 파이썬만의 개념이 아니다. 언어마다 표현 방식이 다를 뿐 동일한 패턴이다.
# Python - yield
def get_db():
db = Session()
yield db # 실제 로직 실행
db.close()
// Node.js Express - next() 함수
app.use((req, res, next) => {
// 전처리
next() // 실제 로직 실행
// 후처리
})
// Spring AOP - proceed()
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 전처리
Object result = pjp.proceed(); // 실제 로직 실행
// 후처리
return result;
}
이 패턴을 미들웨어 패턴이라고 부른다. 프레임워크가 있는 곳엔 거의 다 존재하는 개념이다.
Depends 원리
FastAPI의 Depends는 라우터 함수에 필요한 값을 자동으로 주입해주는 도구다.
# 우리가 선언만 하면
@router.get("/todos")
def get_todos(db: Session = Depends(get_db)):
...
FastAPI가 요청을 받으면 내부적으로 이런 일이 일어난다.
요청 들어옴
↓
Depends(get_db) 발견
↓
get_db() 호출 → generator 생성
↓
next() 호출 → db 세션 받음
↓
라우터 함수에 db 주입해서 실행
↓
next() 다시 호출 → db.close() 실행
우리는 Depends(get_db) 라고 선언만 하면 된다. 실제 next() 호출과 세션 정리는 FastAPI가 담당한다.
여러 라우터가 같은 Depends를 쓰면 각 요청마다 독립적으로 실행된다.
@router.get("/todos")
def get_todos(db = Depends(get_db)): # 요청마다 새 세션
...
@router.post("/todos")
def create_todo(db = Depends(get_db)): # 요청마다 새 세션
...
dependency_overrides 원리
dependency_overrides는 FastAPI 앱이 가진 딕셔너리다.
app.dependency_overrides # {} 기본값은 빈 딕셔너리
여기에 원래 함수를 키로, 대체할 함수를 값으로 넣으면 FastAPI가 Depends 처리할 때 원래 함수 대신 대체 함수를 실행한다.
# 원래
@router.get("/todos")
def get_todos(db = Depends(get_db)): # get_db 실행 → 운영 DB 세션
...
# override 후
app.dependency_overrides[get_db] = override_get_db
# get_db 대신 override_get_db 실행 → 테스트 DB 세션
Depends(get_db) 발견
↓
dependency_overrides에 get_db가 있나?
↓ 있음
override_get_db() 실행 → 테스트 DB 세션 반환
테스트가 끝나면 반드시 clear()로 원복해야 한다.
@pytest.fixture
def client(db):
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db # 교체
yield TestClient(app)
app.dependency_overrides.clear() # 원복
clear() 없으면 다음 테스트에도 override가 남아있어서 다른 테스트에 영향을 준다.