Spaces:
Running
Running
File size: 4,911 Bytes
a152b95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | # 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.
|