FastAPI/
Lesson

Your FastAPI app has a clean dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. chain: endpoints depend on get_current_user, which depends on get_db and oauth2_scheme. In production, get_db connects to PostgreSQL and get_current_user decodes real JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup. tokens. In tests, you want none of that. You want a test database (or no database), a fake user, and fast execution. FastAPI's dependency_overrides system makes this possible without changing a single line of production code.

TestClient basics

FastAPI's TestClient (built on Starlette's, which is built on httpx) lets you send HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. requests to your app without starting a server:

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

TestClient creates your app in-process. Requests do not go over the network, they are handled directly by your ASGI app. This makes tests fast and deterministic: no port conflicts, no network timeouts, no race conditions.

02

Overriding dependencies

The core mechanism is app.dependency_overrides, a dictionary that maps original dependencies to replacement functions:

from app.main import app
from app.dependencies import get_db, get_current_user

# Create a mock database dependency
def override_get_db():
    test_db = TestSessionLocal()
    try:
        yield test_db
    finally:
        test_db.close()

# Create a mock auth dependency
def override_get_current_user():
    return {"id": 1, "email": "test@example.com", "role": "user"}

# Tell FastAPI to use the mocks
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user

client = TestClient(app)

def test_protected_endpoint():
    response = client.get("/me")
    assert response.status_code == 200
    assert response.json()["email"] == "test@example.com"

When the test calls client.get("/me"), FastAPI sees that get_current_user has an override. Instead of extracting a tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. and querying the database, it calls override_get_current_user and returns the fake user. The endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. runs with that fake user as if it were real.

This works because dependency_overrides uses the function object as the key. get_db is a specific function in memory, and FastAPI replaces it wherever it appears in the dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. graph, even in nested dependencies.

03

Setting up a test database

For integration tests that actually exercise database queries, you need a test database:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base

# Use SQLite in-memory for fast tests
TEST_DATABASE_URL = "sqlite:///./test.db"
test_engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestSessionLocal = sessionmaker(bind=test_engine)

# Create all tables before tests run
Base.metadata.create_all(bind=test_engine)

def override_get_db():
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

This gives you a real database with real SQLWhat is sql?A language for querying and managing data in relational databases, letting you insert, read, update, and delete rows across tables. queries, but isolated from your production data. Each test run starts fresh (you can drop and recreate tables between tests for full isolation).

Good to know
SQLite in-memory databases (sqlite://) are the fastest option, but they do not support all PostgreSQL features (JSON operators, array types, RETURNING clause behavior). If your queries use PostgreSQL-specific features, use a real PostgreSQL test database with a different database name.
04

Using pytest fixtures

Hardcoding overrides at moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. level works but is inflexible. Pytest fixtures give you setup/teardown lifecycle:

import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies import get_db, get_current_user

@pytest.fixture
def test_db():
    Base.metadata.create_all(bind=test_engine)
    db = TestSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=test_engine)

@pytest.fixture
def client(test_db):
    def override_get_db():
        yield test_db

    app.dependency_overrides[get_db] = override_get_db
    app.dependency_overrides[get_current_user] = lambda: {
        "id": 1, "email": "test@example.com", "role": "user"
    }

    yield TestClient(app)

    # Cleanup: remove all overrides after the test
    app.dependency_overrides = {}

def test_create_user(client):
    response = client.post("/users", json={"name": "Alice", "email": "alice@test.com"})
    assert response.status_code == 201

def test_list_users(client):
    response = client.get("/users")
    assert response.status_code == 200

The client fixture sets up overrides before each test and clears them after. This prevents override leakage, a test that overrides get_current_user with an admin user does not affect the next test that expects a regular user.

05

The override cleanup trap

If you forget to reset app.dependency_overrides, your tests become order-dependent. Consider this scenario:

# test_admin.py
def test_admin_endpoint():
    app.dependency_overrides[get_current_user] = lambda: {"role": "admin"}
    response = client.get("/admin/users")
    assert response.status_code == 200
    # Forgot to clean up!

