pytest console

testharness-fun // screen reference
What the specimen is, where every seam sits, and which test file drives which part of it. fwvault is a firmware artifact intake service that exists to be tested: it parses a blob, asks a signing oracle about the digest, applies policy, and stores what survives.
system map
testharness-fun/
├─ src/fwvault/          the specimen. no runtime dependencies.
│  ├─ parse.py     303  UF2 + ELF walkers, and serialize_uf2
│  ├─ policy.py    118  9 rules, precedence-ordered
│  ├─ signing.py   158  outbound client. transport is a PARAM
│  ├─ store.py     116  content-addressed, atomic writes
│  ├─ app.py       220  raw ASGI3. no framework.
│  ├─ cli.py       168  argv in, exit code out
│  ├─ errors.py     55  the taxonomy the design hangs on
│  ├─ clock.py      47  time as a dependency
│  └─ testing.py   158  builders + doubles, SHIPPED
│
├─ tests/                one file per technique family
│  ├─ conftest.py            isolation, specimens, plugin hooks
│  ├─ mechanics   parse, fixtures, parametrize, markers,
│  │              assertions, time, cli, store
│  ├─ doubles     doubles, monkeypatch, transport,
│  │              asgi_raw, asgi_clients
│  ├─ evidence    hostile, property, metamorphic,
│  │              differential, policy, oracle_independence
│  └─ meta        suite_hygiene, contract, lean_install,
│                 mutation, ai_authored, cards
│
├─ terms.py              67 terms. every glossary generates from it
├─ frag.html             deep dive source. the page is built.
├─ card_content*.py       the two printed cards
└─ build_*.py gen_*.py    generators. see the COMMANDS tab.
the request, end to end
POST /artifact  ← 1024 bytes of UF2
     │
     ├─ parse(blob)            sniff magic, walk 512-byte
     │                        blocks, fold to an Image
     │
     ├─ digest_of(blob)        sha256, computed by US,
     │                        never taken from the client
     │
     ├─ client.verify(digest)  GET oracle/keys/<digest>
     │                        → Verdict, or VaultUnavailable
     │
     ├─ policy.enforce(...)    9 rules, precedence order,
     │                        raises the first hit
     │
     └─ store.put(blob, m)     → 201 {"digest": ...}
Four collaborators: a parser, an HTTP client, a rule engine, a filesystem. The minimum shape that needs all four, which is why it was picked.
seam inventory
seamenabling pointcontractwhat it buys
transportSigningClient(transport=).request(method,url,body,headers)retries, URLs, calls that did not happen
clockSigningClient(clock=).now(), .sleep(s)assert clock.slept == [0.5,1.0,2.0] in zero time
cacheSigningClient(cache=)a dictstale-verdict behaviour, prepopulated
storecreate_app(store=).put/.get/.has/__len__assert len(vault) == 0 after a refusal
clientcreate_app(client=).verify(digest)the 503 path, via a 3-line DeadClient
policycreate_app(policy=)frozen dataclass, 9 fieldsevery rule via replace(policy, ...)
body limitcreate_app(max_body=)int413 without generating 8 MB
argv + streamscli.main(argv,stdout,stderr)filesthe whole CLI in microseconds
vault rootStore(root)a pathtmp_path, so tests never collide
No module-level singletons, no settings object, no global registry. That is what lets two tests run two differently-configured apps in one process, and why the app fixture is one line.
seams that are not parameters
STRUCTURAL   split a function so both halves are reachable
   walk_uf2()   generator, yields Block records
   parse_uf2()  folds that stream into an Image
   assert on block 10 of 900 without building a
   manifest; test the fold with hand-built records
   and no bytes at all.

INVERSE      serialize_uf2() is walk_uf2 backwards
   buys the round-trip relation, which needs no
   oracle at all. see the EVIDENCE tab.

IMPORT       sys.modules[name] = None
   makes `import name` raise. tests the
   dependency-absent path on a machine that has it.

ENVIRONMENT  FWVAULT_WALKER_RAISE
   a walker bug is a warning in production and a
   traceback in the suite. both states tested.
