testing-playbook
Skill4 starsChanged 3 months agoApache-2.0View on GitHub
Testing strategies and patterns for Python/pytest projects. Reference when writing tests, reviewing test coverage, or designing test strategies.
In this repository25 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- adr-writingSkill
- authoring-goal-contractsSkill
- collaborating-asyncSkill
- committing-changesSkill
- cross-agent-dispatchSkill
- documenting-decisionsSkill
- feature-status-registrySkill
- grill-meSkill
- grill-yourselfSkill
- handling-micro-fixesSkill
- multi-instance-dispatchSkill
- notifying-the-developerSkill
- orchestrating-goal-loopsSkill
- orchestrating-lean-dispatchSkill
- performance-playbookSkill
- python-project-patternsSkill
- recovering-from-failuresSkill
- running-build-checkpointsSkill
- searching-prior-artSkill
- security-checklistSkill
- selecting-review-gatesSkill
- severity-calibrationSkill
- syncing-framework-docsSkill
- wrapping-up-sessionsSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 4 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 46
Test individual functions in isolation
- Distinctive: line 51
Test API endpoints end-to-end
- Distinctive: line 53
Verify request → processing → response → database state
- Very specific: line 107
Greppable failure lines: every failing check emits exactly one ERROR <check>: <reason> line on stderr (match with ^ERROR \w+: ).
- Distinctive: line 172
Keep the helper tests. They cover the combinatorics one end-to-end run cannot.
- Very specific: line 174
Add at least one CLI-level test per guaranteed outcome, driving real argv through main() in a tmp_path sandbox (repoint the module's path …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Testing Playbook
pytest Patterns
Test Client for FastAPI
import pytest
from httpx import ASGITransport, AsyncClient
from src.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield clientFixtures for Test Data
@pytest.fixture
async def sample_todo(client):
response = await client.post("/todos", json={"title": "Test Todo"})
return response.json()Parametrize for Input Variations
@pytest.mark.parametrize("title,expected_status", [
("Valid Title", 201),
("", 422),
("x" * 201, 422),
])
async def test_create_todo_validation(client, title, expected_status):
response = await client.post("/todos", json={"title": title})
assert response.status_code == expected_statusTest Categories
Unit Tests
- Test individual functions in isolation
- Mock external dependencies
- Fast execution, high specificity
Integration Tests
- Test API endpoints end-to-end
- Use test database
- Verify request → processing → response → database state
Edge Case Tests
Always test:
- Empty inputs (empty string, empty list, None)
- Boundary values (0, -1, MAX_INT, very long strings)
- Not-found scenarios (invalid IDs, deleted resources)
- Duplicate operations (create same thing twice)
- Concurrent access (if applicable)
Test Naming Convention
test_<action>_<scenario>_<expected_result>Examples:
test_create_todo_with_valid_data_returns_201test_create_todo_with_empty_title_returns_422test_get_todo_with_invalid_id_returns_404
Assertion Patterns
Verify Response Shape
data = response.json()
assert "id" in data
assert data["title"] == "Test Todo"
assert data["completed"] is FalseVerify Database State
# After creating, verify it's retrievable
get_response = await client.get(f"/todos/{data['id']}")
assert get_response.status_code == 200Verify Side Effects
# After deletion, verify it's gone
await client.delete(f"/todos/{todo_id}")
get_response = await client.get(f"/todos/{todo_id}")
assert get_response.status_code == 404Running Tests
pytest tests/ -v— verbose outputpytest tests/ --cov=src— with coveragepytest tests/ -k "test_create"— filter by namepytest tests/ -x— stop on first failure
Quality-Gate Conventions (SPEC-20260716-233400, R3.1-R3.4)
For derived projects consuming scripts/quality_gate.py:
- Greppable failure lines: every failing check emits exactly one
ERROR <check>: <reason>line on stderr (match with^ERROR \w+:); warnings follow the same shape (WARN <check>: <reason>). Parse these, not the colored human summary. - Gate log is additive-only:
metrics/quality_gate_log.jsonlrecords gain fields (profile,baseline_debt_count,rebaseline,fast) but existing fields never change or disappear (schema-pinned by test). Check statuses:pass | fail | skipped | disabled—disabled= turned off by the gate profile (by-design; does not demoteoverall). --fastis for mid-build iteration only: a deterministic ~25% sample of test files (stable across runs; changes only when the file list changes). It logsfast: true, skips coverage, and is NEVER commit-gate evidence — the pre-commit hook must never pass it.- Failing tool output is truncated to the last 20 lines with a rerun pointer — rerun the underlying command for full output.
- Debt baseline (
config/gate_baseline.json, python-fastapi profile): fingerprints are(check, posix-relative path, rule code)— no line numbers. Baselined findings WARN; new fingerprints fail RED by set membership (a 1-for-1 swap fails). The baseline only shrinks automatically (--fix/--shrink-baseline);--rebaselineproposes — it prints theconfig/gate_baseline.jsona developer would have to commit and writes nothing. Both gate config files are review-gated viacheck_review_existence.- Corrected 2026-08-08 — this bullet used to say "
--rebaselineis a developer-consent action the agent must never run autonomously". That was a rule addressed to the actor it was meant to bind, and the code did not back it:main()called_write_baseline(current_fps)the moment the flag appeared. Do not restate the old wording; it is the exact pattern the next section is about. Note the file has never existed in this template or in any of the four derived projects (measured 2026-08-08:ls config/gate_baseline.json→ absent in all five), so no recorded debt has ever been compared against. - Second correction, same day: "no baseline file" ≠ "the comparison never
runs". Three write-ups said it did not; measured,
--rebaselineon a repo with no baseline file routes format and lint through_check_against_baselineagainst an empty set, reporting every finding as "N NEW finding(s) not in baseline" when no baseline exists. Verdict is the same RED, wording is a wart. Pinned at the CLI byTestWhichPathRunsWhenNoBaselineFileExists. The transferable habit: when you write "X never runs", spy on X in a CLI test rather than reasoning about the guard condition — the reasoning had covered the ordinary run and silently generalised.
- Corrected 2026-08-08 — this bullet used to say "
A guarantee about main() needs a test of main()
The single most expensive testing mistake in this repo's record, worth stating as a rule because it has now happened twice on the same mechanism:
A property asserted in a docstring and covered only by a unit test of a pure helper is not covered. If the claim names the CLI, the test must drive the CLI.
The case. scripts/quality_gate.py claims "no invocation of this script — no
flag, no combination of flags — can add a fingerprint to
config/gate_baseline.json". The fix that made it true also deleted the only
test that drove --rebaseline end-to-end through main() and replaced it with
unit tests on the pure decision function _baseline_write_plan. The helper
tests were good tests — they pin all 8 flag combinations — but the guarantee is
about main(), and after the swap grep -n '"--rebaseline"' tests/*.py
returned nothing. A reviewer restored the literal pre-fix defect
(_write_baseline(current_fps) back in main()) and measured the suite:
163 passed, exit 0. The guard had moved out of a test and into prose.
What to do instead — both, not either:
- Keep the helper tests. They cover the combinatorics one end-to-end run cannot.
- Add at least one CLI-level test per guaranteed outcome, driving real
argvthroughmain()in atmp_pathsandbox (repoint the module's path globals withmonkeypatch.setattrso a reintroduced write lands in tmp, never in the repo). - Prove the CLI test can fail: apply the defect in a scratch copy of the tree and watch it go red. Never mutate the repo to test a mutation.
- Give every such test a non-vacuity assertion — something that proves the branch actually executed (a printed line, a log field). "No file was written" passes trivially if the code path never ran.
Live example to copy, in tests/test_quality_gate.py:
TestRebaselineWritesNothingThroughMain (3 tests / 6 cases, CLI-level) sitting
alongside TestBaselineCannotGrow (the helper tests) — neither replaces the
other. Measured 2026-08-08 against a 173-passing scratch copy: with the
defect restored in main(), 7 tests fail, 166 pass; deselect the CLI class
and 1 still fails (166 pass), because
TestWhichPathRunsWhenNoBaselineFileExists independently asserts no baseline
file is created.
That last number replaced an earlier "165 pass, exit 0 — nothing else notices", and the replacement is worth more than the correction: the count of tests that catch a mutation is itself a measurement with a shelf life. Re-run the mutation before quoting the figure; do not inherit it.
Corollary: name the guarantee precisely, then test that
While writing the CLI tests above, a second mutation exposed a subtler version
of the same failure. The mechanism makes two promises — "cannot GROW the
baseline" and "writes NOTHING" — and deleting the --rebaseline veto from the
helper breaks only the second: the write falls through to a subset of the
existing baseline, so the growth invariant still holds. The whole suite stayed
green, because the existing helper test only ever passed a current containing
new debt, where the result is None for an unrelated reason.
A test can be about the right thing and still pass for the wrong reason. Two habits catch it:
- Vary the input so the guard under test is the only thing producing the result. Parametrize over the states where the other early-returns do not fire.
- Assert the strongest promise you actually make. "The file did not grow" is weaker than "the file is byte-identical". If the docstring says writes nothing, assert bytes.
A test that stays green when you break the thing it guards is not evidence.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

