pytest Suites skill for Claude Code

Strict, fast pytest suites with clean fixtures. Claude sets pytest up strictly so typos in markers and config fail loudly, then designs fixtures with the narrowest safe scope, rollback-per-test database sessions, and factory fixtures instead of shared mutable data. Tests use parametrize with readable ids, tmp_path and monkeypatch, and the suite is proven green in random order and in parallel.

  • namepytest-suites
  • size213 lines · ≈2,050 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Structures and writes pytest suites with layered conftest fixtures, factory fixtures, parametrize with ids, registered markers, monkeypatch, tmp_path, caplog, pytest-cov branch coverage, xdist parallelism, and async tests. Covers pyproject configuration with strict markers, fixture scope decisions, and debugging slow or order-dependent tests. Use when writing or refactoring Python tests, when fixtures get tangled, when tests are slow or pass only in a certain order, or when setting up pytest config and coverage in CI.

Try asking

  • Our conftest.py is 600 lines and tests are slow. Restructure the fixtures
  • Write pytest tests for the invoice module with parametrized edge cases
  • Tests pass alone but fail when run with the whole suite. Help me find why

Use it when

  • Writing or refactoring Python tests with pytest
  • Fixtures are tangled, slow, or leak state between tests
  • Setting up pyproject pytest config, markers, and coverage
  • Running a suite in parallel with pytest-xdist

Skip it when

How to check the result

The suite passes twice with random ordering and xdist enabled, --durations shows no unmarked slow tests, and the branch coverage gate passes.

The common mistake it prevents

Monkeypatching the function where it is defined instead of where the code under test looks it up, so the real implementation still runs.

~/.claude/skills/pytest-suites/SKILL.md

--- name: pytest-suites description: Structures and writes pytest suites with layered conftest fixtures, factory fixtures, parametrize with ids, registered markers,… license: MIT ---

A healthy pytest suite has small fixtures with the narrowest scope that works, explicit data per test, strict config that fails on typos, and runs green in random order and in parallel. Fixtures that do hidden work for every test are the usual cause of slow, confusing suites.

Ground rules

  • Configure strictly: unknown markers, config keys, and passing xfails are errors.
  • Fixture scope defaults to function. Widen to module/session only for expensive, immutable resources (a DB engine, a container), never for mutable data.
  • Use yield fixtures for teardown; code after yield runs even when the test fails.
  • autouse=True only for true invariants (block network, reset a global). Everything else is requested explicitly so readers see dependencies.
  • Use tmp_path, monkeypatch, capsys, caplog instead of hand-rolled equivalents.
  • Never let a unit test hit the network. Block it and stub at the HTTP client boundary.

Workflow

- [ ] 1. Read pyproject/pytest.ini, conftest.py files, and plugins in use
- [ ] 2. Tighten config (strict markers/config, testpaths, import mode)
- [ ] 3. Design fixtures: scope, location, factories
- [ ] 4. Write tests with parametrize and precise assertions
- [ ] 5. Run focused, then full suite randomized and parallel
- [ ] 6. Add branch coverage gate

1–2. Configuration baseline

# pyproject.toml
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-markers", "--strict-config", "--import-mode=importlib"]
xfail_strict = true
filterwarnings = ["error", "ignore::DeprecationWarning:some_third_party.*"]
markers = [
  "slow: takes more than 1s; excluded from the default PR run",
  "integration: needs docker services",
]
pythonpath = ["src"]          # only if not installing the package in editable mode
  • --import-mode=importlib avoids rootdir/sys.path surprises and duplicate test basenames clashing; with it, test helpers should live in importable packages or conftest fixtures, not be imported test-to-test.
  • filterwarnings = ["error"] surfaces deprecations early; add targeted ignores for third-party noise.
  • Check which plugins are active before relying on one: pytest -VV prints the registered plugins with versions.

3. Fixture design

Place fixtures at the lowest conftest.py that covers all users: tests/conftest.py for global ones, tests/api/conftest.py for API-only ones.

# tests/conftest.py
import pytest

@pytest.fixture(scope="session")
def engine():
    eng = create_engine(os.environ["TEST_DATABASE_URL"])
    yield eng
    eng.dispose()

@pytest.fixture
def db(engine):
    """Each test runs inside a transaction that is rolled back."""
    with engine.connect() as conn:
        tx = conn.begin()
        yield Session(bind=conn)
        tx.rollback()

@pytest.fixture
def make_user(db):
    """Factory fixture: call it as many times as the test needs."""
    created = []
    def _make(**overrides):
        user = User(email=f"u{len(created)}@example.test", **overrides)
        db.add(user); db.flush()
        created.append(user)
        return user
    return _make