which file drives which surface
test filesurfaceswhat it is really about
test_parse.pyparserthe assertion vocabulary, parametrize and ids
test_hostile.pyparsertruncation sweeps, bit flips, seeded fuzz
test_property.pyparser, policy, storehypothesis: round trip, invariant, never-crash
test_metamorphic.pyparserrelations that need no oracle
test_differential.pyparsertwo independent implementations over one corpus
test_policy.pypolicyproving a refusal, for the right reason
test_store.pystoretmp_path, atomicity, path traversal by regeneration
test_transport.pytransportretry schedules, cache staleness, caplog
test_asgi_raw.pyHTTPa 20-line ASGI client, and what only it can do
test_asgi_clients.pyHTTP, transportthe same app through httpx and TestClient
test_cli.pyCLIin-process with streams, plus four real processes
test_doubles.pytransport, policythe five doubles, each with a working example
test_monkeypatch.pymostevery method, four traps, and when not to patch
test_fixtures.pystorescope, teardown order, factories, the anti-pattern
test_parametrize.pypolicy, storeindirect, generate_tests, stacking limits
test_markers.pyparserskip vs xfail, strict, pytest.param, flaky policy
test_time.pytransportinject a clock, never sleep in a test
test_contract.pyallthe published shape: names, codes, routes, schema
test_lean_install.pyallthe dependency boundary, statically and behaviourally
test_suite_hygiene.pythe suitedoes every test still assert anything
test_mutation.pythe suite8 mutants, each paired with its catcher
test_ai_authored.pythe suitegenerated-shaped suite vs asserting, measured
test_oracle_independence.pythe suitewhy the author cannot grade its own homework
test_cards.pythe docsgenerated files, cross-refs, page fit
The tool itself. Collection, configuration, fixtures, cases, assertions. Everything here is pytest behaviour rather than an opinion about testing, so it transfers to any project unchanged.
what gets collected
filestest_*.py or *_test.py
classesTest*, and no __init__
functionstest*  not  test_*
# a helper named tests_in() IS COLLECTED as a test.
# if it is a generator, the whole FILE dies:
'yield' keyword is allowed in fixtures,
 but not in tests
This repo walked into it twice: once in test_suite_hygiene.py, once in test_oracle_independence.py with a helper called tests_from_belief. Name helpers find_* or suite_*.
conftest.py, three jobs
visibility follows the DIRECTORY tree.
nearest wins. no import needed, anywhere.
repo/conftest.py       everything below
  tests/conftest.py    tests/ and below
    tests/test_x.py    a fixture here SHADOWS both
1. isolate the process (autouse)
@pytest.fixture(autouse=True)
def isolate_environment(monkeypatch, tmp_path_factory):
    home = tmp_path_factory.mktemp("fwvault_home")
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.setenv("USERPROFILE", str(home))  # windows
    monkeypatch.setenv("FWVAULT_WALKER_RAISE", "1")

2. supply specimens and collaborators
3. extend pytest itself
def pytest_addoption(parser):
    parser.addoption("--runnet", action="store_true")

def pytest_collection_modifyitems(config, items):
    # where a marker becomes behaviour
Deleting an env var is not isolation. With it unset the code falls back to expanduser("~") and the suite writes into a real home directory. Set a fake one, and set both HOME and USERPROFILE.
pyproject block, steal it
[tool.pytest.ini_options]
testpaths    = ["tests"]
pythonpath   = ["src"]     # runs without installing
addopts      = "-rs --strict-markers"
xfail_strict = true
markers = [
  "slow: takes real wall-clock time",
  "net: touches a real network",
]
filterwarnings = [
  # YOUR deprecations are errors. SCOPE it, or
  # someone else's release breaks your build.
  "error::DeprecationWarning:yourpkg.*",
]
-rsprint every skip reason
--strict-markersa typo'd marker is an error
xfail_strictan xfail that passes is a failure
pythonpatha fresh clone runs immediately
fixture lifecycle
build order is DEPENDENCY order.
teardown is its exact reverse.

 outer:setup  inner:setup  TEST
              inner:teardown  outer:teardown

code after `yield` runs even if the test FAILS.
it does NOT run if setup before the yield raised.
→ put what CAN fail after what MUST be cleaned up.
scopebuiltsafe when
functionper testalways. the default
classper classa class groups a scenario
moduleper fileexpensive AND immutable
packageper directoryrare
sessionper runnever mutable
A session-scoped mutable fixture is a global with extra steps. The test that corrupts it fails a different test, three files later, in one ordering only.
fixture shapes
FACTORY: the test gets the FUNCTION
@pytest.fixture
def make_uf2(): return build_uf2
  beats forty fixtures named after their defects

PARAMETRIZED: requester runs once per case
@pytest.fixture(params=["uf2","elf"], ids=[...])
def any_artifact(request): ...

NAMED: function name is not the fixture name
@pytest.fixture(name="digest")
def _make_digest(uf2): ...

OVERRIDE: a module-level fixture shadows conftest
  silently. nothing announces it.

CONDITIONAL CLEANUP: addfinalizer beats yield
    request.addfinalizer(t.unlink)  # only now that
                                    # it exists
Assertions belong in tests. Fixtures build things. An assert in a fixture reports an ERROR against every test that uses it, with a traceback pointing at conftest.
parametrize
@pytest.mark.parametrize("fam,want",
    [(RP2040,"RP2040"), (NRF,"NRF52840")],
    ids=["rp2040","nrf52840"])       # explicit
    ids=lambda n: "block%d" % n      # callable
    ids=repr                         # byte specimens

STACKED = cartesian product. 2x2 = 4 tests.
multiplies fast: a 4th axis of 5 is 40 tests.

pytest.param = per-case id AND marks
pytest.param(x, id="unknown",
  marks=pytest.mark.xfail(strict=True, reason="..."))

indirect: the param goes to a FIXTURE first,
so each case gets setup AND teardown
@parametrize("vault",[0,1,5], indirect=True)

