fixtures.rst
Overview
This file serves as a comprehensive guide and reference documentation for using **pytest fixtures**, a powerful feature of the pytest testing framework that allows modular, reusable, and maintainable setup and teardown logic for tests. It explains the principles, usage patterns, and advanced capabilities of fixtures in pytest, illustrating how fixtures can be requested by tests and other fixtures, how to manage fixture scope and lifecycle, parameterization, finalization, and best practices for ensuring safe and effective test environments.
The content is presented as a tutorial combined with practical examples, code snippets, and explanations to help users understand how to write and organize fixtures to improve test quality and efficiency.
Purpose and Functionality
To teach users how to define and use pytest fixtures for test setup and teardown.
To illustrate how fixtures can be requested by tests and other fixtures, including multiple requests and fixture dependencies.
To explain fixture scopes and lifetime management, including function, class, module, package, and session scopes.
To demonstrate advanced features such as autouse fixtures, dynamic scopes, fixture finalization (teardown), parametrization, and factory patterns.
To provide guidance on overriding fixtures, using fixtures across projects, and safely structuring fixtures to avoid resource leaks.
To show how to introspect test context and pass data via markers.
To clarify usage of fixtures in classes and modules using
usefixtures.To supply best practices and patterns for organizing fixtures to support scalable and maintainable test suites.
Detailed Explanations
The file primarily consists of explanatory sections punctuated with annotated Python code examples that illustrate fixture concepts and idioms.
Fixture Requesting
Fixtures are functions decorated with `@pytest.fixture`. Tests request fixtures by naming them as function arguments. pytest automatically finds, executes, and injects fixture return values into test functions.
Example:
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
fruit_salad = FruitSalad(*fruit_bowl)
assert all(fruit.cubed for fruit in fruit_salad.fruit)
Here, `test_fruit_salad` requests `fruit_bowl`. pytest runs the `fruit_bowl` fixture and passes its return value to the test.
Fixtures can also request other fixtures, forming a dependency graph:
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
order.append("b")
assert order == ["a", "b"]
Fixture Reusability and Isolation
Each test requesting a fixture gets a fresh instance by default, ensuring tests are isolated and do not share mutable state inadvertently.
Example:
def test_string(order):
order.append("b")
assert order == ["a", "b"]
def test_int(order):
order.append(2)
assert order == ["a", 2]
Each test runs the `order` fixture anew.
Requesting Multiple Fixtures
Tests and fixtures can request multiple fixtures simultaneously by listing them as parameters.
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def second_entry():
return 2
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
def test_string(order):
order.append(3.0)
assert order == ["a", 2, 3.0]
Fixture Caching
Fixtures are executed only once per test invocation and their results are cached for that test, so multiple requests for the same fixture within a test share the same instance.
Autouse Fixtures
Autouse fixtures run automatically for all tests without needing explicit request.
@pytest.fixture(autouse=True)
def setup_order(order, first_entry):
order.append(first_entry)
Tests implicitly get this fixture applied.
Fixture Scopes
Fixtures can be scoped to control when they are created and destroyed:
function(default): created per test function.class: shared across tests in a class.module: shared across tests in a module.package: shared across a package.session: shared for an entire test session.
Example of module-scoped fixture:
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
Dynamic Fixture Scope
Scope can be determined dynamically by a callable accepting `fixture_name` and `config`.
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
Finalization / Teardown
Fixtures can define teardown logic with two main approaches:
Yield Fixtures (recommended):
Useyieldinstead ofreturn. Setup happens beforeyield, teardown after.@pytest.fixture def sending_user(mail_admin): user = mail_admin.create_user() yield user mail_admin.delete_user(user)Add Finalizers:
Userequest.addfinalizerto register teardown callbacks.@pytest.fixture def receiving_user(mail_admin, request): user = mail_admin.create_user() def delete_user(): mail_admin.delete_user(user) request.addfinalizer(delete_user) return user
Finalizers run in last-in-first-out order.
Safe Teardowns and Fixture Structure
Prefer small, atomic fixtures with dedicated setup and teardown.
Compose complex setups from smaller fixtures.
This reduces risk of leftover state if exceptions occur.
Example: Creating user and browser driver are separate fixtures with independent teardown.
Running Multiple Assertions Safely
Use autouse fixtures and higher fixture scopes (e.g., class scope) to set up shared state once per test class, then run multiple assertions in separate test methods without redundant setup.
Example is a Selenium login test suite with class-scoped fixtures.
Request Context Introspection
Fixtures can accept a `request` parameter to introspect the test requesting the fixture, e.g., to read module attributes or markers.
Example:
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_conn = smtplib.SMTP(server, 587, timeout=5)
yield smtp_conn
smtp_conn.close()
Passing Data to Fixtures via Markers
Fixtures can access test markers to obtain data:
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
data = marker.args[0] if marker else None
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
Factories as Fixtures
Fixtures can return factory functions to generate multiple instances within a test.
@pytest.fixture
def make_customer_record():
def _make_customer_record(name):
return {"name": name, "orders": []}
return _make_customer_record
Parametrizing Fixtures
Fixtures can be parametrized to run dependent tests multiple times with different fixture values.
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_conn = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_conn
smtp_conn.close()
Tests depending on `smtp_connection` run once per parameter.
Using Fixtures in Classes and Modules with usefixtures
To apply fixtures to all tests in a class or module without explicit request:
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
def test_cwd_starts_empty(self):
...
Overriding Fixtures
Fixtures can be overridden at various levels:
Folder level (
conftest.pyin subfolders)Module level (local fixture with same name)
Direct test parameterization overrides fixture value
Parametrized and non-parametrized fixtures can override each other depending on scope.
Using Fixtures from Other Projects
Fixtures from other pytest plugin projects can be made available by setting `pytest_plugins` in your `conftest.py`:
pytest_plugins = "mylibrary.fixtures"
Implementation Details and Algorithms
Fixture requests form a directed acyclic graph (DAG) of dependencies; pytest resolves and executes fixtures in dependency order.
Fixture results are cached per test invocation to ensure consistency and avoid redundant setup.
Teardown finalizers are executed in reverse order of setup to safely unwind resources.
Parametrization expands tests dynamically by injecting multiple fixture values transparently.
Autouse fixtures provide implicit fixture injection, reducing boilerplate.
Dynamic scope allows runtime control of fixture lifetime based on configuration.
Use of
requestobject allows fixtures to introspect test context, enabling dynamic fixture behavior.
Interactions with Other Parts of the System
Fixtures interact closely with pytest’s test discovery and execution engine.
They provide reusable setup/teardown logic that tests depend on.
Fixture functions themselves may depend on other fixtures, forming a layered setup.
Fixtures can interact with external resources, e.g., databases, network connections, browser drivers.
Parametrized fixtures influence test collection by generating multiple test cases for combinations of parameters.
Fixtures can be shared across multiple test modules via
conftest.pyfiles or plugins.Integration with pytest markers and request context allows flexible data passing and behavior customization.
Fixtures can be overridden or extended in different test scopes or projects to tailor test environments.
Usage Examples Summary
Basic fixture usage with
@pytest.fixtureand test requesting fixture by argument name.Fixtures requesting other fixtures for dependency composition.
Autouse fixtures to apply setup automatically.
Scoped fixtures for sharing resources efficiently.
Yield fixtures for setup and teardown.
Adding finalizers for cleanup.
Parametrized fixtures for exhaustive testing.
Factory fixtures for generating multiple instances.
Using markers and request context to pass test-specific data.
Overriding fixtures at different levels.
Using fixtures across projects via plugins.
Mermaid Diagram: Fixture System Structure
classDiagram
class Fixture {
+name: str
+scope: str
+params: list
+request(): object
+teardown(): void
}
class TestFunction {
+name: str
+request_fixtures(): list~Fixture~
+run(): void
}
class RequestContext {
+module: Module
+node: TestFunction
+addfinalizer(func): void
+get_marker(name): Marker
}
Fixture <.. TestFunction : "requested by"
Fixture o-- "0..*" Fixture : "depends on"
Fixture *-- RequestContext : "uses for introspection and finalizers"
TestFunction .. RequestContext : "has"
This diagram models the core entities and their relationships:
Fixture: Represents a pytest fixture function. It has properties like name, scope, and parameters. It can be requested by tests or other fixtures. It supports setup (
request()) and teardown (teardown()) phases.TestFunction: Represents a pytest test function that requests one or more fixtures and uses their results for its execution.
RequestContext: Represents the pytest internal request object passed to fixtures for introspection, marker access, and registering finalizers (teardown callbacks).
Fixtures can depend on other fixtures, forming a dependency graph. Tests request fixtures to receive prepared test data or setup. The `RequestContext` facilitates advanced fixture operations like introspection and cleanup.
This documentation should serve as a detailed reference and learning guide for effectively using pytest fixtures, covering basics through advanced scenarios, improving test modularity, performance, and safety.