Testing FastAPI Applications

Introduction

pytest plus FastAPI’s TestClient simulate HTTP requests without binding a port. dependency_overrides swap database and auth for isolated tests. This chapter covers fixtures, CRUD tests, authenticated routes, and optional async testing with httpx.

Prerequisites

Install and Run

bash
pip install pytest httpx
pytest
pytest -v tests/test_items.py

tests/conftest.py structure:

Code explanation:

  • In-memory SQLite with StaticPool shares one connection for tests
  • dependency_overrides replaces get_db for all requests in test client

Compare Flask testing.

Test Health and JSON

tests/test_health.py:

python
def test_health(client):
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json()["status"] == "ok"

Test CRUD

Test Authentication

Login uses form data when using OAuth2PasswordRequestForm.

Override Current User

python
from app.api.deps import get_current_user
from app.models.user import User
 
def test_admin_route(client):
    fake_admin = User(id=1, username="admin", hashed_password="x", role="admin")
 
    app.dependency_overrides[get_current_user] = lambda: fake_admin
    response = client.get("/api/v1/admin/stats")
    app.dependency_overrides.pop(get_current_user, None)
    assert response.status_code == 200

Clear overrides after each test to avoid leakage.

Validation Error Tests

python
def test_create_item_invalid_price(client):
    response = client.post("/api/v1/items/", json={"name": "Bad", "price": -1})
    assert response.status_code == 422
    assert "detail" in response.json()

Settings Override

python
from app.core.config import get_settings, Settings
 
def get_settings_override():
    return Settings(secret_key="test", database_url="sqlite://")
 
app.dependency_overrides[get_settings] = get_settings_override

Async Tests (Optional)

bash
pip install pytest-asyncio
python
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
 
@pytest.mark.asyncio
async def test_root_async():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        response = await ac.get("/")
    assert response.status_code == 200

Use for async def routes and async dependencies.

Coverage

bash
pip install pytest-cov
pytest --cov=app --cov-report=term-missing

Link CI to Git and CI/CDDocker and CI chapter.

Tip

Test Behavior, Not OpenAPI Snapshot

Focus on status codes and business rules unless you intentionally snapshot API contracts.

FAQ

TestClient vs live server?

TestClient is faster and deterministic—no port conflicts.

Database not isolated?

Ensure drop_all in fixture teardown; avoid shared file SQLite path.

422 vs assertion?

Inspect response.json()["detail"] for field errors.

Lifespan events in tests?

TestClient triggers lifespan on enter/exit when used as context manager.

Parallel pytest workers?

Each worker needs isolated DB—use in-memory SQLite per worker.

Mock external HTTP?

Use respx or unittest.mock.patch on httpx clients.