pytest_generate_tests(metafunc): build the case
list at collection, from the source of truth
test_family[3-2] tells you nothing. test_family[nrf52840] tells you everything. You read these in CI, in a bisect, and in a message from someone without the repo open.
skip, xfail, and the trap
skipdoes not apply here
skipif(cond)same, decided at collection
xfailthis is broken and we know
xfail(strict=True)and tell me when it stops
xfail(raises=E)fails FOR THIS REASON
@pytest.mark.xfail(reason="bug FW-118")
def test_the_bug():
    assert True        # someone fixed it
# → XPASS. exit 0. GREEN. the marker sits there
# for two years and the test asserts nothing.
@pytest.mark.xfail(strict=True, reason="FW-118")
# → FAILED: "delete this marker."
Without raises=, an xfail absorbs every failure, including the ImportError you added this morning.
assertions and capture
with pytest.raises(ParseError, match=r"bad magic"):
    parse(blob)
  match is re.SEARCH, not fullmatch.
  ESCAPE YOUR METACHARACTERS. "offset (0x1fc)"
  is a group. a match that matches NOTHING still
  passes if the exception type is right.

with pytest.raises(TruncatedError) as ei: ...
assert ei.value.needed == 32   # the OBJECT, not
                               # just the class
capsysstdout/stderr at the Python level
capfdat the fd level. C ext, subprocess
caploglog records. set the level or capture nothing
tmp_pathfresh dir per test, last 3 runs kept
tmp_path_factorythe session-scoped variant
Assert on caplog.records, not caplog.text. Records carry .levelname, .name, .getMessage(). Text is whatever the formatter felt like, and nobody thinks of a format string as an interface.
the flaky test

A flaky test is a bug report, against the test or the code. Never noise. Something is genuinely non-deterministic and you have found it.

causefix
unseeded randomfix the seed
real clock / sleepinject a clock
shared statenarrow the scope
fixed path + -ntmp_path
real networka marker plus a fake
dict / set ordersort, or compare sets
Quarantine, do not rerun. Move it behind a deselected marker with a ticket. --reruns 3 on your own code turns a real bug into a slower green run, permanently.
What you put where the real collaborator goes, and how you get it in there. The words are load-bearing: which kind you reach for predicts how your test fails, and whether it can fail at all.
the five doubles
DUMMY  fills a signature, never used
STUB   canned answers, no state
FAKE   a real, working, SIMPLIFIED impl  ← this one
SPY    a stub that records its calls
MOCK   a spy that asserts on ITSELF      ← almost never
  • Fake is the only kind that can be wrong in a way a test notices. A stub returning 200 forever cannot fail to model a 404, because it was never modelling anything.
  • Spy for the negative assertion. "the cache was used" is invisible in a return value and obvious in a call log.
  • Mock fails inside the double, so the message describes a call that did not happen, not a behaviour that is wrong.
loose = Mock()          # no spec=
loose.method_that_does_not_exist()   # passes.
spec= is not optional.
the transport seam
SigningClient
     │  .verify(digest)
     ▼
transport.request(method, url, body, headers)→ Response
     ├── UrllibTransport     real. stdlib. net-marked only
     ├── RecordingTransport  routes + a call log  (spy)
     ├── ScriptedTransport   one answer per call  (sequence)
     └── FakeOracle          a working oracle in RAM (fake)

The narrower the seam, the cheaper the fake.
`Response` is a 3-field dataclass, deliberately
NOT an httpx.Response.
Nothing is patched. The test passes a different object. Patching enters only where you do not own the seam.
shipped doubles

A library whose only seam is a protocol owes its users a working fake for it, the way httpx ships MockTransport. Otherwise every downstream user writes their own and each gets a detail wrong.

# fwvault/testing.py, part of the package
build_uf2(blocks=3, bad_end_magic=1)   # named defect
build_elf(machine=0x28, bit64=True)
signed_body(signer="ci-builder")

