반응형
FastAPI 공부하면서 내가 제일 “오… 이거 좋다” 했던 포인트가 테스트였다.
왜냐면 현실의 서버는 이런 애들이 붙어있다.
- DB
- 외부 HTTP API
- AI API
근데 테스트에서 그걸 진짜로 호출하면?
- 느림
- flaky(가끔 실패)
- 돈 나감(특히 AI)
그래서 테스트에서는 dependency_overrides로 의존성을 바꿔치기 한다.
1) create_app(settings)로 테스트용 앱 만들기
테스트용 DB URL을 주입해서 app를 만든다.
settings = Settings(database_url=f"sqlite:///{tmp_path / 'test.db'}")
app = create_app(settings)
client = TestClient(app)
이렇게 하면 테스트끼리 DB 파일이 분리돼서 깔끔하다.
2) dependency_overrides로 AI generator를 mock으로 바꾼다
AI 생성 테스트의 핵심은 이거.
mock_gen = AsyncMock(spec=GeminiDiaryGenerator)
mock_gen.generate = AsyncMock(return_value=_FAKE_AI_RESULT)
app.dependency_overrides[get_diary_generator] = lambda: mock_gen
이러면 실제 Gemini는 한 번도 호출되지 않는다.
근데 라우터는 “AI가 반환한 것처럼” 동작한다.
즉, 내 코드만 테스트할 수 있게 된다.
3) “실패 케이스”를 테스트로 박제하기
이게 진짜 중요하다.
- 키 없으면 503
- AI가 예외를 던지면 500
- 잘못된 입력이면 422
예를 들어 “AI 키 없음”은 이렇게 테스트로 박제해둘 수 있다.
app.dependency_overrides[get_diary_generator] = lambda: None
response = client.post("/api/v1/diaries", data={"date": "2026-03-13"})
assert response.status_code == 503
그리고 “AI 내부 오류”는 이렇게.
mock_gen.generate = AsyncMock(side_effect=RuntimeError("Gemini API 오류"))
app.dependency_overrides[get_diary_generator] = lambda: mock_gen
response = client.post("/api/v1/diaries", data={"date": "2026-03-13"})
assert response.status_code == 500
이렇게 해두면, 나중에 리팩토링하다가 실수해도 테스트가 바로 잡아준다.
4) Weather도 똑같이 mock 주입한다
날씨 라우터도 패턴이 동일하다.
get_weather_service를 override해서 mock 반환- 또는
None반환시켜서 503 확인 - 또는
ConnectError던지게 해서 502 확인
즉, DI를 잘 설계해두면 테스트가 엄청 쉬워진다.
실습: 진짜로 pytest 한 번 돌려보기
서버 디렉토리에서:
cd apps/server
uv run pytest
테스트가 통과하면, “부트스트랩/설정/라우팅/DB/DI/검증”이 서로 잘 맞물려 있다는 증거다.
마무리
FastAPI는 “프레임워크 자체 기능”도 좋지만,
내 기준엔 테스트하기 좋은 구조로 강제되는 느낌이 제일 좋았다.
다음 편(옵션)에서는 도커/컴포즈로 로컬과 서버 실행 환경을 맞추는 얘기를 해보자.
참고
반응형