Master Pytest: From Basic Assertions to Advanced Fixtures, Parametrize, and Hooks

This guide dives deep into Pytest’s core mechanisms—Fixture, Parametrize, and Hook—showing how to replace repetitive setup code, generate data‑driven tests, and customize test execution, with practical examples that transform a simple test suite into a maintainable, enterprise‑grade framework.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Master Pytest: From Basic Assertions to Advanced Fixtures, Parametrize, and Hooks

Fixture – Dependency Injection Engine

Define reusable resources with @pytest.fixture. The fixture can yield a value to the test and perform cleanup after the test finishes, acting as a dependency‑injection mechanism.

def test_user_create():
    db = connect_db()
    user = create_user(db, "Alice")
    assert user.name == "Alice"
    db.close()

def test_user_delete():
    db = connect_db()  # duplicated
    ...

Advanced version with a session‑scoped fixture:

import pytest

@pytest.fixture(scope="session")
def db_connection():
    conn = connect_db()
    yield conn  # provided to tests
    conn.close()

def test_user_create(db_connection):
    user = create_user(db_connection, "Alice")
    assert user.name == "Alice"

def test_user_delete(db_connection):
    delete_user(db_connection, "Alice")
    assert not get_user(db_connection, "Alice")

Common fixture scopes:

function : a fresh instance for each test function (e.g., isolated HTTP client).

class : one instance per test class (e.g., shared browser session).

module : one instance per .py file (e.g., shared test data files).

session : a single instance for the whole test run (e.g., database connection, mock service).

Fixtures can depend on other fixtures, enabling nesting:

@pytest.fixture
def user(db_connection):
    return create_test_user(db_connection)

def test_profile_update(user):
    update_profile(user.id, "[email protected]")

Parametrize – Data‑Driven Test Generation

Use @pytest.mark.parametrize to generate multiple test cases from a single definition, eliminating duplicated test functions.

@pytest.mark.parametrize(
    "username, password, expected",
    [
        ("admin", "123456", True),   # success
        ("admin", "wrong", False), # wrong password
        ("", "123456", False),      # empty username
        ("hacker", "123456", False) # non‑existent user
    ],
    ids=["success", "wrong_pwd", "empty_user", "user_not_exist"]
)
def test_login(username, password, expected):
    assert login(username, password) is expected

Combine multiple parametrize decorators to create Cartesian products:

@pytest.mark.parametrize("user_type", ["admin", "guest"])
@pytest.mark.parametrize("action", ["read", "write"])
def test_permission(user_type, action):
    # generates 4 test cases
    ...

Parameters can be loaded dynamically from a function, file, or database:

def load_test_cases():
    return [("case1", "input1"), ("case2", "input2")]

@pytest.mark.parametrize("case_id, input_data", load_test_cases())
def test_dynamic(case_id, input_data):
    ...

Hook – Customizing Pytest Behaviour

Hooks are functions placed in conftest.py that Pytest calls at specific points in the test lifecycle.

Terminal summary hook – prints a brief overview after the run:

# conftest.py
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """Print a brief overview after tests finish"""
    passed = len(terminalreporter.stats.get('passed', []))
    failed = len(terminalreporter.stats.get('failed', []))
    total = passed + failed
    print(f"
📊 Test overview: {passed}/{total} passed, failure rate: {failed/total:.1%}")

Retry hook – re‑run a failing test up to two additional times:

# conftest.py
from _pytest.runner import runtestprotocol

def pytest_runtest_protocol(item, nextitem):
    """Retry failed tests up to 2 times"""
    for i in range(3):
        reports = runtestprotocol(item, nextitem=nextitem, log=False)
        if all(report.passed for report in reports if report.when == "call"):
            return True  # success
        if i < 2:
            print(f"⚠️ Test {item.name} failed on attempt {i+1}, retrying…")
    return True

Conditional skip hook – skip tests based on environment variables:

# conftest.py
def pytest_runtest_setup(item):
    """Skip certain tests when ENV=staging"""
    if "skip_on_staging" in item.keywords and os.getenv("ENV") == "staging":
        pytest.skip("Staging environment does not support real payments")

Combining Fixtures, Parametrize, and Hooks – Real‑World Example

An e‑commerce order service demonstrates how the three mechanisms work together.

# conftest.py
import pytest

@pytest.fixture(scope="session")
def api_client():
    return APIClient(base_url="https://api.shop.com")

@pytest.fixture
def test_user(api_client):
    user = api_client.create_user()
    yield user
    api_client.delete_user(user.id)

# test_order.py
import pytest

@pytest.mark.parametrize(
    "product, amount",
    [
        ("LAPTOP", 9999),
        ("PHONE", 5999),
        ("INVALID", 100)  # error case
    ],
    ids=["high_value", "normal", "invalid_product"]
)
def test_create_order(api_client, test_user, product, amount):
    resp = api_client.create_order(user_id=test_user.id, product=product, amount=amount)
    if product == "INVALID":
        assert resp.status_code == 400
    else:
        assert resp.status_code == 201

Benefits of this composition:

Session‑scoped api_client is reused across all tests.

Function‑scoped test_user provides isolation and automatic cleanup.

One test function covers multiple scenarios via parametrize.

Additional hooks can log request details on failure.

Common Pitfalls & Correct Practices

Do not embed business logic inside fixtures; keep fixtures limited to resource setup.

Avoid overusing session scope, which can cause state leakage; prefer function scope when isolation is needed.

When a parametrize call has too many parameters, split the data into multiple groups or externalize it (e.g., YAML files).

Hooks should perform lightweight actions; heavy operations inside hooks can slow the entire test suite.

Key Takeaways

Pytest is a programmable testing platform. Mastering the three core mechanisms enables building maintainable automation frameworks:

Fixture – manages dependencies and resource lifecycles.

Parametrize – generates data‑driven test cases, reducing duplication.

Hook – customizes execution flow (reporting, retries, conditional skips).

By combining these tools, a concise test suite can achieve high coverage, fast feedback, and easy extensibility.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Testinghookspytestfixturesparametrize
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.