RecordingTransport(routes={...})       # spy
ScriptedTransport([err, err, Response(200, ...)])
flaky(2, Response(200, body))          # the idiom
# running off the end of a script is a HARD FAILURE
AssertionError: ScriptedTransport exhausted:
call 4 was not scripted (GET https://...)
# a double that keeps answering makes "retried 3
# times" and "retried 300 times" the same test.
monkeypatch, all of it
setattr(o,n,v)raises if n is absent. the guardrail
delattr(o,n)
setitem(d,k,v)sys.modules, os.environ, any table
delitem(d,k)raising=False for cleanup
setenv / delenvstrings only
syspath_prepend+ invalidates importlib caches
chdir(p)and puts it back
context()undo EARLY, mid-test
patch where the name is LOOKED UP:
from . import families;  families.FAMILIES[x]
  read at CALL time  → patchable
from .families import FAMILIES;  FAMILIES[x]
  bound at IMPORT time into a SECOND namespace
  → patching does NOTHING, and the test PASSES
monkeypatch is function-scoped. A session fixture that requests it gets ScopeMismatch. Build the context manager yourself: with pytest.MonkeyPatch.context() as m:
four patching traps
1 raising=False disables your only signal
  a patch aimed at a typo'd name is a patch
  aimed at nothing, and it passes.

2 patching a constant does not move an
  already-bound default
  @dataclass  x: int = CONST  ← evaluated ONCE at
  class creation. same for def f(x=CONST) and
  `from mod import CONST`.

3 the string form walks getattr
  "pkg.mod.CONST" imports pkg then getattrs along.
  if pkg re-exported a FUNCTION over its own
  submodule name, the walk dies there.
  `import pkg.mod as m` does NOT save you.
  importlib.import_module() reads sys.modules.

4 sys.modules[n] = None makes `import n` raise
  delitem FIRST, or the None assignment is what
  takes effect and you pass for the wrong reason.
inject or patch
INJECT what you DO own
       your transport, clock, store, policy,
       streams, argv, the vault root
PATCH  what you do NOT own
       urllib, datetime, os.replace, a vendor lib
SPLIT  when there is nothing to pass
       a generator that WALKS plus a function that
       FOLDS is a seam too, and often a better one
Patching a seam you own passes even after the code stops using that seam. Injecting notices at once: the fake stops being asked anything. If a test has more patching than assertion, the code has no seam.
Each surface of the service takes a different kind of test, for a reason that comes from the surface rather than from taste. What is hard about each one is the useful column.
parser · bytes in
walk_uf2(blob)     generator → Block records
parse_uf2(blob)    folds → Image
serialize_uf2(bs)  the inverse
hard becausethe input space is adversarial by definition
the state machine is deep, so a wrong branch still returns something plausible
a length field is a trust boundary, not a style issue
easy becausea real oracle usually exists. almost no other code has one

Driven by: test_parse, test_hostile, test_property, test_metamorphic, test_differential.

Raise on malformed, warn on odd-but-survivable. Getting that line wrong in either direction is a bug: raise too eagerly and half the real world is unparseable, warn too eagerly and a corrupt image ships.
policy · decisions and writes
evaluate(image, verdict, policy, flags)
  → EVERY rejection, in precedence order
enforce(...)
  → raises the FIRST one

Returning every hit rather than the first is what makes the rules testable without combinatorial fixtures. One hostile artifact that trips four rules gives four codes in one assertion, so a rule that silently stopped firing cannot hide behind one that fires earlier.

PRECEDENCE = (MALFORMED, OVERSIZE, EMPTY_PAYLOAD,
  UNKNOWN_FAMILY, DENIED_MACHINE, NOT_MAIN_FLASH,
  REVOKED_KEY, UNSIGNED, TOO_MANY_WARNINGS)
codes are a PUBLIC CONTRACT. clients branch on
`code`, never on `detail`, so codes are frozen in
test_contract.py while the wording stays free.
A meta-test asserts every code in PRECEDENCE is produced by some specimen. Add a rule and it fails until you add a case.
store · the filesystem
Store(root).put(blob, manifest) → (digest, created)
content-addressed. atomic. two-level fan-out.
  • Atomic: temp file in the SAME directory plus os.replace. A temp in the system temp dir and a cross-device move is a copy, and a copy is the torn write you were avoiding.
  • Idempotent: the same bytes twice is a no-op, not a rewrite.
  • Path safety by regeneration: the key is not sanitised, it is hashed from the bytes. A traversal string is just a miss.
# the traversal test everyone writes:
"../../etc/passwd"   "..%2f..%2fetc"   "....//"
this list can never be complete.
# so the design does not rely on it. the sweep
# documents the threat; regeneration IS the defence.
Every write goes to tmp_path. Never a fixed path: two workers under -n 4 will race for it.
HTTP · ASGI, three ways
async def app(scope, receive, send)
  scope    dict describing the connection
  receive  await it to pull events IN
  send     await it to push events OUT
that is the whole protocol.
raw harnesschunk boundaries, disconnects, malformed scopes, visible lifespan
ASGITransportsubstitutes the server. real header handling
MockTransportsubstitutes what you call. opposite end of the wire
TestClientwraps ANY asgi app, lifespan via with
If swapping the client changes what you assert, you were testing the client. The raw and httpx files here assert nearly identical things about the same app.
Answer lifespan or a test client hangs at fixture time, and it looks like the framework is broken rather than your app.
CLI · process boundary
def main(argv=None, stdout=None, stderr=None) -> int
  takes argv. returns an int. writes to streams it
  was HANDED. the highest-leverage decision in a
  CLI's design, and it costs nothing.
in-processmicroseconds, every branch. the coverage lives here
subprocessfour cases, but the ones in-process cannot see
  • importable from a clean interpreter
  • the exit code as the shell sees it
  • buffering, encoding, line endings
  • a stray print() in the import path corrupting your JSON
Exit codes are a contract. "your input is bad" and "our infrastructure is down" must be different numbers, or a red build pages the wrong team. Here: 3 rejected, 4 malformed, 5 unavailable.
transport · outbound network
what is worth asserting is not "it returned a
Verdict". it is WHEN, HOW OFTEN, and WHAT IT DID
NOT DO. none of those show in a return value.

assert clock.slept == [0.5, 1.0, 2.0]   schedule
assert len(transport.calls) == 1        no retry
assert verdict.stale is True            honesty
oracle saidmeans
200an answer
404an answer. NOT retried
4xxOUR credentials are wrong → unavailable
5xx / resetretry, then unavailable
An outage rendered as "unsigned" is a wall of confident refusals indistinguishable from real ones, found when someone asks why every build failed overnight.
containers and real dependencies

fwvault has no container in the loop, so nothing here is demonstrated by a running test. The pattern is the one this repo already uses for the single real HTTP call, and it generalises unchanged.

# the shape: a marker, deselected by default
@pytest.mark.net          # or .docker, .integration
def test_against_the_real_thing():
    ...

# conftest turns the marker into behaviour
def pytest_collection_modifyitems(config, items):
    if config.getoption("--runnet"): return
    skip = pytest.mark.skip(reason="needs a real network;
                                    pass --runnet")
    for item in items:
        if "net" in item.keywords:
            item.add_marker(skip)
The reason to name it rather than delete it: the fake cannot cover the class it is a fake OF. Keeping a deselected test with a printed skip reason keeps the gap visible instead of pretending it is covered.
In CI this becomes a separate job with the service running, on a schedule rather than per-commit. Same reasoning as the bare job: an environment that can disprove a claim the others cannot.
the suite itself

A surface most projects never test. These read the test tree as data.

  • every test asserts something
  • every skip explains itself
  • no fixed paths, no path outside the repo
  • nothing silently disabled: a bare return at the top leaves no skip line at all
  • no duplicate test names across files
  • no mutated module-level state
  • the published shape is pinned: names, codes, routes, schema version
  • the dependency boundary, statically and behaviourally
Two rules or it becomes folklore. (1) every check names the file and line it objects to. (2) every check is shown FIRING at least once, or a broken pattern passes everything forever.
What each technique actually proves, and what it cannot. Read the third column first. Most arguments about testing are two people using techniques from different rows and assuming the same guarantees.
evidence layers
layerprovescannot proveneeds you to knowwhere
examplethis input gives that outputanything about the inputs you did not writethe answertest_parse
contractthe published shape has not movedthat anything behind it workswhat you promisedtest_contract
guardrailit refuses, with this codethat the rule is the right rulethe policytest_policy
hostilenothing escapes the declared errorthat it does anything usefulthe error contracttest_hostile
propertya rule holds across generated inputsthat the rule is completea ruletest_property
metamorphictwo runs relate as they mustthat either run is correctnothingtest_metamorphic
differentialtwo implementations agreethat both are not wrong the same waynothing, if independenttest_differential
mutationthe suite notices a breakthat it notices the breaks nobody wroteplausible breakstest_mutation
metathe suite is still a suiteanything about the codewhat rot looks liketest_suite_hygiene
The fourth column is the ladder. Example needs the answer. Differential borrows it. Property implies it. Metamorphic needs none, which is why it is the layer a generated suite cannot fake.
coverage vs mutation, measured
                 lines covered   mutants killed
ai-authored           93            0 / 5
asserting             93            5 / 5
  identical coverage. a --cov gate cannot tell
  these two files apart.

Two suites over the same twelve specimens. One written in the never-crash shape, one with positive assertions. Coverage counted by a twenty-line sys.settrace tracer so the number owes nothing to a plugin.

line coveragedid this line execute
branch coveragedid BOTH sides of the if
mutation scoreif I BREAK it, does anything go red
100% line coverage is compatible with zero assertions. Only the third row is a question about your tests.
the never-crash trap
# every assertion in test_hostile.py passes
# against this:
def parse(b): raise ParseError("no")

# that is a truncation sweep, twelve bit flips
# and twenty seeds. all green. against a parser
# that does nothing.

The same trap reappears wherever assertions describe relationships instead of values. Every metamorphic relation in this repo is satisfied by a parser that ignores its input and returns a constant.

# the companion, in the SAME file so the pairing
# is visible:
def test_hostile_specimens_do_not_all_look_alike():
    assert parse(VALID).block_count == 3
Every never-crash assertion needs a positive companion. This is the single highest-value rule on this page.
metamorphic relations
ROUND TRIP: the strongest, when an inverse exists
assert serialize_uf2(walk_uf2(blob)) == blob

INVARIANCE: changing X must NOT change Y
scrambled = every payload byte randomised
assert parse(scrambled) == parse(blob)

EQUIVARIANCE: change X, predict the change in Y
assert after.payload_bytes ==
       before.payload_bytes + 200 * extra

IDEMPOTENCE: twice is once
assert serialize_uf2(walk_uf2(once)) == once
Stating a bound without naming it: raising payloadSize can never lower the total, and past some point it stops moving. That states the clamp without repeating 476, so the test cannot simply agree with the implementation.
differential, and its two rules
specimen ─┬─► walk_uf2()          ─┐
          │   fast, subtle        ├─► == ?
          └─► _reference_walk()   ─┘
              naive, obvious, INDEPENDENT
  • The reference must be independent. Shared helpers mean a shared bug cancels out and both agree. Asserted structurally here: the reference borrows no constant, import or type from the implementation.
  • Compare outcomes, not just outputs. Agreeing on the result is not enough if one raises and the other does not.
  • The corpus needs a guard. If every specimen raises, both agree everything is broken and the test is vacuous.
  • One test per specimen, named by specimen. Adding a corpus entry adds a test with no assertion to write. That is the economics.
equivalent mutants, the honest cost

A mutation score is never 100%, and the reason has a name: some edits cannot be detected because they change no behaviour.

# the first attempt at "PRECEDENCE gets alphabetised"
# SURVIVED, and it was not a hole:
real:         MALFORMED, OVERSIZE, EMPTY_PAYLOAD, ...
alphabetical: DENIED_MACHINE, EMPTY_PAYLOAD, ... OVERSIZE

# for the specimen chosen, OVERSIZE came first in
# BOTH orders. the mutation changed nothing
# observable.
OVERSIZE + EMPTY_PAYLOAD is the pair that
actually distinguishes them.
Telling equivalent mutants from real gaps is manual work that does not go away. Say so, or the practice becomes the thing it criticises.
One-liners, grouped by what you are trying to find out. Most of these answer a question without running a test, which is the fastest loop available and the one people skip.
environment
# nothing but pytest. this is the claim the repo makes.
pip install pytest
python -m pytest -q                # 547 passed

# plus hypothesis, httpx, starlette
pip install -e ".[test]"
python -m pytest -q                # 557 passed

# the deselected-by-default tests
python -m pytest --runnet

# no install at all: pythonpath is set in pyproject
git clone ... && cd testharness-fun && pytest
If a clone needs an install step to run its own tests, the first thing a new reader meets is a packaging problem. pythonpath = ["src"] costs one line.
answer it without running anything
# what WOULD run. the first thing to try when a
# test is not running.
pytest --collect-only -q
pytest --collect-only -q | wc -l

# every fixture visible here, and where it is from
pytest --fixtures
pytest --fixtures-per-test tests/test_policy.py

# which markers exist and what they mean
pytest --markers

# the fixture setup plan, without executing tests
pytest --setup-plan tests/test_asgi_raw.py

# what a marker expression selects
pytest --collect-only -q -m "net"
pytest --collect-only -q -k "metamorphic and not round"
--setup-plan is the one people never learn. It prints the fixture tree per test and runs nothing, which is how you find out that a session fixture is being rebuilt.
discovering seams
# every injectable collaborator in the package
grep -rn "def __init__\|def create_app\|def main" src/ \
  | grep "="

# the same, structurally
python - <<'PY'
import inspect, fwvault.signing as s, fwvault.app as a
print(inspect.signature(s.SigningClient.__init__))
print(inspect.signature(a.create_app))
PY

# what does this module import at MODULE level?
# (a lazy import inside a function is the contract)
python -c "import ast,sys;print([n.names[0].name
  for n in ast.parse(open(sys.argv[1]).read()).body
  if isinstance(n,ast.Import)])" src/fwvault/app.py

