agentbench / data /tech_docs /fastapi_testing.md
Nomearod's picture
feat: Day 4 — corpus, ingest script, first 10 golden questions
a152b95
# 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
```python
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:
```python
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:
```python
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`:
```python
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
```python
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.