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


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:

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:

  1. Yield Fixtures (recommended):
    Use yield instead of return. Setup happens before yield, teardown after.

    @pytest.fixture
    def sending_user(mail_admin):
        user = mail_admin.create_user()
        yield user
        mail_admin.delete_user(user)
    
  2. Add Finalizers:
    Use request.addfinalizer to 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

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:

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


Interactions with Other Parts of the System


Usage Examples Summary


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:

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.