# who patches, and who injects?
grep -rln "monkeypatch" tests/
grep -rn "transport=" tests/ | head
The ratio of the last two lines is a design metric. Lots of patching against your own package means the seams are missing.
affected layers
# one surface at a time
pytest tests/test_parse.py tests/test_hostile.py \
       tests/test_metamorphic.py tests/test_differential.py

# by name expression, across files
pytest -k "parse or walk or serialize"
pytest -k "policy or reject"
pytest -k "asgi or transport"

# the meta tier only
pytest tests/test_suite_hygiene.py tests/test_contract.py \
       tests/test_lean_install.py tests/test_cards.py

# everything EXCEPT the slow generated ones
pytest -k "not property and not metamorphic"

# what changed, if you use git
pytest $(git diff --name-only HEAD~1 | grep ^tests/)
the debug loop
pytest --lf                 # only what failed last time
pytest --lf -x --tb=short    # the shortest useful loop
pytest --ff                 # failed first, then the rest

# one test, exactly
pytest tests/test_policy.py::test_oversize
pytest "tests/test_parse.py::test_family_naming[rp2040]"

# see prints and live logs
pytest -s
pytest --log-cli-level=DEBUG

# locals in the traceback, then a debugger
pytest -l
pytest --pdb
pytest --trace              # break at the START of the test

