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.
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 expectedCombine 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 TrueConditional 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 == 201Benefits 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
