Fixtures and Test Data
A fixture builds the state a test needs and tears it down; scope decides how often, and sharing it wrongly couples your tests.
@pytest.fixture(scope="function", params=None, autouse=False, ids=None, name=None)A fixture is a function that produces the state a test needs. Decorate it with @pytest.fixture(scope="function", params=None, autouse=False, ids=None, name=None), then name it as a parameter of the test, and pytest calls it and passes the result in. Fixtures may request other fixtures the same way, so a session fixture can build on a config fixture without either knowing where it is used. A fixture defined in conftest.py is available to every test in that directory and below with no import.
Setup, yield, teardown
A fixture that yields hands the value to the test and resumes afterwards: everything after the yield is teardown, and the shape is exactly that of a contextlib.contextmanager. The longhand is request.addfinalizer(cleanup), which does the same job with the cleanup written first. pytest runs that teardown whether the test passed or failed, so you do not need a try/finally for a failing test; wrap the yield in one when the fixture's own setup can still raise after the resource exists, because a fixture that raises before it yields gets no teardown at all.
Scope, and the coupling it buys
The default function scope rebuilds the state for every test. class, module, package and session build it once per that unit — right for something expensive and read-only, such as a started service or a compiled schema. The trap is a wider-scoped object a test mutates: the next test inherits the change, the suite passes in file order, and one test run alone fails. Keep anything a test writes to at function scope, or undo the writes in teardown — a database transaction rolled back, a directory from the built-in tmp_path fixture, which is a fresh pathlib.Path per test.
Example
When to use it
- Give every test its own empty database, directory or client without repeating the setup
- Start something slow once per session and still keep each test independent
- Build five similar records with one line each using a factory fixture
- Force test-only environment variables so a stray real API key can never be picked up
More examples
Expensive once, mutable per test
The split that keeps a database suite both fast and independent: schema creation is session-scoped, data is function-scoped and rolled back. Reach for this the moment two tests start interfering, instead of adding cleanup code to each test.
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from reporting.models import Base
@pytest.fixture(scope="session")
def engine():
# Built once: creating the schema is slow and no test changes it.
engine = create_engine("postgresql+psycopg://localhost/reporting_test")
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def db(engine):
# Function scope: every test gets a transaction that is never committed,
# so rows written by one test are invisible to the next.
connection = engine.connect()
transaction = connection.begin()
session = Session(bind=connection)
try:
yield session
finally:
session.close()
transaction.rollback()
connection.close()A factory fixture, for tests that need several objects
A fixture that returns a function rather than a value. Use it when the count or the field values differ per test: the defaults keep each call short, and the fixture still owns the cleanup. The alternative - one fixture per shape of object - multiplies fixtures without reducing work.
@pytest.fixture
def make_invoice():
created = []
def _make(cents=1000, status="open", **extra):
invoice = Invoice(cents=cents, status=status, **extra)
created.append(invoice)
return invoice
yield _make
for invoice in created:
invoice.delete()
def test_open_total_ignores_paid(make_invoice):
make_invoice(cents=500)
make_invoice(cents=700)
make_invoice(cents=900, status="paid")
assert open_total() == 1200autouse, and when it is not a trap
autouse is right for guard rails that must not be forgotten - stubbing out configuration, pointing caches at tmp_path, blocking a real credential. It is wrong for supplying data, because a reader of the test then cannot see where the data came from. monkeypatch undoes every change when the test ends.
@pytest.fixture(autouse=True)
def isolate_environment(monkeypatch, tmp_path):
# Applies to every test in this file or package without being requested.
monkeypatch.setenv("BILLING_API_KEY", "test-key")
monkeypatch.setenv("BILLING_CACHE_DIR", str(tmp_path))
monkeypatch.delenv("HTTP_PROXY", raising=False)
Discussion