Home/Learn/FastAPI/Test Fixtures & Overrides — Real Tests, Fake Infrastructure

Test Fixtures & Overrides — Real Tests, Fake Infrastructure

Intermediate
Testing

dependency_overrides swaps get_db for a per-test SQLite/test-Postgres session and get_current_user for canned users — pytest fixtures wire it so every test starts on a clean, isolated world.

Overview

Real endpoints touch databases and auth; tests must control both without touching production code. Because everything flows through Depends, app.dependency_overrides can replace any node: get_db yields sessions bound to a test database (in-memory SQLite for speed, or a Dockerized Postgres for fidelity — the honest trade-off being that SQLite lacks Postgres types and constraint behaviours), and get_current_user returns canned users so tests choose their identity per test instead of forging JWTs. pytest fixtures orchestrate it: a session-scoped engine creates the schema once, a function-scoped session rolls back after each test so no test sees another's data, and role fixtures (as_student, as_tpo) make authorization tests read like the permission matrix they verify. The payoff is the testing pyramid working as designed — fast, isolated, deterministic API tests that CI runs on every push.

The conftest.py — Test DB + Rollback Isolation

Engine once per run, schema once, and each test inside a transaction that rolls back — clean state without re-creating tables 500 times. The override points get_db at the test session.

Schema once, transaction per test, rollback = isolation
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.core.db import Base, get_db

# SQLite in-memory: fast, zero setup. (Postgres-in-Docker when you rely on
# JSONB/arrays/constraint semantics — see notes below.)
engine = create_engine("sqlite://",
                       connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=engine)

@pytest.fixture(scope="session", autouse=True)
def create_schema():
    Base.metadata.create_all(engine)        # once per test run
    yield
    Base.metadata.drop_all(engine)

@pytest.fixture
def db():
    """Each test runs inside a transaction that is rolled back."""
    connection = engine.connect()
    trans = connection.begin()
    session = TestSession(bind=connection)
    yield session
    session.close()
    trans.rollback()                        # ← test's writes vanish
    connection.close()

@pytest.fixture
def client(db):
    app.dependency_overrides[get_db] = lambda: (yield db)
    with TestClient(app) as c:              # 'with' runs lifespan too
        yield c
    app.dependency_overrides.clear()        # NEVER leak overrides across tests

# tests/test_students_db.py — tests are now clean and oblivious:
def test_create_then_fetch(client):
    created = client.post("/students", json={
        "name": "Asha", "email": "asha@nitk.edu.in",
        "branch": "CS", "cgpa": 8.7}).json()

    fetched = client.get(f"/students/{created['id']}")
    assert fetched.json()["name"] == "Asha"

def test_previous_test_left_nothing_behind(client):
    assert client.get("/students").json()["total"] == 0    # rollback proof

Fake Identities + the SQLite/Postgres Trade-off

Role fixtures make authorization tests declarative. And know what in-memory SQLite does NOT test — choose Postgres-in-Docker when those behaviours are the point.

as_student / as_tpo fixtures; SQLite speed vs Postgres fidelity
# ── identity fixtures: pick who you are per test ──
from app.core.security import get_current_user

@pytest.fixture
def as_student(client):
    app.dependency_overrides[get_current_user] = lambda: {
        "email": "asha@nitk.edu.in", "role": "student"}
    yield client

@pytest.fixture
def as_tpo(client):
    app.dependency_overrides[get_current_user] = lambda: {
        "email": "rane@college.edu", "role": "tpo"}
    yield client

def test_student_cannot_create_drive(as_student):
    assert as_student.post("/drives", json={"company": "TCS"}).status_code == 403

def test_tpo_can_create_drive(as_tpo):
    assert as_tpo.post("/drives", json={"company": "TCS"}).status_code == 201

def test_anonymous_gets_401(client):
    assert client.post("/drives", json={"company": "TCS"}).status_code == 401
# Three tests = the permission matrix, executable. (JWT encode/decode
# itself gets separate unit tests — here we test AUTHORIZATION, not tokens.)

# ── SQLite vs Postgres-in-Docker for the test DB ──
#   SQLite in-memory:  instant, free, CI-trivial
#     but: no JSONB/ARRAY, looser types, different constraint timing,
#     no CONCURRENTLY, ILIKE differences — code using these will pass
#     tests and fail production (or vice versa)
#   Postgres via docker/testcontainers: identical semantics, ~2s startup
#     pytest fixture: testcontainers.postgres.PostgresContainer("postgres:16")
# Rule: ORM-portable CRUD → SQLite is fine. Postgres-specific SQL,
# JSONB fields, or migration tests → test against real Postgres.
# (Alembic migrations are ALSO testable: upgrade head on a fresh
# container, assert schema — catches the autogenerate misses.)

Key Points to Remember

  • 1dependency_overrides[get_db] → test DB; [get_current_user] → canned identities
  • 2Transaction-per-test with rollback gives isolation without table re-creation
  • 3Always clear overrides after each test — leaked fakes cause haunted failures
  • 4SQLite for portable CRUD speed; Postgres-in-Docker when DB semantics matter

Interview Questions

Sign in to ask Aria
1

Design test isolation for a FastAPI + Postgres service — schema, transactions, fixtures.

HardRazorpay
2

When does testing on SQLite lie to you about Postgres behaviour? Three concrete cases.

HardZerodha
3

Why override get_current_user instead of minting real JWTs in tests? When would you do the opposite?

MediumPostman

Ask Aria about Test Fixtures & Overrides — Real Tests, Fake Infrastructure

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…