@pytest.fixture(autouse=True)
def _no_network(monkeypatch):
    import socket
    def guard(*a, **k): raise RuntimeError("network access in unit test")
    monkeypatch.setattr(socket, "create_connection", guard)

Scope decision:

ResourceScope
Plain data objects, mocksfunction
DB connection with per-test rollbackengine session, transaction function
Docker/Testcontainers servicesession
Loaded large read-only filemodule or session
Anything a test mutatesfunction, always

Show fixture setup order when debugging: pytest --setup-show -k name. List available fixtures: pytest --fixtures tests/api.

4. Tests

import pytest
from decimal import Decimal

@pytest.mark.parametrize(
    ("amount", "currency", "expected"),
    [
        pytest.param(Decimal("10.00"), "USD", "$10.00", id="usd"),
        pytest.param(Decimal("0"), "EUR", "€0.00", id="zero"),
        pytest.param(Decimal("-5"), "USD", None, id="negative-rejected",
                     marks=pytest.mark.xfail(raises=ValueError, strict=True)),
    ],
)
def test_format_money(amount, currency, expected):
    assert format_money(amount, currency) == expected

def test_refund_over_balance_raises(make_user):
    user = make_user(balance=Decimal("5"))
    with pytest.raises(InsufficientFunds, match="balance 5"):
        refund(user, Decimal("6"))

def test_export_writes_csv(tmp_path):
    out = tmp_path / "report.csv"
    export_report(out)
    assert out.read_text().splitlines()[0] == "id,total"

def test_reads_api_key_from_env(monkeypatch):
    monkeypatch.setenv("API_KEY", "k-test")
    assert load_settings().api_key == "k-test"

def test_logs_retry(caplog):
    with caplog.at_level("WARNING", logger="app.http"):
        call_with_retry(flaky_once)
    assert "retrying" in caplog.text
  • Use pytest.approx for floats; Decimal for money.
  • For expected exceptions inside parametrize, prefer separate test functions over xfail when the error path is a primary behavior.
  • indirect=True passes params into a fixture (request.param) when the parameter needs setup.
  • Async: with pytest-asyncio, set asyncio_mode = "auto" in config or mark tests with @pytest.mark.asyncio; with anyio, use @pytest.mark.anyio. Do not mix both plugins on the same tests.
  • Mocking: prefer monkeypatch.setattr on the name where it is looked up (app.billing.send_email, not app.email.send_email). pytest-mock’s mocker fixture is fine when spies or call assertions are needed.
  • Time: time-machine or freezegun to pin now(); never sleep.

5. Run

pytest -q                                  # whole suite
pytest tests/test_billing.py::test_refund_over_balance_raises -vv
pytest -k "refund and not slow" -x         # name filter, stop at first failure
pytest -m "not slow and not integration"   # PR default
pytest --lf                                # rerun last failures
pytest --durations=15                      # slowest tests and fixtures
pytest -n auto                             # pytest-xdist parallel
pytest --randomly-seed=1234               # pytest-randomly: replay a printed seed
pytest -p no:randomly                      # disable shuffling when bisecting
pytest --pdb                               # drop into debugger on failure

Order-dependent failures: pytest-randomly shuffles on every run once installed and prints the seed in the header; replay it with --randomly-seed=<seed>. Then run the victim test alone, and with halves of the preceding tests, to find the one that pollutes state.

With xdist, session-scoped fixtures run once per worker, not once total. Use worker_id (provided by xdist) to give each worker its own DB name or port.

6. Coverage

pytest --cov=src/myapp --cov-branch --cov-report=term-missing --cov-report=xml --cov-fail-under=85

Configure omissions in [tool.coverage.run] (omit = ["*/migrations/*"]) rather than on the command line. Mark unreachable code # pragma: no cover with a reason.

Gotchas

  • monkeypatch.setattr("module.func", ...) patches the module attribute; if the code under test did from module import func, patch the importing module’s name.
  • A session fixture that returns a mutable dict shared by all tests is a hidden global.
  • Parametrize ids default to reprs that change with data; set id= for stable -k filters and readable CI output.
  • conftest.py fixtures override same-named fixtures from higher directories. Name collisions silently swap behavior.
  • caplog only sees records that propagate; loggers with propagate = False need a handler.
  • Passing xfails hide fixed bugs unless xfail_strict = true.

Verification

  1. Full suite green with pytest-randomly active and -n auto, twice in a row.
  2. pytest --durations=15: no unit test over ~1 s without a slow marker.
  3. Coverage gate passes; uncovered lines reviewed, not just excluded.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc