Pytest Fixtures

Difficulty: Advanced

Pytest fixtures are a powerful mechanism for setting up test prerequisites and tearing them down after tests complete. When testing FastAPI applications, fixtures are used to create test clients, mock dependencies, set up test databases, and manage test data. Combined with FastAPI's dependency override system, fixtures enable thorough, isolated testing.

A fixture is a function decorated with @pytest.fixture. It runs before each test that requests it (or before all tests if scoped to 'session' or 'module'). Fixtures can yield a value (for setup/teardown patterns) or return a value. Tests request fixtures by including them as function parameters.

The most important fixture for FastAPI testing is the test client. You create it once and share it across tests. Using a fixture ensures the client is created consistently and can include any setup (like dependency overrides) that all tests need.

Dependency overrides are crucial for testing. In production, your app might depend on a real database, external APIs, or authentication services. In tests, you replace these with mocks using app.dependency_overrides. A common pattern is to override the database dependency with an in-memory SQLite database, and override authentication to always return a test user.

Test isolation is critical - each test should be independent and not affect other tests. For database tests, use a transaction rollback strategy: wrap each test in a transaction, and roll it back after the test. This ensures the database is clean for every test without recreating tables.

The conftest.py file is a special pytest file for sharing fixtures across multiple test files. Fixtures defined in conftest.py are automatically available to all tests in the same directory and subdirectories. This is the ideal place for app-wide fixtures like the test client and database setup.

Code examples

Basic Pytest Fixtures for FastAPI

# conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

@pytest.fixture
def client():
    """Create a test client for each test."""
    return TestClient(app)

@pytest.fixture
def sample_user():
    """Provide sample user data."""
    return {"name": "Alice", "email": "alice@example.com"}

# test_users.py
def test_create_user(client, sample_user):
    response = client.post("/users", json=sample_user)
    assert response.status_code == 201
    assert response.json()["name"] == sample_user["name"]

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

Fixtures are declared in conftest.py and automatically available to test functions. The client fixture creates a TestClient. The sample_user fixture provides reusable test data. Tests request fixtures by name in their parameter list.

Dependency Overrides in Fixtures

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

# Mock database
def mock_get_db():
    return {"type": "test_db"}

# Mock authenticated user
def mock_get_current_user():
    return {"id": 1, "name": "Test User", "role": "admin"}

@pytest.fixture
def client():
    # Override dependencies
    app.dependency_overrides[get_db] = mock_get_db
    app.dependency_overrides[get_current_user] = mock_get_current_user

    client = TestClient(app)
    yield client

    # Cleanup: restore original dependencies
    app.dependency_overrides.clear()

@pytest.fixture
def unauthenticated_client():
    """Client without auth overrides for testing 401 responses."""
    app.dependency_overrides[get_db] = mock_get_db
    # Do NOT override get_current_user

    client = TestClient(app)
    yield client

    app.dependency_overrides.clear()

# test_protected.py
def test_admin_access(client):
    # Uses mock user with admin role
    response = client.get("/admin")
    assert response.status_code == 200

def test_unauthorized_access(unauthenticated_client):
    response = unauthenticated_client.get("/admin")
    assert response.status_code == 401

The client fixture overrides database and auth dependencies with mocks. unauthenticated_client only overrides the database, leaving auth intact. yield ensures cleanup (clearing overrides) runs after each test.

Test Database with Transactions

# 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.database import Base, get_db

# Test database
TEST_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="session", autouse=True)
def create_tables():
    """Create tables once for all tests."""
    Base.metadata.create_all(bind=engine)
    yield
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def db_session():
    """Create a fresh session for each test with rollback."""
    connection = engine.connect()
    transaction = connection.begin()
    session = TestSession(bind=connection)

    yield session

    session.close()
    transaction.rollback()
    connection.close()

@pytest.fixture
def client(db_session):
    """Test client with test database."""
    def override_get_db():
        yield db_session

    app.dependency_overrides[get_db] = override_get_db
    client = TestClient(app)
    yield client
    app.dependency_overrides.clear()

# test_items.py
def test_create_and_list_items(client):
    # Create an item
    response = client.post("/items", json={"name": "Widget", "price": 9.99})
    assert response.status_code == 201

    # List items (should see the created item)
    response = client.get("/items")
    assert response.status_code == 200
    assert len(response.json()) == 1

    # After this test, the transaction is rolled back
    # The next test starts with a clean database

Tables are created once (session scope). Each test gets a fresh session wrapped in a transaction that is rolled back after the test. This provides test isolation without the overhead of recreating tables for every test.

Key points

Concepts covered

Fixtures, conftest, Dependency Override, Test Database, Isolation