Pytest Fixtures, Parametrization & Mocking

Writing maintainable test suites requires mastering Pytest Fixtures, Matrix Parametrization (@pytest.mark.parametrize), and Mocking (unittest.mock / pytest-mock). Fixtures act as dependency injection DAGs with explicit lifecycle scopes, while parametrization eliminates duplicate test logic, and mocking isolates test units from external infrastructure.

This chapter details Fixture scope lifecycles, teardown yield mechanics, matrix parametrization, and mocker patch boundaries.


1. Fixture Scope Lifecycles & Dependency Injection DAG

A @pytest.fixture provides explicit setup, resource instantiation, and automatic dependency injection into test functions based on parameter names:

Fixture Scope Lifecycles:

1. function (Default): Instantiated per test function call; torn down immediately after test completes.
2. class:            Instantiated once per test class.
3. module:           Instantiated once per Python test module file.
4. package:          Instantiated once per test package directory.
5. session:          Instantiated ONCE for the ENTIRE pytest execution run (e.g. Test Database container).

Dependency Injection DAG:

Fixtures can request other fixtures as arguments, forming a Directed Acyclic Graph (DAG):

import pytest

@pytest.fixture(scope="session")
def db_engine():
    engine = create_test_db_engine()
    yield engine
    engine.dispose()  # Session Teardown!

@pytest.fixture(scope="function")
def db_session(db_engine):  # Requests session-scoped db_engine fixture!
    connection = db_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    yield session  # Yields control to test function!

    # Function Teardown: Rollback transaction to isolate test state!
    session.close()
    transaction.rollback()
    connection.close()

2. Matrix Testing with @pytest.mark.parametrize

Instead of writing 10 identical test functions for different inputs, use @pytest.mark.parametrize to generate multiple test executions dynamically:

import pytest

@pytest.mark.parametrize(
    "input_str, expected_count",
    [
        ("hello world", 11),
        ("", 0),
        ("python", 6),
        ("πŸš€", 1),
    ],
    ids=["standard", "empty", "ascii", "emoji"]  # Descriptive test output names!
)
def test_string_length(input_str: str, expected_count: int):
    assert len(input_str) == expected_count

3. Mocking & Patch Boundaries (pytest-mock / mocker)

Mocking replaces real objects (like network calls or payment gateways) with test double MagicMock instances.

CRITICAL MOCKING RULE: Patch where an object is USED, not where it is DEFINED!

# app/services.py
from app.clients import PaymentClient

def process_order(amount):
    client = PaymentClient()
    return client.charge(amount)
# tests/test_services.py
def test_process_order(mocker):
    # βœ… CORRECT: Patch PaymentClient inside 'app.services' (where it is USED)!
    mock_client = mocker.patch("app.services.PaymentClient")
    mock_client.return_value.charge.return_value = {"status": "success"}

    result = process_order(100)
    assert result["status"] == "success"
    mock_client.return_value.charge.assert_called_once_with(100)

Why pytest-mock (mocker) Wins:

mocker.patch() automatically un-patches all mocked objects at the end of the test function, preventing mock leakage across test suites (unlike raw unittest.mock.patch which requires manual teardown).

Display Options
Appearance
Text Size
100%