Skip to content
C Codeloom
FastAPI

FastAPI Testing with pytest

Write fast, reliable tests for FastAPI apps using TestClient, pytest fixtures, dependency overrides, and a separate test database.

·3 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How TestClient drives endpoints in-process
  • Designing fixtures for setup and teardown
  • Overriding dependencies for tests
  • Using a separate test database
  • Writing async tests with httpx

Prerequisites

  • Comfortable with FastAPI and basic pytest

What and Why

Testing a FastAPI app is fast and ergonomic. The provided TestClient runs your ASGI app in-process, so you skip the network and HTTP server entirely. Combine that with pytest fixtures and dependency overrides, and you can cover the full surface of your API in seconds.

Mental Model

TestClient wraps your app in httpx plus an ASGI transport. Each request goes through the real middleware, routing, and dependencies, but no socket is opened. Fixtures supply the test database session, fake users, and any other inputs.

pytest fixture
   |
   v
TestClient(app)
   |
   v
ASGI in-process
   |
   v
Middleware -> Routes -> Dependencies
   |
   v
Response object asserted in test
Test flow with FastAPI

Hands-on Example

A minimal app with a dependency.

# app.py
from fastapi import FastAPI, Depends
from pydantic import BaseModel

app = FastAPI()

def get_db():
    # in production this yields a real session
    yield {"users": {1: {"id": 1, "name": "Ada"}}}

class User(BaseModel):
    id: int
    name: str

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db = Depends(get_db)):
    return db["users"][user_id]

Tests with TestClient.

# tests/test_users.py
from fastapi.testclient import TestClient
from app import app, get_db
import pytest

@pytest.fixture
def client():
    def fake_db():
        yield {"users": {1: {"id": 1, "name": "TestAda"}}}
    app.dependency_overrides[get_db] = fake_db
    yield TestClient(app)
    app.dependency_overrides.clear()

def test_read_user_ok(client):
    r = client.get("/users/1")
    assert r.status_code == 200
    assert r.json() == {"id": 1, "name": "TestAda"}

def test_read_user_404(client):
    r = client.get("/users/99")
    assert r.status_code == 500  # in real code raise HTTPException(404)

Use a real test database with SQLAlchemy.

# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db import Base, get_session
from app.main import app
from fastapi.testclient import TestClient

@pytest.fixture(scope="session")
def engine():
    e = create_engine("sqlite+pysqlite:///:memory:", future=True)
    Base.metadata.create_all(e)
    return e

@pytest.fixture
def db(engine):
    TestingSession = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    s = TestingSession()
    try:
        yield s
    finally:
        s.rollback()
        s.close()

@pytest.fixture
def client(db):
    def override():
        yield db
    app.dependency_overrides[get_session] = override
    yield TestClient(app)
    app.dependency_overrides.clear()

For async endpoints prefer httpx.AsyncClient.

import pytest, httpx
from httpx import ASGITransport
from app import app

@pytest.mark.asyncio
async def test_root():
    async with httpx.AsyncClient(
        transport=ASGITransport(app=app), base_url="http://test"
    ) as ac:
        r = await ac.get("/")
        assert r.status_code == 200

Common Pitfalls

  • Forgetting to clear app.dependency_overrides. State leaks across tests.
  • Sharing a database session between fixture and request. Always override get_session so the endpoint sees the same session your test seeded.
  • Mocking too much. Tests that mock every dependency become tightly coupled to internals and miss real bugs.
  • Slow setup. Recreating the schema for every test takes seconds at scale. Use a session-scoped engine and per-test transactions.
  • Calling external services in tests. Mock the HTTP boundary with respx or use a recorded fixture.

Practical Tips

  • Group tests by route module. One file per resource keeps fixtures focused.
  • Use parametrize to cover happy paths and error cases in one place.
  • Snapshot test JSON responses for documentation endpoints.
  • Run pytest -x --ff while iterating to fail fast and rerun failures first.
  • Measure coverage with pytest-cov and treat dependency overrides as code you must cover.

Wrap-up

FastAPI is one of the easiest Python frameworks to test. TestClient runs in-process for speed, dependency overrides let you swap out the database and auth without monkeypatching, and pytest fixtures keep setup tidy. Build a small but solid harness once, and writing new tests becomes nearly free.