Python Test Fixtures Best Practices — Core Concepts

What fixtures do

A test fixture provides the preconditions a test needs to run. In pytest, fixtures are functions decorated with @pytest.fixture that set up and optionally tear down test dependencies — database connections, test users, temporary files, API clients, or any other state a test requires.

Scope matters

Pytest fixtures support four scopes that control how often the fixture runs:

  • function (default): Runs before each test function. Maximum isolation but slowest.
  • class: Runs once per test class. Tests in the class share the fixture’s result.
  • module: Runs once per test file. All tests in the file share it.
  • session: Runs once for the entire test suite. Every test shares the same instance.

The rule of thumb: use the narrowest scope that keeps tests fast enough. A database connection might be session-scoped (expensive to create), while test data should be function-scoped (must be fresh for each test).

Isolation is non-negotiable

The most common fixture mistake is sharing mutable state. When one test modifies a shared fixture and another test depends on the original state, you get tests that pass individually but fail when run together — or worse, only fail in certain orderings.

Fix this by either making fixtures function-scoped or ensuring shared fixtures return immutable data. If a session-scoped fixture provides a database connection, each test should still set up and tear down its own data within that connection.

Composition over complexity

Instead of building one massive fixture that sets up everything, compose small fixtures together. A test that needs a user with an order doesn’t need a user_with_order fixture. It needs a user fixture and an order fixture that depends on user.

This keeps fixtures reusable. The user fixture works for authentication tests, profile tests, and order tests. The order fixture can be used anywhere an order is needed, not just with users.

Cleanup patterns

Fixtures that create state must clean it up. Pytest supports two patterns:

yield fixtures run setup code before yield and cleanup code after:

Setup happens → yield value to test → test runs → cleanup happens

finalizers are registered during setup and called after the test, even if the test fails. Less common but useful when cleanup is complex.

The critical principle: cleanup must happen regardless of test outcome. A test that fails shouldn’t leave stale data that contaminates subsequent tests.

Factory fixtures

When tests need similar but slightly different data, use factory fixtures — fixtures that return a function rather than a value. The test calls the factory with its specific parameters.

This avoids the “fixture explosion” problem where you end up with user_active, user_inactive, user_admin, user_premium — each nearly identical.

conftest.py organization

Pytest automatically discovers fixtures in conftest.py files. Place fixtures at the right directory level:

  • Root conftest.py: Shared across all tests (database connections, app factories)
  • Directory conftest.py: Specific to a test subdirectory (API test helpers in tests/api/conftest.py)
  • Test file: Fixtures used by only one file belong in that file

This hierarchy keeps fixtures discoverable without polluting the global namespace. When a developer reads a test, they can find its fixtures by checking the local file, then the directory conftest, then the root conftest.

Performance traps

Two patterns silently destroy test suite performance:

Over-isolation: Making every fixture function-scoped when some could safely be module or session-scoped. Creating a fresh database for every test when a transaction rollback would achieve the same isolation in milliseconds.

Hidden I/O: Fixtures that make network calls, read large files, or start services on every invocation. Cache these at higher scopes when the data doesn’t change between tests.

One thing to remember: Great fixtures are invisible — tests read like plain English descriptions of behavior, with all the setup and cleanup machinery hidden behind well-named fixture functions.

pythontestingbest-practices

See Also