# test_regular.py (runs after test_admin.py)
def test_regular_user_cannot_access_admin():
    # This test PASSES even though it should not -
    # the admin override from the previous test is still active!
    response = client.get("/admin/users")
    assert response.status_code == 403  # FAILS: returns 200 because user is still admin

The second test fails because it inherits the admin override from the first test. Cleaning up overrides is not optional, it is a correctness requirement.

AI pitfall
AI-generated test files almost never clean up dependency overrides. The tests work in isolation (pytest test_admin.py) but fail when run together (pytest). If you see AI-generated tests with app.dependency_overrides and no cleanup, add app.dependency_overrides = {} in a fixture's teardown or in finally blocks.
06

Testing different user roles

A common testing need is running the same endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. with different user roles:

@pytest.fixture
def admin_client():
    app.dependency_overrides[get_current_user] = lambda: {
        "id": 1, "role": "admin", "email": "admin@test.com"
    }
    yield TestClient(app)
    app.dependency_overrides = {}

@pytest.fixture
def user_client():
    app.dependency_overrides[get_current_user] = lambda: {
        "id": 2, "role": "user", "email": "user@test.com"
    }
    yield TestClient(app)
    app.dependency_overrides = {}

def test_admin_can_delete(admin_client):
    response = admin_client.delete("/users/2")
    assert response.status_code == 200

def test_user_cannot_delete(user_client):
    response = user_client.delete("/users/2")
    assert response.status_code == 403

Each fixture creates a client with a specific user role. The tests are readable, the fixture name tells you what role is being tested.

07

Are you testing mocks or logic?

This is the most important question to ask about any test, and it is where AI-generated tests fail most often. Consider:

# AI-generated test
def test_get_user():
    app.dependency_overrides[get_current_user] = lambda: {"id": 1, "name": "Alice"}
    response = client.get("/me")
    assert response.status_code == 200
    assert response.json()["name"] == "Alice"

What does this test actually prove? It proves that when you inject a user with name: "Alice", the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. returns name: "Alice". That is testing the mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions., not the logic. The test would pass even if the endpoint was just return current_user with zero business logic.

A meaningful test exercises real behavior:

# Meaningful test: actually creates data in the test DB and verifies behavior
def test_user_can_only_see_own_notes(client, test_db):
    # Create notes for two different users
    test_db.add(Note(id=1, user_id=1, content="User 1's note"))
    test_db.add(Note(id=2, user_id=2, content="User 2's note"))
    test_db.commit()

    # Current user is user 1 (from the client fixture)
    response = client.get("/notes")
    notes = response.json()

    # Should only see their own notes
    assert len(notes) == 1
    assert notes[0]["content"] == "User 1's note"

This test creates real data, runs a real query, and verifies real filtering logic. If the endpoint has an IDORWhat is idor?Insecure Direct Object Reference - a vulnerability where an API lets any logged-in user access any resource by simply changing the ID in the URL. bug (returns all notes instead of the user's notes), this test catches it.

AI pitfall
AI generates tests that look comprehensive, high coverage numbers, many test functions, all passing. But most of them are testing mocks: inject fake data, check that fake data comes back. Ask yourself: "If I broke the endpoint's logic, would this test still pass?" If yes, the test is worthless.
08

Quick reference: testing patterns

What to testOverride strategyWhat to verify
Endpoint returns correct dataOverride get_db with test DB, seed dataResponse matches expected data from DB
Auth blocks unauthenticated usersDo NOT override get_current_userResponse is 401
Role-based accessOverride with different rolesAdmin gets 200, regular user gets 403
Input validationOverride auth, send invalid payloadsResponse is 422 with validation errors
Database writesOverride get_db, check DB after requestNew row exists with correct values
Error handlingOverride with a DB that raises exceptionsResponse is 500 with error message