Fixtures
Fixtures provide test dependencies - database connections, API clients, sample data - via dependency injection. Define once in conftest.py, inject by parameter name.
Recipe
# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return {"id": 1, "email": "ada@example.com"}
# tests/test_users.py
def test_user_email(sample_user):
assert "@" in sample_user["email"]When to reach for this:
- Multiple tests share the same setup
- Tests need teardown (close connections, delete temp files)
- You want composable test dependencies
Working Example
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
@pytest.fixture(scope="session")
def engine():
eng = create_engine("sqlite:///:memory:")
yield eng
eng.dispose()
@pytest.fixture
def db_session(engine):
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def user_factory(db_session):
def _make(email: str = "test@example.com"):
user = User(email=email)
db_session.add(user)
db_session.flush()
return user
return _makedef test_create_user(user_factory):
user = user_factory(email="ada@example.com")
assert user.id is not NoneWhat this demonstrates:
scope="session"shares the engine across all tests- Per-test
db_sessionrolls back for isolation - Factory fixture returns a callable for flexible data
yieldfixtures run teardown after the test
Deep Dive
Fixture Scopes
| Scope | Lifetime | Use for |
|---|---|---|
| function | One test | Default, most fixtures |
| class | One test class | Shared class setup |
| module | One file | Expensive per-file setup |
| session | Entire run | DB engine, app instance |
Composing Fixtures
@pytest.fixture
def api_client(app):
return TestClient(app)api_client automatically receives the app fixture.
Gotchas
- Scope mismatch - session fixture depending on function fixture. Fix: broader scope cannot depend on narrower scope.
- Mutable shared state - session-scoped dict modified by tests. Fix: function scope or copy-on-write.
- Missing teardown - connections leak. Fix: use
yieldfixtures for cleanup. - Fixture name typos - injected as a parameter that pytest does not recognize. Fix: parameter name must match fixture name exactly.
- Over-fixturing - one-line setup in a fixture. Fix: inline simple arrange; fixture only when reused.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| setUp/tearDown | unittest style | pytest project |
| Manual setup in each test | One-off cases | Shared setup |
| Factory pattern (no fixture) | Simple data | Need teardown |
FAQs
Where do fixtures live?
conftest.py in tests/ or subdirectories. Auto-discovered by pytest.
What is yield vs return?
yield enables teardown code after the test. return has no teardown.
How do I use a fixture in another fixture?
Declare it as a parameter: def api(app): ....
What is autouse?
@pytest.fixture(autouse=True) runs for every test in scope without being requested.
How do I override a fixture?
Redefine it in a subdirectory conftest.py or use @pytest.fixture(name="...").
Can fixtures be async?
Yes with pytest-asyncio: @pytest.fixture + async def.
What is params on fixtures?
@pytest.fixture(params=[1, 2, 3]) runs the test once per param value.
How do I access a fixture without injecting?
request.getfixturevalue("name") inside a fixture or test.
Session scope and test isolation?
Session fixtures persist state. Ensure tests do not mutate shared objects.
Fixture vs factory?
Fixture provides an instance. Factory fixture returns a function that creates instances.
Related
- pytest Setup - conftest discovery
- Parametrization - multiple test inputs
- Mocking & Patching - mock fixtures
- Testing Web APIs - TestClient fixtures
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.