Spaces:
Running
Testing FastAPI Applications
FastAPI applications are tested using the TestClient class, which provides a synchronous interface for sending requests to your application without running an actual server. For async testing, use httpx.AsyncClient.
Basic Testing with TestClient
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
result = {"item_id": item_id}
if q:
result["q"] = q
return result
client = TestClient(app)
def test_read_item():
response = client.get("/items/42?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 42, "q": "test"}
def test_read_item_not_found():
response = client.get("/items/abc")
assert response.status_code == 422 # Validation error
The TestClient is built on top of httpx (which replaced requests as of Starlette 0.20.0). It supports all HTTP methods: client.get(), client.post(), client.put(), client.delete(), client.patch(), client.options(), and client.head().
Pytest Fixtures
Use fixtures to share the TestClient and set up test data:
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from myapp.main import app
from myapp.database import Base, engine
@pytest.fixture(scope="module")
def client():
Base.metadata.create_all(bind=engine)
with TestClient(app) as c:
yield c
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def auth_headers():
return {"Authorization": "Bearer test-token-12345"}
def test_create_item(client, auth_headers):
response = client.post(
"/items/",
json={"name": "Widget", "price": 35.99},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert "id" in data
Using scope="module" means the fixture is created once per test module rather than once per test function, improving performance when database setup is expensive. The with statement ensures proper cleanup of the test client's underlying transport.
Overriding Dependencies in Tests
Override dependencies to inject mock services or test databases:
from fastapi import FastAPI, Depends
app = FastAPI()
async def get_db():
db = ProductionDatabase()
try:
yield db
finally:
db.close()
@app.get("/items/")
async def read_items(db=Depends(get_db)):
return db.query_all_items()
# In your test file:
def get_test_db():
db = TestDatabase()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = get_test_db
client = TestClient(app)
def test_read_items():
response = client.get("/items/")
assert response.status_code == 200
# Clean up overrides after tests
app.dependency_overrides.clear()
The app.dependency_overrides dictionary maps original dependencies to their replacements. This works for any dependency in the chain, including sub-dependencies. Always call app.dependency_overrides.clear() after tests to prevent overrides from leaking between test modules.
Async Testing with httpx
For testing async-specific behavior (e.g., async database calls, WebSocket-related setup), use httpx.AsyncClient with pytest-asyncio:
import pytest
from httpx import AsyncClient, ASGITransport
from myapp.main import app
@pytest.mark.anyio
async def test_read_items_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/items/")
assert response.status_code == 200
@pytest.mark.anyio
async def test_create_item_async():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post(
"/items/",
json={"name": "Widget", "price": 35.99},
)
assert response.status_code == 201
The ASGITransport connects httpx directly to the ASGI application without network overhead. The base_url parameter is required but can be any valid URL since no real network requests are made. Install the async test dependencies with pip install httpx pytest-asyncio (or use anyio with the @pytest.mark.anyio marker).
Testing WebSockets
def test_websocket():
client = TestClient(app)
with client.websocket_connect("/ws") as websocket:
websocket.send_text("hello")
data = websocket.receive_text()
assert data == "Message received: hello"
The websocket_connect context manager establishes a WebSocket connection. It supports send_text(), send_json(), send_bytes(), receive_text(), receive_json(), and receive_bytes() methods.