[FastAPI] Testing
정의
FastAPI Testing 은 endpoint 를 실제 서버 없이 직접 호출해 응답을 검증하는 workflow 입니다. Starlette 의 TestClient (내부적으로 httpx sync) 또는 httpx.AsyncClient 로 앱 인스턴스를 감쌉니다.
TestClient 기본
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/hello")
def hello():
return {"msg": "world"}
client = TestClient(app)
def test_hello():
resp = client.get("/hello")
assert resp.status_code == 200
assert resp.json() == {"msg": "world"}
TestClient 는 프로세스 내부에서 ASGI 앱을 직접 호출. HTTP 서버 미기동. 매우 빠름.
pytest 통합
# conftest.py
import pytest
from fastapi.testclient import TestClient
from myapp.main import app
@pytest.fixture
def client():
with TestClient(app) as c:
yield c
# test_users.py
def test_read_user(client):
resp = client.get("/users/1")
assert resp.status_code == 200
def test_create_user(client):
resp = client.post("/users/", json={"email": "a@b.com", "password": "secret123"})
assert resp.status_code == 201
data = resp.json()
assert data["email"] == "a@b.com"
Async 테스트 (httpx.AsyncClient)
TestClient 는 sync. Async endpoint 를 async 스타일로 테스트하려면 httpx.AsyncClient.
import pytest
import httpx
from httpx import ASGITransport
from myapp.main import app
@pytest.mark.asyncio
async def test_read_user_async():
async with httpx.AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as ac:
resp = await ac.get("/users/1")
assert resp.status_code == 200
pytest-asyncio 필요. asyncio_mode = "auto" 로 설정하면 @pytest.mark.asyncio 생략 가능.
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
dependency_overrides (핵심)
Endpoint 코드를 안 바꾸고 dependency 를 mock. 테스트 인프라의 심장.
from fastapi import Depends
from sqlalchemy.orm import Session
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
@app.get("/users/{id}")
def read_user(id: int, db: Session = Depends(get_db)):
return db.query(User).get(id)
테스트:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
TEST_DB_URL = "sqlite:///./test.db"
engine = create_engine(TEST_DB_URL, connect_args={"check_same_thread": False})
TestSessionLocal = sessionmaker(bind=engine)
def override_get_db():
db = TestSessionLocal()
try: yield db
finally: db.close()
@pytest.fixture(autouse=True)
def setup_db():
Base.metadata.create_all(engine)
app.dependency_overrides[get_db] = override_get_db
yield
Base.metadata.drop_all(engine)
app.dependency_overrides = {}
autouse=True 로 모든 테스트에 자동 적용. 테스트 후 override clear.
DB 테스트 패턴
패턴 1: SQLite in-memory
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
- Pros: 매우 빠름, 격리 완벽
- Cons: PostgreSQL 특화 기능 (JSONB, LATERAL, …) 없음
패턴 2: Docker Postgres + fresh DB per test
@pytest.fixture(scope="session")
def db_engine():
engine = create_engine("postgresql://test:test@localhost/testdb")
yield engine
@pytest.fixture(scope="function")
def db_session(db_engine):
connection = db_engine.connect()
transaction = connection.begin()
Session = sessionmaker(bind=connection)
session = Session()
yield session
session.close()
transaction.rollback() # 테스트 후 롤백
connection.close()
Pros: 프로덕션 DB 와 동일. Transaction rollback 으로 격리. Cons: Docker 필요, 느림.
패턴 3: testcontainers
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16") as p:
yield p.get_connection_url()
Auth 테스트
@pytest.fixture
def authed_client():
def override_current_user():
return User(id=1, email="test@example.com", is_admin=True)
app.dependency_overrides[get_current_user] = override_current_user
with TestClient(app) as c:
yield c
app.dependency_overrides = {}
또는 실제 토큰 발급 후 헤더 전달:
@pytest.fixture
def token():
return create_access_token({"sub": "user@example.com"})
def test_protected(client, token):
resp = client.get("/me", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
파일 업로드 테스트
def test_upload(client):
with open("tests/fixtures/sample.pdf", "rb") as f:
resp = client.post(
"/upload",
files={"file": ("sample.pdf", f, "application/pdf")},
)
assert resp.status_code == 200
WebSocket 테스트
def test_websocket():
with client.websocket_connect("/ws") as ws:
ws.send_text("hello")
data = ws.receive_text()
assert data == "echo: hello"
Startup / Shutdown 이벤트
TestClient context manager (with TestClient(app) as c) 는 lifespan 이벤트를 실행.
@asynccontextmanager
async def lifespan(app: FastAPI):
print("startup")
yield
print("shutdown")
app = FastAPI(lifespan=lifespan)
def test_lifespan():
with TestClient(app) as client:
client.get("/") # startup 이미 실행
# exit 시 shutdown
외부 API mocking (httpx)
httpx.AsyncClient 를 httpx_mock 또는 respx 로 mock.
import respx
from httpx import Response
@respx.mock
def test_external_call(client):
respx.get("https://api.example.com/data").mock(
return_value=Response(200, json={"key": "value"})
)
resp = client.get("/wrapped")
assert resp.json()["proxied"] == "value"
Test isolation
각 테스트는 격리되어야 함. 안 지키면 flaky.
- DB: transaction rollback 또는 truncate
- Cache (Redis):
FLUSHDB또는 db=1 - File system:
tmp_pathfixture - Global state: 반드시 avoidance (dependency injection 사용)
성능 테스트
로컬 부하 테스트:
pip install locust
locust -f locustfile.py --host http://localhost:8000
from locust import HttpUser, task
class WebsiteUser(HttpUser):
@task
def get_items(self):
self.client.get("/items/")
@task(3) # 3배 자주
def get_root(self):
self.client.get("/")
Coverage
pytest --cov=myapp --cov-report=html --cov-report=term-missing
.coveragerc 로 branch coverage 등 세부 설정.
함정
WARNING
TestClient 안에서 request body 를 pydantic 검증하는 실제 endpoint 를 호출. Sync/async 미스매치 조심.
CAUTION
dependency_overrides clear 안 하면 다음 테스트로 leak. autouse=True fixture 로 자동 cleanup.
WARNING
DB session 을 fixture 로 재사용 시 lazy load 실패. SQLAlchemy 는 session close 후 relationship 접근 불가. expire_on_commit=False 또는 explicit refresh.
IMPORTANT
TestClient(app) 은 lifespan 을 실행하지 않는 사용 방식이 있음. with TestClient(app) as c 형태로 반드시 사용.
CAUTION
AsyncClient(transport=ASGITransport(app=app)) 처럼 명시적 transport 지정. base_url 도 정확히.
관련 위키
- FastAPI - 상위 개요
- FastAPI DI - dependency_overrides
- FastAPI Async - async test
- FastAPI Routing - endpoint 정의
- pytest - 테스트 프레임워크
- unittest.mock - mocking
- asyncio - async 근간
이 글의 용어 (7개)
- [FastAPI] Async / Sync Endpointsfastapi
- 정의 FastAPI 는 ASGI 프레임워크 이므로 endpoint 를 로 정의해 이벤트 루프에서 실행하거나, 로 정의해 스레드풀에서 실행할 수 있습니다. 어느 쪽을 선택하는지가 …
- [FastAPI] Dependency Injectionfastapi
- 정의 FastAPI Dependency Injection 은 로 함수를 endpoint 에 자동 주입하는 시스템입니다. DB 세션, 인증 사용자, 설정, 서비스 계층 등을 end…
- [FastAPI] Routing (Path Operations)fastapi
- 정의 FastAPI Routing 은 HTTP method + URL 을 Python 함수 (path operation) 에 매핑하는 시스템입니다. Starlette 의 라우팅 …
- [Python] asyncio: 이벤트 루프, async/awaitpython
- 정의 는 Python 표준 라이브러리의 단일 스레드 이벤트 루프 기반 비동기 I/O 프레임워크다. 로 코루틴을 정의하고 로 다른 코루틴/awaitable 완료를 기다린다. I/O…
- [Python] FastAPIfastapi
- 정의 FastAPI 는 Python 3.8+ 을 위한 ASGI 기반 현대 웹/API 프레임워크 입니다. Sebastián Ramírez (tiangolo) 가 2018년 발표했고…
- [Python] pytest: fixture, parametrize, mockpython
- 정의 는 Python 표준 테스트 프레임워크의 사실상 표준(stdlib는 이지만 pytest가 더 강력). 단순한 함수 로 시작해서 fixture, parametrize, plu…
- [Python] unittest.mock: Mock, patch, MagicMockpython
- 정의 은 테스트용 대역체(test double) 생성과 패칭을 위한 표준 라이브러리. , , 를 통해 외부 의존성을 격리하고 호출을 검증한다. pytest와도 잘 어울리며 같은 …
💬 댓글