Spaces:
Running
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 | |
| ```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. | |