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.
- name
pytest-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
- You want generated inputs and shrinking with Hypothesis: use property-based-testing
- You need model factories and seed data design: use test-data-factories
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.
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 tomodule/sessiononly for expensive, immutable resources (a DB engine, a container), never for mutable data. - Use
yieldfixtures for teardown; code afteryieldruns even when the test fails. autouse=Trueonly for true invariants (block network, reset a global). Everything else is requested explicitly so readers see dependencies.- Use
tmp_path,monkeypatch,capsys,caploginstead 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=importlibavoidsrootdir/sys.pathsurprises 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 -VVprints 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:
| Resource | Scope |
|---|---|
| Plain data objects, mocks | function |
| DB connection with per-test rollback | engine session, transaction function |
| Docker/Testcontainers service | session |
| Loaded large read-only file | module or session |
| Anything a test mutates | function, 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.approxfor floats;Decimalfor money. - For expected exceptions inside parametrize, prefer separate test functions over xfail when the error path is a primary behavior.
indirect=Truepasses params into a fixture (request.param) when the parameter needs setup.- Async: with
pytest-asyncio, setasyncio_mode = "auto"in config or mark tests with@pytest.mark.asyncio; withanyio, use@pytest.mark.anyio. Do not mix both plugins on the same tests. - Mocking: prefer
monkeypatch.setattron the name where it is looked up (app.billing.send_email, notapp.email.send_email).pytest-mock’smockerfixture is fine when spies or call assertions are needed. - Time:
time-machineorfreezegunto pinnow(); 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 didfrom 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-kfilters and readable CI output. conftest.pyfixtures override same-named fixtures from higher directories. Name collisions silently swap behavior.caplogonly sees records that propagate; loggers withpropagate = Falseneed a handler.- Passing xfails hide fixed bugs unless
xfail_strict = true.
Verification
- Full suite green with pytest-randomly active and
-n auto, twice in a row. pytest --durations=15: no unit test over ~1 s without aslowmarker.- 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.