Testing in Practice · Foundations: Strategy, pytest and Test Doubles

pytest Fixtures, Parametrisation and Markers

Share setup with fixtures (including scopes and teardown), cover many cases with parametrize, and organise suites with markers.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A test suite for a coupon service: a fixture builds a fresh service per test (with teardown), tmp_path provides a temporary file, parametrize checks a table of cases in one test function, and a custom marker tags slow tests so CI can choose what to run.

The idea

  • Fixtures provide test setup by name; yield fixtures run teardown after the test. Scopes (function, module, session) control reuse.
  • Built-in fixtures: tmp_path (a temporary directory), monkeypatch (environment and attributes), capsys (captured output).
  • @pytest.mark.parametrize runs the same test with many inputs; each case reports separately.
  • Markers (@pytest.mark.slow) let you select or skip groups: pytest -m "not slow". Register them in configuration.

The code

Fixtures, parametrize and markers
# file: coupons.py
import json

class CouponService:
    def __init__(self, path):
        self.codes = json.loads(path.read_text())
        self.closed = False

    def discount(self, code: str, total: float) -> float:
        rule = self.codes.get(code.upper())
        if rule is None or total < rule["min_total"]:
            return 0.0
        return round(total * rule["percent"] / 100, 2)

    def close(self):
        self.closed = True

# file: conftest.py
import json
import pytest
from coupons import CouponService

def pytest_configure(config):
    config.addinivalue_line("markers", "slow: tests that take a while")

@pytest.fixture
def service(tmp_path):
    path = tmp_path / "coupons.json"
    path.write_text(json.dumps({"WELCOME10": {"percent": 10, "min_total": 0}, "BIG20": {"percent": 20, "min_total": 100}}))
    svc = CouponService(path)
    yield svc
    svc.close()                          # teardown runs after each test

# file: test_coupons.py
import pytest

@pytest.mark.parametrize(("code", "total", "expected"), [
    ("WELCOME10", 50, 5.0),
    ("welcome10", 50, 5.0),             # case-insensitive
    ("BIG20", 99.99, 0.0),               # below the minimum
    ("BIG20", 100, 20.0),                # boundary
    ("UNKNOWN", 100, 0.0),
])
def test_discount(service, code, total, expected):
    assert service.discount(code, total) == expected

@pytest.mark.slow
def test_many_lookups(service):
    assert sum(service.discount("WELCOME10", 10) for _ in range(10_000)) == pytest.approx(10_000)

# file: main.py
import pytest
pytest.main(["-v", "-p", "no:cacheprovider", "-m", "not slow", "--no-header"])

Output

============================= test session starts ==============================
collecting ... collected 6 items / 1 deselected / 5 selected

test_coupons.py::test_discount[WELCOME10-50-5.0] PASSED                  [ 20%]
test_coupons.py::test_discount[welcome10-50-5.0] PASSED                  [ 40%]
test_coupons.py::test_discount[BIG20-99.99-0.0] PASSED                   [ 60%]
test_coupons.py::test_discount[BIG20-100-20.0] PASSED                    [ 80%]
test_coupons.py::test_discount[UNKNOWN-100-0.0] PASSED                   [100%]

======================= 5 passed, 1 deselected in <time> ========================

Run this on your computer — see “Get Started” for setup.

The slow test was deselected by the marker expression, and each parametrised case shows up as its own test.

Common mistakes

  • Shared mutable fixtures with module or session scope that leak state between tests.
  • Copy-pasted test functions that differ only in input values; use parametrize.
  • Unregistered markers (typos silently select nothing); register them and use --strict-markers.

Key points

  • Fixtures build setup by name, with teardown after yield.
  • parametrize turns tables of cases into separate tests.
  • Markers organise suites; register them.

Further reading: pytest fixtures · Parametrizing tests · Markers

Practice questions

How does a yield fixture handle teardown?

Answer

Code before yield sets up and the yielded value goes to the test; code after yield runs after the test finishes, even if it failed.

What's the risk of session-scoped fixtures?

Answer

They're shared across tests; if a test mutates them, later tests see the changes and become order-dependent.

How do you skip slow tests in a fast CI stage?

Answer

Mark them (@pytest.mark.slow), register the marker, and run pytest -m 'not slow'.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?