Fixtures

The problem: repeated setup across tests

Every test in the previous concept needed the same TestClient(app) instance. Writing that setup line, or anything more involved, into every single test function duplicates it exactly the way pagination logic duplicated across routes motivated Depends() back in the FastAPI lesson:

def test_get_agent():
    client = TestClient(app)   # repeated in every test
    response = client.get("/agents/42")
    assert response.status_code == 200

def test_get_agent_not_found():
    client = TestClient(app)   # repeated again
    response = client.get("/agents/999")
    assert response.status_code == 404

@pytest.fixture — declare it once, receive it as a parameter

A fixture is a function decorated with @pytest.fixture that provides something a test needs — pytest calls it for you and passes its return value into any test function that names it as a parameter:

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

@pytest.fixture
def client():
    return TestClient(app)

def test_get_agent(client):
    response = client.get("/agents/42")
    assert response.status_code == 200

def test_get_agent_not_found(client):
    response = client.get("/agents/999")
    assert response.status_code == 404

Both test functions declare client as a parameter; pytest recognizes the name matches a fixture, calls client() for each test, and injects its return value automatically. This is genuinely the same shape as Depends(): declare what you need as a parameter, and something else calls a function and hands you the result — Depends() does this for a route needing a shared resource; a fixture does it for a test needing shared setup.

Fixtures for resetting state between tests

A fixture’s real value goes beyond just avoiding repeated setup lines — it’s also the standard place to guarantee test isolation: each test starting from a clean, known state, unaffected by whatever a previous test did.

@pytest.fixture
def client():
    app.state.registry = {}     # reset before every test uses this fixture
    app.state.next_id = 1
    return TestClient(app)

def test_create_agent(client):
    response = client.post("/agents", json={"name": "research_agent", "model": "claude-sonnet"}, headers={"X-Api-Key": "secret-key-123"})
    assert response.status_code == 201

def test_registry_starts_empty(client):
    response = client.get("/agents/1", headers={"X-Api-Key": "secret-key-123"})
    assert response.status_code == 404   # this test's registry is fresh, unaffected by the previous test

Without resetting app.state.registry inside the fixture, an agent created in test_create_agent could still be sitting there when test_registry_starts_empty runs — the second test’s outcome would depend on which order the tests happened to run in, exactly the kind of flakiness Concept 1 named as the actual point of test isolation, now applying to a whole test’s setup, not just a single function’s inputs.

By default, a fixture runs fresh for every test that uses it — this is called function scope, and it’s the default for exactly this reason: fresh state per test, unless you deliberately ask for something shared. (A fixture can be given a broader scope="session" for something genuinely expensive to set up once and safely reuse across an entire test run — outside this lesson’s depth, but worth knowing the option exists.)

Check your understanding
1/4

What does declaring client as a parameter in a test function do, given a fixture named client exists?