# why is this fixture what it is
pytest --setup-show tests/test_fixtures.py
If you take one flag away, take --lf. It changes the edit-run loop more than anything else here.
quality of the suite
# coverage as a MAP, not a score
pytest --cov=fwvault --cov-branch --cov-report=term-missing

# the branch list only, which is what you act on
pytest --cov=fwvault --cov-branch --cov-report=term-missing \
  | grep -E "^src.*[0-9]+%" | sort -t% -k1

# does the suite notice a break? (slow, occasional)
pip install mutmut && mutmut run --paths-to-mutate src/fwvault

# the cheap in-repo version, milliseconds
pytest tests/test_mutation.py tests/test_ai_authored.py -v

# order dependence, the cheapest detector you own
pytest -p xdist -n 4
pytest -p no:randomly            # baseline to compare

# slowest ten. run it monthly.
pytest --durations=10
regenerating the artifacts
# after editing terms.py
python gen_glossary.py     # → GLOSSARY.md, cards, frag

# after editing card_content*.py
python build_card.py       # the 3-sheet reference
python build_basics.py     # the 1-sheet beginner card
python build_console.py    # this page

# layout without writing anything
python build_card.py --check
python build_console.py --check

# the deep dive: edit frag.html, NEVER the built page
build.py frag.html testharness_deep-dive.html "<title>"

# the check that catches a stale artifact
pytest tests/test_cards.py
test_cards.py fails if a generated file is stale, if a cross-reference cites a section number that moved, or if a card column would overflow its page. A bad rebuild fails locally, not on the site.
CI
jobs:
  bare: pip install pytest   # and NOTHING else
        # the only environment that can DISPROVE a
        # no-dependencies claim.

  full: matrix [ubuntu, windows] x [3.11, 3.13]
        fail-fast: false   # one red cell must not
                            # hide the others

  cov:  --cov=fwvault --cov-branch --cov-report=term-missing
        # no --cov-fail-under. a threshold is a number
        # people game, and the cheapest way to game it
        # is a test with no assertions.
Windows in the matrix is not politeness. expanduser, path separators and os.replace semantics all differ, and every one has produced a green local run and a red CI run.
What changes when the thing writing the code also offers to write the tests. Everything below is measured or cited, and the citations were checked against the abstracts rather than taken from a summary.
the argument, in two moves
Su          lights-out codebases: code no human reads.
            volume makes review impossible, and the chess
            analogy says trying will look like a category
            error.

Venturini   the better version. you do not read your
            compiler's output, and NOT because compilers
            are trusted blindly. they have shipped
            catastrophic bugs. it is because an apparatus
            was built that makes reading unnecessary:
            behavioural tests, type systems, reproducible
            builds, fuzzers, sanitizers, formal methods.

            you trust the process, not the artifact.

his diagnosis: that apparatus does not exist for agents
yet, so the fear is rational rather than nostalgic.
"Nobody reads compiler output" is false. Compiler Explorer is beloved. The true claim is narrower: reading it is not the quality gate. Read generated code when curious or debugging. Do not make reading it the thing standing between you and production, because at volume that is a queue, not a gate.
where the analogy breaks
compileragent
determinismsame source, same binarysame prompt, different code. nothing to pin or bisect
the specthe source IS the spec, and a human wrote itthe prompt is the spec, and inventing the rest is the job
verificationCompCert, Alive2, Csmith. decades of formal methodsa suite somebody wrote, often the same somebody
trusting trusta compiler can backdoor itself and hide it in its own sourceone model writing both code and tests has that exact structure
Anyone invoking the compiler analogy is invoking that apparatus. Almost nobody invoking it has costed it.
the numbers
whatfindingsource
coverage vs mutationa suite can reach 100% coverage at a 4% mutation score. quoted in MUTGEN's abstract as the motivating examplearXiv 2506.02954
the benchmark gapon 3,909 real-world Python functions: 41.3% accuracy, 45.1% statement, 30.2% branch, 40.2% mutation. the same models on TestEval: 91.8 / 92.2 / 82.0 / 49.7arXiv 2508.00408
metric validitywhether coverage and mutation predict real fault detection is context-dependent: usable in regression scenarios, unreliable when the code may already be buggyarXiv 2607.22880
self-preferenceevaluators recognise and favour their own generations, above what human annotators give themarXiv 2404.13076
whythe bias tracks perplexity. familiar-looking, not betterarXiv 2410.21819
scale does not fix itadvanced capability is uncorrelated, sometimes negatively correlated, with low biasarXiv 2604.22891
self-correctionmodels struggle to self-correct without external feedback, and sometimes degradearXiv 2310.01798
provenance effectmodels correct external errors but not the same ones in their own traces. relabelling identical content as external restores itarXiv 2606.05976
The benchmark gap says generated tests are far worse than the headline. The self-preference results say the model grading them is systematically kind to work that looks like its own. Neither is fixed by a better prompt, and the second explicitly does not improve with capability.
the loop that cannot close
self-verification          provenance separation

model ──► code            model A ──► code
  │        ▲                            ▲
  └──► tests ┘            model B ──► tests ┘
        │                          │
        ▼                          ▼
     GREEN                   independent oracle
and it means nothing:    still not sufficient, but
both halves share the    a wrong belief now has to
same wrong belief        be held twice

test_oracle_independence.py runs exactly this, with no model involved. A parser and its tests built on one plausible wrong number: self-consistent, green, and wrong. Only the spec catches it.

It is a property of where the assertions came from, and it holds for a human who writes tests by running the code and pasting the output.
eight mitigations
mutation gatea test that kills no mutant is not a test
positive companionor a refusing parser passes the file
provenance separationthe author does not grade it
differentialan independent implementation, not an expectation
properties over examplesreview five, not five hundred
meta-testssuites rot toward passing, and a generator helps
metamorphicfor when you cannot state the answer
equivalent mutantsthe manual cost, stated honestly
Properties over examples is the strongest practical one. Examples are cheap for a model to emit and expensive for a human to validate. Properties invert both costs.
where it runs out
a test can check the code does what the test says.
nothing can check the test says what was meant.

spec ──► tests ──► code
 ▲
 └─ a human wrote this, and a human has to read it

every mitigation lives to the RIGHT of the spec.
all of them assume the tests encode the intent.
none can check that they do.

the only defence is that the spec is small enough
to be read.
Worth saying plainly rather than ending on the eight mitigations as though they closed the loop. An agent can verify code against tests all day. It cannot verify that the tests capture what anyone wanted.
Generated from terms.py, the same source as GLOSSARY.md, the printed card and section 02 of the deep dive. One line is the budget: a meaning needing two lines is an argument, and arguments live in the deep dive.
the basics
testa function whose name starts with test. It passes if it does not raise
assertiona line stating what must be true. assert x == y
test suiteall your tests together
test runnerthe tool that finds and runs them. pytest is one
collectionpytest finding your tests, before running any
fail vs errorfail = an assertion was false. error = it blew up before reaching one
AAAarrange, act, assert. The three parts of most tests, in that order
TDDwrite the failing test first, then the code that makes it pass
fixtures
fixturenamed setup a test asks for by putting its name in the arguments
factory fixturea fixture returning a function, so a test can make several things
setup / teardownbefore and after. In pytest, teardown is whatever follows yield
scopehow often a fixture is rebuilt: function, class, module, package, session
autousea fixture applied to every test without being asked for
conftest.pyshared fixtures for a folder and everything under it. No import needed
overridea fixture in a test file shadowing one of the same name from conftest
tmp_pathbuilt in: an empty folder, new per test, cleaned up for you
capsys / capfdbuilt in: captured output. capfd when the write bypasses Python
caplogbuilt in: log records. Set the level or you capture nothing
monkeypatchbuilt in: change something temporarily, put back automatically
running and selecting
test idthe name in the report, e.g. test_naming[2-many]
parametrizerun one test many times with different inputs, each reported separately
indirectsend a parametrize value to a fixture, so each case gets setup and teardown
markera label on a test: @pytest.mark.slow. Select with -m
skip / skipifdoes not apply here. skipif decides at collection
xfailknown broken. Needs strict=True or it stays green once fixed
xpassan xfail that unexpectedly passed. Usually means delete the marker
deselectexcluded from this run by -k or -m. Not the same as skipped
test doubles
test doubleany stand-in used instead of the real thing
dummypassed only to fill a signature. Never actually used
stubalways returns the same canned answer
fakea real but simplified implementation. A dict instead of a database
spyworks, and records how it was called
mocka spy that also asserts it was called correctly
patchingreplacing something in place, by name
injectionpassing the collaborator in, rather than the code fetching it itself
seama place you can change behaviour without editing there. An argument is one
mock transporta fake put in the place where code would talk to the network
kinds of test
unitone function or class, nothing real underneath it
integrationseveral pieces together, often with a real file or database
end-to-endthe whole system as a user meets it. Slow, valuable, few
regression testwritten to pin a bug you just fixed, so it cannot come back
smoke testdoes it start at all
property-basedassert a rule for all inputs and let a tool hunt counterexamples
fuzzingthrow generated input at it and check nothing escapes the contract
differentialrun two implementations over one input; whichever disagrees is wrong
oracledifferential testing where the reference is a trusted external tool
golden / snapshotcompare output against a stored known-good file
contract testpin the shape you publish: names, codes, status codes, schema version
meta-testa test about the test suite, e.g. does every test still assert
test quality
coveragewhich lines ran. Not whether anything checked them
branch coveragedid both sides of each if run
mutation testingbreak the code deliberately and see whether the suite notices
equivalent mutanta deliberate break that changes no behaviour, so nothing can catch it
mutation gateadmit a generated suite only if it kills a threshold of mutants
flakypasses and fails without the code changing. Always a bug, never noise
quarantinemove a flaky test behind a deselected marker with a ticket
test smella sign the test or code is wrong. More patching than assertion is one
an AI author
apparatusthe machinery that makes reading generated output unnecessary: tests, types, sanitizers, canaries
provenance separationwhatever wrote the code does not grade it, and ideally is not the same family
self-preference biasan evaluator scoring its own generations higher than a human would
intrinsic self-correctiona model revising its own output with no external signal. Does not reliably work
external feedbacka signal from outside the model. A test run is one, which is the point
characterization testassertions transcribed from what the code currently does. Pins change, not correctness
the oracle problemknowing what the right answer is, independently of the thing being tested
metamorphic relationa property linking two runs when you cannot state the answer for either
translation validationproving one output matches its input for this run, rather than proving the tool
the intent gaptests cannot check that they encode what was actually wanted. No technical fix