Fixture Management
Fixture Management is a core module responsible for providing a flexible and powerful fixture system in the testing framework. Fixtures are reusable components that provide setup and teardown logic or data to tests. This module addresses the problem of managing fixture lifecycles, scoping, parametrization, and dependencies, enabling test functions to request fixtures declaratively and have their dependencies injected automatically.
Core Concepts and Purpose
Fixtures: Functions that prepare test data or state and optionally perform cleanup after tests run.
Fixture Scope: Controls the lifetime of fixture instances. The available scopes are
function,class,module,package, andsession.Fixture Parametrization: Fixtures can be parameterized to run multiple times with different data inputs.
Dependency Injection: Fixtures can depend on other fixtures; the system resolves this dependency graph and manages execution order.
Autouse Fixtures: Fixtures that are automatically applied to tests without explicit declaration.
Fixture Caching: Fixture values are cached per scope to avoid repeated setup when not necessary.
Fixture Finalization: Fixtures can register finalizers (teardown code) that run after tests or scope ends.
The [fixtures.py](/projects/286/67370) module encapsulates these concepts by defining classes and functions that manage fixture definitions, requests, lifecycle, and execution.
How Fixture Management Works
Fixture Definition and Registration
Fixtures are defined as Python functions decorated with the `@fixture` decorator. The decorator wraps the function in a `FixtureFunctionDefinition` object, which holds metadata such as scope, parameters, and autouse flags.
Example snippet illustrating fixture declaration:
@fixture(scope="module", params=[1, 2])
def sample_fixture(request):
return request.param
During test collection, the `FixtureManager` parses fixture functions from test modules, plugins, and conftest files via the `parsefactories` method. It creates `FixtureDef` objects representing each fixture, storing the underlying function, scope, parameters, and other attributes. These `FixtureDef` instances are registered in a map keyed by fixture name (`_arg2fixturedefs`).
Fixture Request and Resolution
When a test or fixture requests a fixture by name, a `FixtureRequest` object manages the retrieval and execution of the fixture function. The key method `getfixturevalue(argname)` dynamically resolves the fixture by:
Looking up applicable
FixtureDefinstances for the requested fixture name and test context.Checking for cached fixture results to avoid redundant execution.
Executing the fixture function if no cached value exists, injecting dependencies by recursively resolving requested fixtures.
Caching the fixture's return value or exception.
Registering finalizers to ensure proper teardown.
The `FixtureRequest` class has two primary implementations:
TopRequest: Represents the request context of a test function itself.SubRequest: Represents nested requests from fixtures depending on other fixtures.
Both track scope to enforce correct usage: higher scoped fixtures cannot depend on lower scoped ones.
Fixture Lifecycle and Finalization
`FixtureDef` objects manage the fixture lifecycle, including setup, teardown, and caching. When a fixture is executed via the `execute` method, it calls `pytest_fixture_setup`, which runs the fixture function, passing resolved dependencies as keyword arguments.
If the fixture function uses `yield`, the code after `yield` is registered as a finalizer to run teardown logic.
The `finish` method runs all finalizers in reverse order, handling exceptions and clearing cached results.
Fixture Parametrization
Fixtures can be parametrized, causing tests to run multiple times with different fixture values. The `FixtureManager`'s `pytest_generate_tests` hook generates parametrized test instances based on fixture parameters.
The system supports multiple layers of parameterization, including explicit `@pytest.mark.parametrize` and fixture-level parameters, coordinating them carefully to manage caching and execution order.
Dependency Graph and Closure
The `FixtureManager` calculates the complete set of fixtures used by each test via `getfixtureinfo` and `getfixtureclosure`. This includes:
Direct parameters on test functions.
Fixtures marked as
autouseor requested viausefixtures.Transitive dependencies of all requested fixtures.
The closure calculation ensures all required fixtures are known and their dependencies resolved before test execution.
Reordering of Items for Parametrized Fixtures
To minimize redundant setup and teardown, test items are reordered based on fixture parameter keys with higher scopes. This grouping ensures fixtures with expensive setup are reused efficiently.
Interaction with Other System Components
Test Collection (
nodes.py,python.py): The fixture system depends on collected test nodes and their metadata to determine fixture applicability and scoping.Test Execution (
runner.py): Fixtures are resolved and setup before test function calls; their teardown happens afterward.Configuration (
config/__init__.py): Provides user options like defaultusefixturesand fixture visibility scope.Marking and Parametrization (
mark/structures.py): Fixtures interact with the marker system for parametrization and autouse behavior.Plugin System (
hookspec.py,config/__init__.py): Plugins can define and register fixtures, extending the fixture system.
The `FixtureManager` acts as a central registry, coordinating fixture discovery, resolution, and lifecycle in cooperation with collection and execution components.
Important Classes and Their Roles
Class | Role |
|---|---|
`FixtureDef` | Encapsulates a fixture function, its scope, parameters, caching, and finalizers. |
`FixtureRequest` | Abstract base for fixture request context; manages resolution and access to fixtures. |
`TopRequest` | Represents the fixture request for test functions (function-scoped). |
`SubRequest` | Represents nested fixture requests (used within fixtures). |
`FixtureManager` | Manages all fixture definitions, discovery, and fixture info per test item. |
`FixtureFunctionMarker` | Decorator metadata for fixture functions. |
`FixtureFunctionDefinition` | Wrapper for fixture functions supporting descriptor protocol and error on direct call. |
Key Functional Workflows
Fixture Resolution and Execution Sequence
Test function or fixture requests a fixture name.
FixtureRequest.getfixturevalue:Checks cache in
FixtureDef.cached_result.If cached and matches the current parameter, returns cached value.
Otherwise, calls
FixtureDef.execute.
FixtureDef.execute:Recursively resolves dependencies via
request._get_active_fixturedef.Calls
pytest_fixture_setupto invoke fixture function.Caches result or exception.
Registers teardown finalizers.
When test or scope ends,
FixtureDef.finishruns finalizers in reverse order.
Parametrization Handling
Parametrized fixtures cause multiple fixture instances with different parameters.
FixtureManager.pytest_generate_testsinjects parametrization into test generation.Fixture cache keys include parameter values to differentiate instances.
Fixture tree is rebuilt when parameters differ to enforce scope correctness.
Illustrative Code Snippets
Fixture Definition Decorator
def fixture(...):
fixture_marker = FixtureFunctionMarker(...)
if fixture_function:
return fixture_marker(fixture_function)
return fixture_marker
Fixture Execution and Caching
def execute(self, request: SubRequest) -> FixtureValue:
if self.cached_result is not None:
if cache_key matches:
return cached value
else:
self.finish(request) # teardown old instance
result = pytest_fixture_setup(self, request)
self.cached_result = (result, cache_key, None)
return result
Fixture Setup Hook
def pytest_fixture_setup(fixturedef, request):
kwargs = {arg: request.getfixturevalue(arg) for arg in fixturedef.argnames}
fixturefunc = resolve_fixture_function(fixturedef, request)
result = call_fixture_func(fixturefunc, request, kwargs)
return result
Unique Approaches and Design Patterns
Recursive Dependency Resolution: Fixtures can depend on other fixtures; the system resolves dependencies transitively and caches results, avoiding redundant execution.
Scope Enforcement: The system strictly enforces fixture scope hierarchy, preventing higher scope fixtures from depending on lower scope fixtures to avoid invalid lifetimes.
Finalizer Management: Uses explicit finalizer registration allowing fixtures to clean up resources reliably after tests or scopes end.
Parametrization Integration: Parametrized fixtures are integrated into the test collection and execution phases, including reordering items to optimize fixture reuse.
Dynamic Fixture Requests: Supports dynamic fixture retrieval via
request.getfixturevalue, enabling conditional or runtime fixture usage.PseudoFixtures: Special built-in fixtures like
requestare represented as pseudo fixtures with custom handling.
Mermaid Sequence Diagram: Fixture Resolution and Execution
sequenceDiagram
participant Test as Test Function
participant TopReq as TopRequest
participant FixtureMgr as FixtureManager
participant FixtureDef as FixtureDef
participant SubReq as SubRequest
participant FixtureFunc as Fixture Function
Test->>TopReq: getfixturevalue("fixture_name")
TopReq->>TopReq: _get_active_fixturedef("fixture_name")
TopReq->>FixtureMgr: getfixturedefs("fixture_name", node)
FixtureMgr-->>TopReq: FixtureDef list
TopReq->>SubReq: create SubRequest for fixture
SubReq->>FixtureDef: execute(SubRequest)
FixtureDef->>SubReq: resolve dependencies (getfixturevalue for args)
SubReq->>FixtureFunc: call fixture function with dependencies
FixtureFunc-->>FixtureDef: fixture value or generator
FixtureDef-->>SubReq: cache fixture value
SubReq-->>TopReq: fixture value
TopReq-->>Test: fixture value injected
Summary
The Fixture Management module implements an extensible, scoped, and parametrized fixture system. It enables declarative dependency injection of test resources with automatic setup and teardown, supporting complex fixture graphs with caching and lifecycle control. This system is tightly integrated with test collection and execution phases to optimize test runs and provide a powerful foundation for test modularity and reuse.