Testing with Pytest
Pytest fixtures: clean tests without the ceremony
Why Pytest clicks
Pytest is one of those tools that feels obvious after you’ve used it for a bit. Tests are just functions. Assertions read like normal Python. And when you need context—database sessions, config, mock data—you reach for fixtures instead of duct tape.
Pytest fixtures handle setup and teardown for you, so each test runs in a predictable, isolated state. You stop worrying about how the test is prepared and focus on what you’re verifying.
The problem fixtures solve
Without fixtures, test setup usually ends up in one of two places:
- Repeated inline setup code (copy-paste city)
- Shared global state (works locally, explodes in CI)
Fixtures give you a middle ground: reusable setup logic that Pytest injects only when a test asks for it.
Tests passing individually but failing as a group because they accidentally depend on shared state. Fixtures help shut that door.
Using fixtures for clean, readable tests
Let’s start with a small example. Assume we have a function that calculates the area of a rectangle. The function itself is boring—and that’s good. Tests should be boring too.
# app.py
def calculate_area(length: float, width: float) -> float:
if length <= 0 or width <= 0:
raise ValueError("Length and width must be positive numbers")
return length * width
Now the test. Instead of hard-coding values directly in the test, we move setup into a fixture.
# test_logic.py
import pytest
from app import calculate_area
@pytest.fixture
def clean_rectangle_data():
"""
Provides valid rectangle dimensions for tests.
Keeps setup logic out of the test body.
"""
return {"length": 10.0, "width": 5.0}
def test_area_calculation(clean_rectangle_data):
area = calculate_area(
clean_rectangle_data["length"],
clean_rectangle_data["width"]
)
assert area == 50.0
The test name says what’s being tested. The fixture name explains the setup. No scrolling, no hidden dependencies.
Markers: controlling test execution at scale
As test suites grow, not all tests are equal. Some are fast. Some hit external systems. Pytest markers let you label tests and decide when they should run.
import pytest
@pytest.mark.slow
def test_large_dataset_processing():
# Expensive test logic here
assert True
You can then include or exclude tests at runtime:
pytest -m "not slow"
Local development
- Run fast tests only
- Quick feedback loop
- Less waiting, more fixing
CI pipelines
- Run full test suite
- Include slow or integration tests
- Higher confidence before deploy
Pros and cons
Pros
- Minimal boilerplate—tests look like normal Python
- Fixtures encourage isolation and reuse
- Markers make large test suites manageable
- Plays nicely with CI/CD tools
Cons
- Fixture overuse can hide setup if names aren’t clear
- Debugging deeply nested fixtures takes practice
- Markers require discipline to stay meaningful
Comments
Post a Comment