Test Harness Deep Dive

pytest · Fixtures · Parametrize · xfail · Doubles · Monkeypatch · Mock Transports · ASGI · Guardrails · Mutation · Meta-Tests
testharness-fun // every technique here runs against fwvault, the specimen project in this repo // 424 tests, no runtime dependencies

The Specimen and the Run

Everything on this page runs against one small service, fwvault: a firmware artifact intake. You POST a blob, it parses the header, asks a signing oracle about the digest, applies policy, and stores what survives. It exists to have seams.

   the specimen, and which section tests which part

   POST /artifact
        
        
   parse.py    UF2 / ELF header walkers      ── §10 parser  §11 hostile
        
        
   signing.py  ask the oracle about a digest  ── §08 transports
                      └─ transport is a PARAMETER
        
   policy.py   allow / refuse, with a code    ── §12 guardrails
        
        
   store.py    content-addressed, atomic      ── tmp_path
        
        
   app.py      raw ASGI, no framework         ── §09 asgi
   cli.py      argv in, exit code out         ── §14 cli
   testing.py  specimen builders + doubles, SHIPPED IN THE PACKAGE

What a pytest run actually does

   one run, five phases. Most confusion is about which phase you are in.

   1 · CONFIG     find rootdir, read pyproject/pytest.ini, load plugins,
                  register markers, apply addopts
        
   2 · COLLECT    walk testpaths, import every test module, discover
                  conftest.py per directory, build the item tree
                  ↳ an ImportError here kills the FILE, not one test
        
   3 · MODIFY     pytest_collection_modifyitems -- deselect, reorder, mark
        
   4 · RUN        per item:  setup fixtures  →  call the test  →  teardown
                             (outermost first)      (reverse order)
        
   5 · REPORT     pass / fail / skip / xfail, plus -rs reasons
The one-sentence version of this page A test is a specimen, a call, and an assertion. Everything else -- fixtures, doubles, patching, harnesses -- exists to get a specimen into place cheaply and get an observation back out. When a technique stops doing that, stop using it.
SectionAnswers
03 fixtureshow do I get a specimen into place, and clean it up
06 doubleswhat do I put where the real collaborator goes
07 monkeypatchwhat do I do when there is no seam to pass anything through
09 asgihow does a test client work, and when do I not want one
11 hostilehow do I test the inputs I did not think of
15 metahow do I stop the suite rotting in the direction of passing

Terms

Every word this page uses, with a one-line meaning and the section that explains it. Generated from terms.py, which is also where GLOSSARY.md and the glossary side of the printed card come from. They held separate copies once and drifted.

One line is the budget A meaning that needs two lines is an argument, and arguments live in the sections rather than here. If a term below reads as thin, the section in the last column is where it is made properly.

the basics

termmeaningsection
testa function whose name starts with test. It passes if it does not raisecollect
assertiona line stating what must be true. assert x == yassert
test suiteall your tests together-
test runnerthe tool that finds and runs them. pytest is onecollect
collectionpytest finding your tests, before running anycollect
fail vs errorfail = an assertion was false. error = it blew up before reaching oneassert
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

termmeaningsection
fixturenamed setup a test asks for by putting its name in the argumentsfixtures
factory fixturea fixture returning a function, so a test can make several thingsfixtures
setup / teardownbefore and after. In pytest, teardown is whatever follows yieldfixtures
scopehow often a fixture is rebuilt: function, class, module, package, sessionfixtures
autousea fixture applied to every test without being asked forfixtures
conftest.pyshared fixtures for a folder and everything under it. No import neededcollect
overridea fixture in a test file shadowing one of the same name from conftestfixtures
tmp_pathbuilt in: an empty folder, new per test, cleaned up for youfixtures
capsys / capfdbuilt in: captured output. capfd when the write bypasses Pythonassert
caplogbuilt in: log records. Set the level or you capture nothingassert
monkeypatchbuilt in: change something temporarily, put back automaticallypatch

running and selecting

termmeaningsection
test idthe name in the report, e.g. test_naming[2-many]param
parametrizerun one test many times with different inputs, each reported separatelyparam
indirectsend a parametrize value to a fixture, so each case gets setup and teardownparam
markera label on a test: @pytest.mark.slow. Select with -mmarkers
skip / skipifdoes not apply here. skipif decides at collectionmarkers
xfailknown broken. Needs strict=True or it stays green once fixedmarkers
xpassan xfail that unexpectedly passed. Usually means delete the markermarkers
deselectexcluded from this run by -k or -m. Not the same as skippedcollect

test doubles

termmeaningsection
test doubleany stand-in used instead of the real thingdoubles
dummypassed only to fill a signature. Never actually useddoubles
stubalways returns the same canned answerdoubles
fakea real but simplified implementation. A dict instead of a databasedoubles
spyworks, and records how it was calleddoubles
mocka spy that also asserts it was called correctlydoubles
patchingreplacing something in place, by namepatch
injectionpassing the collaborator in, rather than the code fetching it itselfpatch
seama place you can change behaviour without editing there. An argument is oneorient
mock transporta fake put in the place where code would talk to the networktransport

kinds of test

termmeaningsection
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 counterexampleshostile
fuzzingthrow generated input at it and check nothing escapes the contracthostile
differentialrun two implementations over one input; whichever disagrees is wrongdifferential
oracledifferential testing where the reference is a trusted external tooldifferential
golden / snapshotcompare output against a stored known-good file-
contract testpin the shape you publish: names, codes, status codes, schema versionmeta
meta-testa test about the test suite, e.g. does every test still assertmeta

test quality

termmeaningsection
coveragewhich lines ran. Not whether anything checked themmutation
branch coveragedid both sides of each if runmutation
mutation testingbreak the code deliberately and see whether the suite noticesmutation
equivalent mutanta deliberate break that changes no behaviour, so nothing can catch itmutation
mutation gateadmit a generated suite only if it kills a threshold of mutantsai
flakypasses and fails without the code changing. Always a bug, never noisemarkers
quarantinemove a flaky test behind a deselected marker with a ticketmarkers
test smella sign the test or code is wrong. More patching than assertion is onemeta

an AI author

termmeaningsection
apparatusthe machinery that makes reading generated output unnecessary: tests, types, sanitizers, canariesai
provenance separationwhatever wrote the code does not grade it, and ideally is not the same familyai
self-preference biasan evaluator scoring its own generations higher than a human wouldai
intrinsic self-correctiona model revising its own output with no external signal. Does not reliably workai
external feedbacka signal from outside the model. A test run is one, which is the pointai
characterization testassertions transcribed from what the code currently does. Pins change, not correctnessai
the oracle problemknowing what the right answer is, independently of the thing being testeddifferential
metamorphic relationa property linking two runs when you cannot state the answer for eitherai
translation validationproving one output matches its input for this run, rather than proving the toolai
the intent gaptests cannot check that they encode what was actually wanted. No technical fixai

Collection & Config

Before a single assertion runs, pytest has decided where the project root is, which files are tests, which fixtures are visible where, and what flags are in force. Most "why is this test not running" questions are answered here.

   what counts as a test, by default

   files       test_*.py  or  *_test.py       ← testpaths, then rootdir
   classes     Test*      (and no __init__ method)
   functions   test*      ← note: test* not test_*

   that last one bites. A helper named `tests_in()` is COLLECTED as a
   test. If it is a generator, pytest refuses the whole file:

     'yield' keyword is allowed in fixtures, but not in tests

   and a collection error takes down every other test in that file.
   This page's own hygiene file hit it. Name helpers `find_tests`.

conftest.py is a plugin, not a junk drawer

   fixture visibility follows the DIRECTORY tree, not imports

   repo/
     conftest.py            ← visible to everything below
     tests/
       conftest.py          ← visible to tests/ and below; shadows the above
       test_parse.py            sees both, nearest wins
       integration/
         conftest.py        ← only here
         test_slow.py

   A fixture defined in a conftest needs NO import in a test file.
   That is the feature and the trap: a name with no visible origin.
tests/conftest.py: the three jobs# 1. extend pytest itself
def pytest_addoption(parser):
    parser.addoption("--runnet", action="store_true", default=False,
                     help="run tests marked `net`, which make real connections")

def pytest_collection_modifyitems(config, items):
    # runs after collection, before the first test.
    # this is where a marker becomes behaviour.
    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)

# 2. isolate the process, once, for everything
@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))   # expanduser reads this on Windows
    monkeypatch.setenv("FWVAULT_WALKER_RAISE", "1")

# 3. hand out specimens and collaborators -- the fixtures themselves
Deleting an env var is not isolation With FWVAULT_HOME unset, the code falls back to expanduser("~") -- so a suite that carefully deletes the variable writes into the developer's real home directory. Setting a fake HOME is what contains it, and both HOME and USERPROFILE have to be set, because expanduser reads the first on POSIX and the second on Windows.

pyproject.toml: the flags that are always on

[tool.pytest.ini_options]testpaths = ["tests"]
pythonpath = ["src"]              # src-layout, no install needed to run

# -rs so a skip explains itself. A run that says "12 skipped" and
# nothing else is a run whose coverage you cannot describe.
addopts = "-rs --strict-markers"

markers = [
    "slow: takes real wall-clock time",
    "net: would touch a real network (never enabled in CI)",
]

filterwarnings = [
    # our own deprecations are errors: a warning nobody reads is a
    # rename nobody did. Scoped, so a dependency's schedule is not ours.
    "error::DeprecationWarning:fwvault.*",
    "ignore::DeprecationWarning:starlette.*",
]
SettingWhy it earns its line
-rsprints every skip reason. Without it a skip is indistinguishable from a test that does not exist
--strict-markersa typo'd @pytest.mark.slwo is an error, not a silently-ignored decorator
testpathsstops a bare pytest from wandering into build/ or .venv/
pythonpathsrc-layout works without pip install -e ., so a clone runs immediately
filterwarningsyour own deprecations become errors. Scope them, or somebody else's release schedule becomes your build break

Fixtures: Scope, Teardown, Factories

A fixture is a named setup whose result pytest caches per scope and tears down in reverse order of setup. Everything else about fixtures is a consequence of that sentence.

   build order is DEPENDENCY order; teardown is its exact reverse

   @fixture outer                     @fixture inner(outer)
       setup                              setup
       yield       ──────────────►      yield      ──────►   test body
       teardown    ◄─────────────       teardown   ◄──────

   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 anything that can fail AFTER everything that must be cleaned up.

Scope: pick the narrowest that is not painfully slow

ScopeBuiltUse when
functiononce per test (default)almost always
classonce per test classa class groups a scenario
moduleonce per fileexpensive AND immutable
packageonce per directoryrare; a shared container per suite area
sessiononce per runa database, a compiled binary. Never mutable
A session-scoped mutable fixture is a global variable with extra steps The test that corrupts it fails a different test, three files later, only when the whole suite runs in one order. If a wide-scoped fixture must exist, make what it hands out immutable -- a tuple, a frozen dataclass, a read-only path.

The five shapes worth knowing

tests/conftest.py# 1. plain value
@pytest.fixture
def uf2():
    # the smallest thing that is genuinely valid. Two blocks, not two hundred.
    return build_uf2(blocks=2)

# 2. FACTORY -- the test gets the function, not a value
@pytest.fixture
def make_uf2():
    return build_uf2          # now a test can build three different ones

# 3. yield, for teardown
@pytest.fixture
def vault(tmp_path):
    return Store(tmp_path / "vault")   # tmp_path cleans itself up

# 4. PARAMETRIZED -- every test that requests it runs once per param
@pytest.fixture(params=["uf2", "elf"], ids=["uf2", "elf"])
def any_artifact(request):
    return build_uf2(blocks=2) if request.param == "uf2" else build_elf()

# 5. COMPOSED -- the whole service, wired to fakes, in one line,
#    because every collaborator in app.py is a parameter
@pytest.fixture
def app(vault, client, policy):
    return create_app(store=vault, client=client, policy=policy)
factory: rightdef test_sizes(make_uf2):
    small = make_uf2(blocks=1)
    large = make_uf2(blocks=9)
    assert len(large) == 9 * len(small)

# the subject is visible in the test
fixture-per-variant: wrongdef test_sizes(one_block, nine_blocks):
    assert len(nine_blocks) == 9 * len(one_block)

# what IS nine_blocks? open conftest.
# now do that forty more times.

autouse, and the two ways to misuse it

An autouse fixture applies to every test in its directory whether the test asks or not. Correct for process-wide state any test could touch by accident and no test should have to remember: the environment, a fake home, a global registry. Wrong for anything a test could reasonably want to opt out of -- that gives you a suite where nobody can explain what a test is actually running against.

the anti-pattern, named@pytest.fixture
def over_helpful_fixture(vault, uf2):
    assert len(vault) == 0       # ← WRONG PLACE
    return vault, uf2

# when that fails, EVERY test using this fixture reports an ERROR,
# with a traceback pointing at conftest.py instead of at the
# behaviour that broke.
#
#   assertions belong in tests. fixtures build things.

Overriding: nearest conftest wins, silently

A fixture defined in a test module shadows one of the same name from conftest.py, for that module only. Nothing anywhere announces it.

   resolution order for the name `policy`

   tests/test_fixtures.py    def policy()  ← wins, for this file
   tests/conftest.py             def policy()
   conftest.py                       def policy()

   nothing says a name has been shadowed. a test three hundred lines
   below is now running against a different object than its neighbour
   in the next file, and both are called `policy`.

   an override can also EXTEND what it shadows, by requesting the same
   name -- the lookup for the PARAMETER starts one level up:

     @pytest.fixture
     def strict_policy(policy):        ← this module's override
         return replace(policy, require_signature=True)

Use it when a whole file genuinely needs a different baseline. Never as a quick fix for one test: that test should ask for what it needs by name.

Two smaller tools

fixture(name=) and addfinalizer# the function is _make_digest; the fixture is `digest`.
# frees the good name for a normal import in the same module, and
# stops a linter flagging the fixture function as unused.
@pytest.fixture(name="digest")
def _make_digest(uf2):
    return digest_of(uf2)


# addfinalizer is the older API. `yield` covers almost every case and
# reads better. addfinalizer still wins when the cleanup should be
# registered CONDITIONALLY -- only once the thing exists:
@pytest.fixture
def conditional_cleanup(request, tmp_path):
    target = tmp_path / "artifact.bin"
    target.write_bytes(bytes(16))
    request.addfinalizer(target.unlink)   # registered only now that it exists
    return target

# with `yield`, the teardown half runs even when the setup half failed,
# and has to defend itself against a thing that was never created.

Parametrize & IDs

One function, many cases, each reported as its own test. The mechanics are easy; the value is entirely in the IDs.

   stacked decorators are a CARTESIAN PRODUCT

   @parametrize("bit64", [True, False], ids=["elf64", "elf32"])
   @parametrize("little", [True, False], ids=["le", "be"])
   def test_elf_entry(bit64, little): ...

                    ┌──────────────┬──────────────┐
                       elf64         elf32      
        ┌───────────┼──────────────┼──────────────┤
            le      [le-elf64]    [le-elf32]   
        ├───────────┼──────────────┼──────────────┤
            be      [be-elf64]    [be-elf32]   
        └───────────┴──────────────┴──────────────┘

   4 tests from 2 lines. All four are real files somebody has --
   entry point width and byte order are independent header fields.
ids: three ways, all better than the default# 1. an explicit list -- best when the cases have names
@pytest.mark.parametrize(
    "family,expected",
    [(RP2040, "RP2040"), (NRF52840, "NRF52840"), (UNKNOWN_FAMILY, None)],
    ids=["rp2040", "nrf52840", "unknown"],
)

# 2. a callable -- best for computed cases
@pytest.mark.parametrize("bad_block", [0, 1, 2], ids=lambda n: "block%d" % n)

# 3. repr -- best for byte specimens
@pytest.mark.parametrize("blob", [b"", b"\x00", b"\x7fEL"], ids=repr)
#   → test_short_buffers[b'\x7fEL']
A failure that names its case is worth ten that do not test_family[3-2] tells you nothing and sends you counting list entries. test_family[nrf52840] tells you everything. You will read these IDs in CI output, in a bisect, and in a message from someone who does not have the repo open.

When parametrize is the wrong tool

SituationDo this instead
the cases need different assertionsseparate tests. A body with if param == "uf2" is two tests wearing a trenchcoat
the list is 400 generated inputsproperty-based (§13). Parametrize when you want each case NAMED
only one case has a setup steppull that case out. A parametrize with a branch in setup is a fixture in disguise
the same cases recur across filesa parametrized fixture, or a module-level constant list both files import

One more habit worth building: when a sweep is shared with a test that checks the sweep, make it one constant. This page's own hostile-input file had a truncation sweep stepping by 97 -- which never lands on a multiple of 512, so every one of its twenty cases hit the same early branch and none ever reached the walker. The meta-test caught it. Two copies of the list would have drifted silently.

tests/test_hostile.py# One list, used by the sweep AND by the meta-test that checks the sweep.
SWEEP = sorted(set(range(0, 1537, 64)) | {1, 7, 511, 513, 1535})

@pytest.mark.parametrize("length", SWEEP)
def test_every_truncation_is_survivable(length):
    _parses_or_raises_parse_error(VALID[:length])

def test_the_sweep_actually_covers_both_outcomes():
    outcomes = {_parses_or_raises_parse_error(VALID[:n]) is not None for n in SWEEP}
    assert outcomes == {True, False}, (
        "every case in SWEEP had the same outcome, so the sweep is "
        "testing one branch twenty-five times")

Indirect: send the parameter to a fixture

Plain parametrize hands the value to the test. indirect hands it to a fixture first, so each case gets real setup and real teardown.

tests/test_parametrize.py@pytest.fixture
def vault_with(request, tmp_path):
    store = Store(tmp_path / "v")
    for n in range(1, request.param + 1):
        store.put(...)
    yield store
    # teardown a plain parametrize cannot express: where a socket, a
    # process or a database transaction would close.

@pytest.mark.parametrize("vault_with", [0, 1, 5], indirect=True,
                         ids=lambda n: "%d-stored" % n)
def test_vault_size(vault_with):
    assert len(vault_with) == vault_with.expected

# indirect=["artifact"] names WHICH params get routed; the others
# arrive normally. Worth it when each case needs setup or teardown.
# NOT worth it when the fixture is just a function call -- that is
# what a factory fixture is for.

pytest_generate_tests: build the list at collection

the hook behind parametrizedef pytest_generate_tests(metafunc):
    # runs once per test function, at collection time.
    if "known_family" in metafunc.fixturenames:
        families = importlib.import_module("fwvault.parse").FAMILIES
        metafunc.parametrize(
            "known_family,expected_name",
            sorted(families.items()),
            ids=[name.lower() for _fid, name in sorted(families.items())])

# the cases come from the SOURCE OF TRUTH rather than a copy of it.
# add a supported chip and a test appears; nobody has to remember.
#
# the cost: a reader of the file cannot see the cases without running
# `pytest --collect-only`. pay it only when keeping a literal list in
# sync is the bigger risk.
Stacking multiplies Two axes of two values is 4 tests. A fourth axis of 5 is 40, a fifth is 200. When most combinations stop testing anything new, switch to a hand-picked list of the ones that matter, or to property-based testing (§13), which searches the space instead of enumerating it.

Assertions & Failure

pytest rewrites the bytecode of your test modules so a bare assert reports the values of its subexpressions. That is why you never need assertEqual. It also means the quality of a failure message is a property of how you wrote the assertion.

tells you nothingassert result

# E  assert None


assert len(hits) > 0

# E  assert 0 > 0
tells you what brokeassert codes(image, SIGNED, policy) == ["OVERSIZE"]

# E  assert ['UNSIGNED'] == ['OVERSIZE']
# E    At index 0 diff: 'UNSIGNED' != 'OVERSIZE'

assert not offenders, "undocumented flags: " + ", ".join(offenders)

# E  AssertionError: undocumented flags: ingest vault

Exceptions: assert the message and the object, not just the class

pytest.raises# the type alone. Passes when the message says "None".
with pytest.raises(ParseError):
    sniff(blob)

# + the message. `match` is re.search, so it is a substring by default.
with pytest.raises(ParseError, match=r"unrecognised magic deadbeef"):
    sniff(b"\xde\xad\xbe\xef" + b"\x00" * 8)

# + the OBJECT. TruncatedError carries offset/needed/available
# precisely so a test can say WHERE.
with pytest.raises(TruncatedError) as excinfo:
    parse(build_elf(truncate_to=26))
assert excinfo.value.needed == 32
assert excinfo.value.available == 26
match is a regex, and your message probably contains regex metacharacters match="offset (0x1fc)" does not do what it looks like -- the parentheses are a group. Use re.escape(), or match a distinctive substring without punctuation. A match that silently matches nothing still passes if the exception type is right.

The assertion vocabulary

ToolFor
assert a == beverything. Rich diffs for str, list, dict, set, and dataclasses
pytest.raises(E, match=)type and message together
pytest.warns(W, match=)the same, for warnings. A warning nobody asserts is a warning nobody reads
pytest.approx(x, rel=, abs=)floats. 0.1 + 0.2 == pytest.approx(0.3)
capsys / capfdstdout and stderr. capsys at the Python level, capfd at file-descriptor level
caploglog records, with caplog.set_level() and caplog.records
recwarnevery warning raised. Broad; prefer pytest.warns for a specific one
capsys or capfd capsys replaces sys.stdout. If your output goes through a C extension, a subprocess, or anything writing to fd 1 directly, capsys sees nothing and capfd sees everything. Reach for capsys first; if output goes missing, that is your answer.

One behaviour per test

A test with four unrelated asserts reports the first failure and hides the other three. Two asserts about the same decision seen from two sides are fine and often better together:

two views of one decisiondef test_oversized_payload_size_is_clamped_and_warned():
    image = parse(build_uf2(blocks=2, oversized_payload=9999))
    assert image.payload_bytes == UF2_PAYLOAD_MAX + 256   # the clamp
    assert any("exceeds 476" in w for w in image.warnings)  # and it SAID so

# a version that clamps silently is the bug this test exists to catch,
# and it needs both halves to catch it.

caplog: two things people miss

tests/test_transport.pydef test_a_retry_is_logged(caplog, clock):
    # 1. SET THE LEVEL. pytest captures at WARNING by default, so an
    #    log.info() you are asserting on never arrives, and the test
    #    fails looking like the code is wrong.
    caplog.set_level(logging.WARNING, logger="fwvault.signing")

    script = ScriptedTransport(flaky(2, Response(200, signed_body())))
    SigningClient("https://x", transport=script, clock=clock, retries=3).verify(d)

    # 2. ASSERT ON RECORDS, NOT ON TEXT. a record has .levelname, .name
    #    and .getMessage(). caplog.text is whatever the formatter felt
    #    like, and asserting on it couples the test to a format string
    #    nobody thinks of as an interface.
    retries = [r for r in caplog.records if "attempt" in r.getMessage()]
    assert len(retries) == 2
    assert all(r.levelname == "WARNING" for r in retries)
    assert all(r.name == "fwvault.signing" for r in retries)
And the negative case, which keeps a service usable test_the_happy_path_logs_nothing asserts caplog.records == [] for a successful call. A log line on every successful request is a log nobody reads, which is the same as no log at all on the night something finally goes wrong.

In the package: log = logging.getLogger(__name__), never logging.warning(...). The module-level function configures the root handler as a side effect and steals output formatting from whatever application imported you.

Markers, xfail & Flakes

Four ways a test can not-run, and they mean completely different things. The one people get wrong is xfail, and it fails quietly.

   skip          this does not apply here        platform, missing dep
   skipif(cond)  the same, decided at collection
   xfail         this is BROKEN and we know     a bug with a ticket
   xfail(strict) ...and tell me the MOMENT it stops being broken
   deselected    a marker the run excluded      -m 'not net'

   ─────────────────────────────────────────────────────────────

   a plain xfail that starts passing reports XPASS and stays GREEN.

   so: the bug gets fixed, nobody notices, the marker sits there for
   two years, and the test has been asserting nothing that whole time.

   strict=True turns that XPASS into a FAILURE. it is the only
   setting that makes xfail a temporary state rather than a permanent
   one. set xfail_strict = true in pyproject and get it by default.
silently green forever@pytest.mark.xfail(reason="bug FW-118")
def test_the_bug_is_fixed_now():
    assert True

# 1 xpassed. exit code 0.
tells you to delete the marker@pytest.mark.xfail(strict=True, reason="bug FW-118")
def test_the_bug_is_fixed_now():
    assert True

# 1 failed. exit code 1.
raises= narrows it further xfail(strict=True, raises=ValueError) means "fails for this reason". Without it an xfail absorbs every failure, including the ImportError you introduced this morning, which is not the bug you were documenting.

pytest.param: marking one case

A bare tuple in a parametrize list cannot carry an id or a marker. pytest.param wraps one case so it can carry both.

tests/test_markers.py@pytest.mark.parametrize(
    "family,expected",
    [
        pytest.param(0xE48BFF56, "RP2040", id="rp2040"),
        pytest.param(0xADA52840, "NRF52840", id="nrf52840"),
        pytest.param(
            UNKNOWN_FAMILY, "SOME-FUTURE-CHIP",
            id="unknown",
            marks=pytest.mark.xfail(
                strict=True,
                reason="we do not name unknown families; parse returns None"),
        ),
    ],
)
def test_family_naming_with_one_expected_failure(family, expected):
    assert parse(build_uf2(blocks=1, family=family)).family == expected

# the third case is a real, documented gap, stated IN THE SUITE rather
# than in a comment. The day someone adds that family to the table,
# this fails and tells them to delete the marker.
selecting one case out@pytest.mark.parametrize(
    "blocks", [1, 2, pytest.param(600, marks=pytest.mark.slow, id="600-blocks")])
def test_marks_can_select_out_one_case(blocks): ...

# `pytest -m 'not slow'` drops ONLY the 600-block case. Without
# pytest.param the marker goes on the whole function and takes the
# two cheap cases with it.

usefixtures, for a fixture with nothing to return

side-effect-only fixtures@pytest.mark.usefixtures("strict_umask")
def test_something():
    ...

# the dependency is still declared and still visible; it just is not
# pretending to be a value your linter has to ignore.
#
# do NOT use it to hide a fixture whose value you actually want. the
# signature is where a reader looks for a test's inputs.

The flaky test

A flaky test is a bug report, against the test or against the code. It is never noise: something is genuinely non-deterministic and you have found it.

   the causes, in the order they actually occur

   unseeded randomness          → fix the seed        (§11)
   a real clock or a real sleep → inject a clock      (§08)
   shared state between tests   → narrow the scope    (§15)
   a fixed path two workers race→ tmp_path           (§15)
   a real network call          → a marker + a fake  (§08)
   dict / set ordering assumed  → sort, or compare sets

   ─────────────────────────────────────────────────────────────

   QUARANTINE, do not rerun.

   move it to a marker that CI deselects, with a ticket. it stops
   blocking and it stops lying. `pytest-rerunfailures` is a last
   resort for a genuinely external dependency you do not control;
   `--reruns 3` on your own code converts a real bug into a slower
   green run, permanently.

   the tell that you have this wrong: anyone on the team has ever
   said "just re-run it".
Test the outcomes, do not describe them Asserting that a skip happened, from inside the same run, means asserting on a thing that by definition did not execute. test_markers.py runs a nested pytest in a temp directory and asserts on its exit code and output, so every claim on this page about XPASS and strict=True is checked rather than remembered.

The Five Test Doubles

The words are Gerard Meszaros's. They are worth using precisely because "mock" has drifted into meaning "any object I made up". The distinction is not pedantry: it predicts how your test fails.

   the ladder, in order of how much they know about themselves

   DUMMY    passed to satisfy a signature, never used
              └─ `evaluate(image, None, unchecked_policy)`

   STUB     returns canned answers. no state worth asking about
              └─ 4 lines. cannot tell you the URL was right.

   FAKE     a real, working, SIMPLIFIED implementation
              └─ the one to reach for. It can be WRONG in a way a test notices.

   SPY      a stub that records how it was called
              └─ for asserting on calls that DID NOT happen

   MOCK     a spy with expectations built in. it asserts on ITSELF
              └─ fails inside the double, so the message describes a call
                 that did not happen rather than a behaviour that is wrong
Rule of thumb Prefer a fake. Fall back to a spy. Reach for a mock almost never. A mock puts the assertion in the setup section, where nobody looks for it.
stub: cannot model a 404class StubTransport:
    def __init__(self, response):
        self.response = response

    def request(self, method, url, body=None, headers=None):
        return self.response

# answers everything identically, so it passes
# against a client that builds a nonsense URL.
fake: models both outcomesclass FakeOracle:
    def __init__(self, known=None):
        self.known = dict(known or {})

    def request(self, method, url, body=None, headers=None):
        digest = url.rsplit("/", 1)[-1]
        if digest not in self.known:
            return Response(404, b'{"error":"unknown"}')
        return Response(200, signed_body(self.known[digest]))

Only the fake can express the distinction the whole service is built around: a 404 is a definite no, not an outage. A stub returning 200 forever cannot fail to model a 404, because it was never modelling anything -- so a suite built on stubs never checks it.

The mock failure mode, twice

unittest.mock, as people actually use itfrom unittest.mock import Mock

transport = Mock(spec=["request"])            # spec= is NOT optional
transport.request.return_value = Response(200, signed_body())

client = SigningClient("https://x", transport=transport, clock=clock)
client.verify("c" * 64)

transport.request.assert_called_once_with(
    "GET", "https://x/keys/" + "c" * 64, headers={"accept": "application/json"})

# FAILURE 1: the message.
#   "Expected call: request('GET', ...)" -- nothing about which
#   BEHAVIOUR is wrong. Compare a spy: you get a URL diff.

# FAILURE 2: a Mock without spec answers ANY attribute with another Mock.
loose = Mock()
loose.this_method_does_not_exist().nor_does_this_one()   # passes. silently.
transport.reqeust                                        # spec= makes this raise

Ship your doubles

A library whose only seam is a transport protocol owes its users a working fake for that protocol -- the way httpx ships MockTransport and Django ships a test client. If the fake lives in tests/, every downstream user writes their own and every one of them gets a detail wrong. fwvault.testing is a shipped module, and §17 asserts its signatures still match the real transport.

fwvault/testing.py: a scripted double for sequencesclass ScriptedTransport:
    """Answers a fixed sequence, one per call. For testing retry loops,
    where WHEN a response arrives is the behaviour under test."""

    def request(self, method, url, body=None, headers=None):
        self.calls.append((method, url, body, headers or {}))
        if not self.script:
            # Running off the end is a HARD FAILURE, not a repeat of the
            # last item. A double that quietly keeps answering turns
            # "retried 3 times" and "retried 300 times" into the same
            # passing test.
            raise AssertionError(
                "ScriptedTransport exhausted: call {} was not scripted"
                .format(len(self.calls)))
        item = self.script.pop(0)
        if isinstance(item, Exception):
            raise item
        return item
The limit no double can cross A stub or a fake encodes your belief about what the collaborator does. When that belief goes stale -- the API changed, the status code is different now -- the double keeps agreeing with you and the suite stays green against a service that no longer exists. Nothing inside your process can detect that. See §10 for how to keep the gap visible instead of pretending it is covered.

Monkeypatching: Every Method, and When Not To

monkeypatch is a function-scoped fixture that records every change it makes and undoes all of them at teardown, in reverse order, whether the test passed, failed, or exploded. That automatic undo is the entire reason to use it over os.environ[...] = x or a hand-rolled try/finally.

MethodPatchesNote
setattr(obj, name, value)an attributeraises if the name does not exist -- that is the guardrail
delattr(obj, name)removes one
setitem(mapping, k, v)a dict entrysys.modules, os.environ, any lookup table
delitem(mapping, k)raising=False for cleanup deletes
setenv(name, value)environmentvalues are strings; be explicit about the coercion
delenv(name)raises if unset. raising=False in fixtures
syspath_prepend(path)sys.pathALSO invalidates importlib caches -- a bare sys.path.insert does not
chdir(path)working directoryand puts it back

Patch where the name is LOOKED UP

   this is the single most common patching mistake, and it fails
   by the test PASSING against unpatched code.

   works                             does nothing

   parse.py                          parse.py
     from . import families           from .families import FAMILIES
     ...                                 ...
     families.FAMILIES[fam]              FAMILIES[fam]
                                            
             read at CALL time                bound at IMPORT time
                                             into a SECOND namespace
   patch families.FAMILIES            patch families.FAMILIES
   → the parser sees it              → the parser never looks there again

   the from-import already copied the reference.

Four traps, all real, all found writing this repo

1: raising=False disables the only signal you have# patching a name that does not exist is almost always a typo or a
# rename you have not noticed, so monkeypatch raises.
monkeypatch.setattr(parse_module, "FAMILEIS", {})                 # AttributeError
monkeypatch.setattr(parse_module, "FAMILEIS", {}, raising=False)  # created,
                                                                # and aimed at nothing
2: patching a constant does not move an already-bound default@dataclass(frozen=True)
class Manifest:
    schema_version: int = SCHEMA_VERSION     # ← evaluated ONCE, at import

monkeypatch.setattr("fwvault.store.SCHEMA_VERSION", 99)
Manifest(...).schema_version                 # still 3.

# same shape as `def f(x=CONST)`, same shape as `from x import CONST`.
# if a value must be patchable, it has to be READ at call time.
3: the string target form walks getattr, and can be ambushed# fwvault/__init__.py does `from .parse import parse`, which rebinds
# the attribute `fwvault.parse` from the MODULE to the FUNCTION.

monkeypatch.setattr("fwvault.parse.UF2_PAYLOAD_MAX", 8)
# AttributeError: 'function' object at fwvault.parse has no attribute ...

import fwvault.parse as m       # does NOT save you -- `as` getattrs too
m = importlib.import_module("fwvault.parse")   # reads sys.modules. works.
from fwvault.parse import sniff              # also works. prefer this.
4: sys.modules[name] = None makes `import name` raise# the import-isolation trick. Test the dependency-is-absent path on a
# machine where it is very much present.

monkeypatch.delitem(sys.modules, "httpx", raising=False)  # ← NOT optional
monkeypatch.setitem(sys.modules, "httpx", None)
with pytest.raises(ImportError):
    import httpx

# the delitem first matters: if the module is already imported, the
# None assignment is what takes effect, and forgetting to clear an
# existing entry gives a test that passes for the wrong reason.

The rule

   PATCH what you do NOT own.      urllib, datetime, os.replace,
                                    a third-party client's internals

   INJECT what you DO own.          your transport, your clock,
                                    your store, your policy

   why it matters, concretely:

   A. monkeypatch UrllibTransport.request    passes even if the client
                                             stops using its transport
                                             entirely

   B. pass transport=<a fake>                fails immediately -- the fake
                                             stops being asked anything
The tell If your test has more lines of patching than of assertion, the code under test has no seam and the patching is a symptom. Adding a parameter is nearly always cheaper than maintaining the patch.

monkeypatch.context(): undo early

The fixture undoes everything at teardown, which is too late when one test needs the patched behaviour and then the real behaviour.

patch, assert, unpatch, assert againdef test_monkeypatch_context_undoes_early(monkeypatch):
    with monkeypatch.context() as m:
        m.setattr(parse_module, "FAMILIES", {})
        assert parse(build_uf2(blocks=1)).family is None

    assert parse(build_uf2(blocks=1)).family == "RP2040"   # already restored
The reason you will actually need it monkeypatch is function-scoped. A session or module-scoped fixture cannot request it: pytest refuses with a ScopeMismatch. To patch from a wider-scoped fixture, build the context manager yourself.

@pytest.fixture(scope="session")
def patched_env():
    with pytest.MonkeyPatch.context() as m:
        m.setenv("FWVAULT_HOME", "/somewhere")
        yield m

Mock Transports

The thing that talks to the network is a parameter, not an import. SigningClient never mentions urllib, sockets or hosts -- it holds a transport with a single request method, and the real one is just the default argument.

   the seam. Nothing is patched; the test passes a different object.

   SigningClient
          .verify(digest)
        
   transport.request(method, url, body, headers) → Response
        
        ├── UrllibTransport      real sockets. stdlib. §net marker 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 that fills it.
   `Response` is a 3-field dataclass, deliberately NOT an httpx.Response.

What is actually worth asserting

Not "it returned a Verdict". The interesting assertions are about when, how often, and what it did not do -- and none of those are visible in a return value.

the retry schedule, in zero wall-clock timedef test_backoff_is_exponential_and_instant(clock):
    script = ScriptedTransport(flaky(3, Response(200, signed_body())))
    client = SigningClient("https://x", transport=script, clock=clock, retries=4)
    client.verify("a" * 64)

    assert clock.slept == [0.5, 1.0, 2.0]

# the schedule IS the behaviour. A real sleep here would put 3.5
# seconds into every CI run to test arithmetic.

def test_no_sleep_on_the_first_attempt(clock):
    # an off-by-one in a backoff loop costs every caller half a second
    # on the HAPPY path, and no return value shows it.
    assert clock.slept == []

The status taxonomy this is all for

   what the oracle said              what it MEANS

   200 {"signed": true}    ──►   Verdict(signed=True)      an answer
   404                     ──►   Verdict(signed=False)     an answer. NOT retried.
   401 / 403 / 400         ──►   VaultUnavailable          OUR credentials are wrong
   500 / 502 / 503         ──►   retry, then VaultUnavailable
   connection reset        ──►   retry, then VaultUnavailable

   VaultUnavailable is NEVER Verdict(signed=False).

   An outage rendered as "unsigned" turns into a wall of confident
   refusals indistinguishable from real ones, and nobody finds it
   until someone asks why every build failed overnight.
A stale yes that says it is stale When the oracle dies and a cached answer exists, the client serves it with stale=True, and the API surfaces stale_verdict. A cached yes served during an outage is useful; a cached yes presenting as fresh is a lie with a timestamp on it. Availability and honesty are two behaviours, so they get two tests -- a change that keeps the first and drops the second should fail one test with an obvious name.

httpx: two transports, opposite ends of the wire

the confusion worth clearing up once# ASGITransport substitutes the SERVER -- the thing that answers us.
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://vault") as c:
    r = await c.post("/artifact", content=uf2)

# MockTransport substitutes the THING WE CALL -- the far side.
def handler(request):
    return httpx.Response(200, json={"signed": True, "signer": "ci-builder"})

httpx_client = httpx.Client(transport=httpx.MockTransport(handler))

# same library, opposite directions, and they get confused constantly.

The gap you cannot close, named rather than hidden

tests/test_transport.py@pytest.mark.net
def test_urllib_transport_against_a_real_server():
    """The one thing no fake can cover: that UrllibTransport speaks HTTP.

    Deselected by default; `pytest --runnet` opts in. The skip reason prints
    on every run because pyproject sets -rs, so the gap stays VISIBLE instead
    of being quietly covered by a fake of the very class under test."""
    response = UrllibTransport(timeout=5.0).request("GET", "https://example.com/")
    assert response.status == 200


def test_urllib_transport_converts_socket_errors(monkeypatch):
    # what CAN be tested without a socket: that the error TRANSLATION
    # is right. urlopen is PATCHED here, and that is correct -- urllib
    # is not our seam, we do not own it, there is no argument to pass.
    monkeypatch.setattr(urllib.request, "urlopen", boom)
    with pytest.raises(TransportError, match="unreachable"):
        UrllibTransport().request("GET", "https://x/")

ASGI & Fake Clients

An ASGI app is a coroutine taking three arguments. That is the whole protocol. Every framework you have used is a very good router and middleware stack wrapped around those three names, and every test client you have used is something that builds a scope, feeds a receive, and collects the sends.

   async def app(scope, receive, send)

   scope     a dict describing the connection
             {"type": "http", "method": "POST", "path": "/artifact",
              "headers": [(b"content-type", b"application/octet-stream")], ...}

   receive   an awaitable you CALL to pull events IN
              {"type": "http.request", "body": b"...", "more_body": True}
              {"type": "http.request", "body": b"...", "more_body": False}
              {"type": "http.disconnect"}

   send      a coroutine you CALL to push events OUT
              {"type": "http.response.start", "status": 201, "headers": [...]}
              {"type": "http.response.body", "body": b'{"digest":...}'}

   plus one more scope type nobody handles until it hangs:
   lifespan  {"type": "lifespan"} → startup / shutdown, each needing a reply

The whole test client, in twenty lines

tests/test_asgi_raw.pydef call(app, method="GET", path="/", body=b"", headers=None, chunks=None):
    """One request, start to finish. Returns (status, headers, parsed body)."""
    incoming = list(chunks) if chunks is not None else [body]
    scope = {
        "type": "http", "asgi": {"version": "3.0"}, "method": method,
        "path": path, "query_string": b"",
        "headers": [(k.lower().encode(), v.encode())
                    for k, v in (headers or {}).items()],
    }

    async def receive():
        if incoming:
            part = incoming.pop(0)
            return {"type": "http.request", "body": part,
                    "more_body": bool(incoming)}
        return {"type": "http.disconnect"}

    sent = []
    async def send(message):
        sent.append(message)

    asyncio.run(app(scope, receive, send))

    start = next(m for m in sent if m["type"] == "http.response.start")
    payload = b"".join(m.get("body", b"") for m in sent
                       if m["type"] == "http.response.body")
    return start["status"], dict(start["headers"]), json.loads(payload)

Three clients, one behaviour

ClientGives youTakes away
raw harnesschunk boundaries, disconnects, malformed scopes, visible lifespanheader normalisation, cookies, redirects
httpx.ASGITransportreal header handling, the same API as your production client codecontrol of the protocol
starlette TestClientall of that, plus lifespan via a context manager, sync over asyncthe same, plus a framework dependency
The assertions should barely change In this repo, test_asgi_raw.py and test_asgi_clients.py assert nearly identical things about the same app. That is the demonstration: if swapping the client changes what you assert, you were testing the client. And TestClient wraps any ASGI app -- fwvault has never heard of Starlette, and the client does not care.

What only the raw harness can do

things no HTTP client will let you send# 1. choose the chunk boundaries. The reassembly loop is exactly
#    where an off-by-one costs you a corrupted upload.
call(app, "POST", "/artifact", chunks=[uf2[:100], uf2[100:700], uf2[700:]])

# 2. prove the size limit fires BEFORE the whole body is buffered.
#    a service that buffers 900 MB and THEN checks has no limit.
app = create_app(store=vault, client=client, policy=policy, max_body=1024)
status, _h, body = call(app, "POST", "/artifact", chunks=[...])
assert (status, body["error"]) == (413, "OVERSIZE")

# 3. hang up mid-body. The app must not respond to a closed
#    connection, and must not raise.
events = [{"type": "http.request", "body": uf2[:100], "more_body": True},
          {"type": "http.disconnect"}]
assert sent == []

# 4. drive lifespan by hand, and assert both events are answered.
assert lifespan(app) == ["lifespan.startup.complete", "lifespan.shutdown.complete"]
Lifespan is where hangs come from A test client sends startup and shutdown events and waits. An app that ignores them hangs at fixture time, and the failure looks like the test framework being broken rather than the app. Answer lifespan, and assert that you do.

Async fixtures need an async plugin

the "coroutine was never awaited" answer@pytest.fixture
def anyio_backend():
    # anyio's plugin parametrizes over backends via this fixture.
    # pinning it stops the run also spawning a trio pass.
    return "asyncio"

@pytest.fixture
async def http(app):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport,
                                 base_url="http://vault") as client:
        yield client

@pytest.mark.anyio
async def test_ingest_over_httpx(http, uf2):
    response = await http.post("/artifact", content=uf2)
    assert response.status_code == 201

# no plugin → pytest collects the coroutine as if it were a test,
# never awaits it, and reports a pass with a RuntimeWarning.

Parser Tests: Walkers, Builders, Corpora

Two parsers, one shape: a generator that walks structure and yields records, wrapped by a function that folds those records into a result. Keeping the walk separate from the fold is what makes a parser testable at two levels.

   walk / fold, and what each half buys you

   bytes ──► walk_uf2() ──► Block, Block, Block ──► parse_uf2() ──► Image
                                                       
                 a GENERATOR                            the FOLD
                 · 40 MB costs one block of RAM         · testable with hand-built
                 · a caller can stop early                records and NO bytes
                 · assert on block 10 of 900            · where warnings accumulate
                   without building a manifest         

   raise vs warn is THE line, and getting it wrong either way is a bug:
     raise too eagerly   half the real world is unparseable
     warn too eagerly    a corrupt image ships

The UF2 block, which is what all of this parses

   512 bytes, three magic numbers, 476 usable payload bytes

   offset   size   field
   ──────────────────────────────────────────────────────────────────
     0       4     magicStart0   0x0A324655   "UF2\n"
     4       4     magicStart1   0x9E5D5157
     8       4     flags         0x2000 = familyID present
    12       4     targetAddr    where this block is flashed
    16       4     payloadSize   ≤ 476, and a parser must clamp
    20       4     blockNo
    24       4     numBlocks     declared, not verified
    28       4     fileSize / familyID
    32     476     data
   508       4     magicEnd      0x0AB16F30
   ──────────────────────────────────────────────────────────────────

   the third magic at 508 is the POINT of the format:
   it makes a block self-identifying even when found mid-stream.

Build specimens, do not commit them

A builder turns keyword arguments into bytes, and every defect argument names a block index. A test then reads as "block 1's end magic is wrong" rather than as a byte offset the reader has to divide by 512 in their head.

fwvault/testing.pydef build_uf2(blocks=2, family=RP2040, payload_size=256, base_addr=0x10000000,
              flags=None, bad_start_magic=None, bad_end_magic=None,
              wrong_num_blocks=None, oversized_payload=None,
              shuffle_block_no=False, trailing_garbage=0, addr_step=None):
    ...

# reads as a sentence:
build_uf2(blocks=3, bad_end_magic=1)              # block 1's end magic is wrong
build_uf2(blocks=10, shuffle_block_no=True)      # blockNo fields out of order
build_uf2(blocks=2, wrong_num_blocks=0xFFFFFFFF) # claims 4 billion blocks
Why generated rather than committed A committed corpus of real files is licensing risk, repository weight, and -- worst -- a corpus that exists on one machine. If a test names a path in a gitignored tree, it runs on the developer's laptop and skips on every runner, and both readings look fine from where you are standing. Generated specimens are deterministic, identical everywhere, and a failure is reproducible from the arguments in the traceback.

Test the builder

the one everybody skipsdef test_the_builder_builds_what_it_claims():
    """A corpus generator is code, and untested code in a test corpus
    produces tests that pass against the wrong bytes.

    This asserts the raw layout DIRECTLY, without going through the parser --
    otherwise a matching pair of bugs in builder and parser cancels out and
    the suite stays green."""
    blob = build_uf2(blocks=1, family=RP2040, payload_size=128)
    assert len(blob) == 512
    assert struct.unpack_from("<I", blob, 0)[0] == UF2_MAGIC0
    assert struct.unpack_from("<I", blob, 16)[0] == 128
    assert struct.unpack_from("<I", blob, 508)[0] == UF2_MAGIC_END

Sniff by magic, never by filename

and locate every finding absolutelydef test_block_offsets_are_absolute():
    # so a finding can be handed to `xxd -s`. Relative offsets are the
    # reason people stop trusting parser output.
    blocks = list(walk_uf2(build_uf2(blocks=3)))
    assert [b.offset for b in blocks] == [0, 512, 1024]

@pytest.mark.parametrize("bad_block", [0, 1, 2], ids=lambda n: "block%d" % n)
def test_bad_end_magic_is_located(bad_block):
    # parametrizing over WHICH block is defective catches an off-by-one
    # in the offset arithmetic that a single specimen would not.
    with pytest.raises(ParseError) as excinfo:
        list(walk_uf2(build_uf2(blocks=3, bad_end_magic=bad_block)))
    assert excinfo.value.offset == bad_block * 512 + 508

The negative case people forget

without this, a parser that warns about everything passesdef test_a_clean_image_warns_about_nothing():
    assert parse(build_uf2(blocks=4)).warnings == ()

Why parsers get more testing than other code

Four properties, and no other kind of code has all four.

PropertyConsequence for testing
the input space is adversarial by definitionthe entire job is consuming bytes somebody else chose. "Nobody would send that" is not available to you
the state machine is deepa wrong branch three levels in still returns something plausible. The failure is a wrong answer, not a crash
length fields are trust boundariesan off-by-one at a length prefix is a memory-safety or trust bug, not a style issue
a real oracle usually existsthe unusual luxury. There is a spec, and often a second implementation. Almost no other code has that

The techniques, in increasing order of return

   cheapest first. each one finds what the one above it cannot.

   1 corpus + golden files     catches regressions. catches nothing new.
   2 truncation sweeps         an interrupted upload is the commonest
                              corrupt file alive. cheapest real finder.
   3 bit flips, seeded fuzz    field boundaries beat random offsets
                              by orders of magnitude.
   4 metamorphic relations     no oracle needed. parse, reserialize,
                              reparse, and assert a fixed point.
   5 differential              the strongest, when a second
                              implementation exists.
   6 structural generation     generate VALID inputs so the fuzzer
                              spends its budget past the header checks
                              instead of bouncing off them.

   and the sweep itself needs a test. this repo's truncation sweep
   stepped by 97, which never lands on a multiple of 512, so all
   twenty cases took the same early branch and none reached the
   walker. a meta-test caught it. see §13.

Metamorphic relations: assertions with no oracle

Every other technique needs somebody to know the right answer. An example test states it, a differential test borrows it, a property test implies it. A metamorphic relation needs none: it says how two runs must relate, never what either one should produce.

the four shapes worth learning# ROUND TRIP: the strongest, when an inverse exists
assert serialize_uf2(walk_uf2(blob)) == blob
#   byte for byte. anything the walker drops shows up as a diff,
#   including fields nothing else in the suite reads.

# INVARIANCE: changing X must NOT change Y
scrambled = serialize_uf2(replace(b, data=random_bytes()) for b in blocks)
assert parse(scrambled) == parse(blob)
#   payload is opaque to a HEADER parser. catches a parser reading
#   a length or a flag out of the data area.

# EQUIVARIANCE: changing X must change Y by a stated amount
after = parse(serialize_uf2(blocks_of(base) + blocks_of(added)))
assert after.payload_bytes == before.payload_bytes + 200 * extra
#   not "the total is 768". the relation holds whatever the
#   numbers are, so it survives the specimen changing.

# IDEMPOTENCE: twice is once
assert serialize_uf2(walk_uf2(once)) == once
Stating a bound without naming it test_clamping_is_monotonic_in_the_declared_size asserts that raising payloadSize can never lower the reported total, and that past some point it stops moving. That states the clamp without naming 476, so the test does not have to agree with the implementation about where the ceiling sits. It only has to be right that there is one. A test that repeats the implementation's constant is a test that cannot disagree with it.
Every relation needs a not-invariant companion All of the above is satisfied by a parser that ignores its input and returns a constant. Two tests in test_metamorphic.py exist only to fail against that: corrupting a magic number must raise, and changing payloadSize must move the total. This is the never-crash trap from §13 in a different costume, and it shows up wherever the assertions describe relationships rather than values.

Why this is the technique that survives an AI author

A relation is a claim about the problem, not about the code, so it cannot be transcribed from the implementation's behaviour. Ask a model what parse returns for a given blob and it can read the answer off the source. Ask it whether reordering blocks may change the payload total and it has to reason about UF2. See §19, and test_oracle_independence.py for what happens when the assertions do come from the implementation.

Two ideas worth stealing from other parser traditions

SourceWorkWhat transfers
NystromPratt Parsers: Expression Parsing Made Easythe precedence table is the grammar. The article is about parsing rather than testing, but the consequence for tests is direct: when the specification is a table, the test surface is a table, and parametrize is the natural fit rather than a convenience
HenkelmannScala Parser Combinators, Part 3sub-parsers are testable in isolation, so you test boolean, int and string separately before anything composes them. That is §01's seam argument arriving from a different language and paradigm, which is the strongest kind of confirmation

The walk/fold split at the top of this section is the same idea in a byte-parser: walk_uf2 is the sub-parser, parse_uf2 composes it, and both are reachable on their own. serialize_uf2 is what makes the pair invertible, which is what buys the round-trip relation.

Hostile Input, Fuzz & Properties

A parser's contract under bad input is narrow and absolute: for any byte string, parse() either returns an Image or raises ParseError. Nothing else. Not IndexError, not struct.error, not MemoryError, not a hang -- every one of those is the same defect wearing a different exception class, and every one reaches a client as a 500 with a stack trace in it.

   four techniques, and what each one is actually good at

   TRUNCATION SWEEP   every prefix of a valid file
                      cheap · exhaustive · finds more real bugs than the rest combined
                      an interrupted upload is the commonest corrupt file in the world

   BIT FLIPS          one byte changed, walking the header FIELD BOUNDARIES
                      12 named offsets do what a fuzzer needs a million iterations for

   SEEDED FUZZ        random garbage from a FIXED seed
                      reproducible from the test ID alone. no artifact to attach.

   PROPERTY-BASED     the machine finds the specimen, then SHRINKS it
                      1,847 bytes of noise → b'\x00' and the same traceback
The unseeded fuzz trap random.randbytes(n) with no seed gives a suite that fails once a fortnight with no way to reproduce it -- which trains everyone to re-run CI until it goes green. A fixed seed gives a fixed corpus that grows only when you decide it should, and growing it is a diff someone reviews.

Guided beats unguided, by three orders of magnitude

why pure random bytes prove almost nothing here@pytest.mark.parametrize("seed", range(8))
def test_valid_prefix_random_tail(seed):
    """The interesting shape: enough valid structure to get PAST sniff,
    then garbage.

    Pure random bytes almost never reach the walker at all -- they fail the
    magic check in the first four bytes and prove nothing about the code past
    it. This is why unguided fuzzing of a parser with a magic number spends
    99.9% of its budget on the same branch."""
    rng = random.Random(1000 + seed)
    tail = bytes(rng.randrange(256) for _ in range(512))
    _parses_or_raises_parse_error(VALID[:512] + tail)

Properties, in order of strength

PropertyShapeStrength
round-tripparse(build(x)) == xstrongest. Subsumes a dozen example tests
invarianta relationship holds for all inputsstrong. Cross-function ones are the best value
oraclefast impl agrees with obvious implstrong -- see §15
idempotencef(f(x)) == f(x)useful where it applies
never-crashno exception outside the declared setweakest. Where everyone starts
tests/test_property.py@COMMON
@given(blocks=st.integers(min_value=1, max_value=12),
       payload=st.integers(min_value=0, max_value=UF2_PAYLOAD_MAX))
def test_uf2_round_trip(blocks, payload):
    image = parse(build_uf2(blocks=blocks, payload_size=payload, family=RP2040))
    assert image.block_count == blocks
    assert image.payload_bytes == blocks * payload
    assert image.size == blocks * UF2_BLOCK_SIZE

@COMMON
@given(entry=st.integers(min_value=0, max_value=2**64 - 1),
       bit64=st.booleans(), little=st.booleans())
def test_elf_entry_round_trips_at_every_width(entry, bit64, little):
    # 32-bit ELF cannot hold a 64-bit entry point, so the precondition
    # is stated with `assume` rather than by narrowing the strategy --
    # hypothesis tracks rejections and complains if the filter is too
    # aggressive, which beats a silently narrower search.
    assume(bit64 or entry < 2**32)
    assert parse(build_elf(entry=entry, bit64=bit64, little=little)).entry == entry
Every never-crash property needs a positive companion test_parse_never_raises_anything_but_parse_error passes completely against def parse(b): raise ParseError("no"). A hostile-input suite asserts only that nothing explodes, and something that refuses everything never explodes. Put the companion assertion in the same file, so the pairing is visible:

def test_hostile_specimens_do_not_all_look_alike(): assert parse(VALID).block_count == 3

Resource exhaustion, without a timing assertion

the classic parser DoSdef test_a_declared_block_count_does_not_allocate():
    """A length field is trusted and pre-allocated. A two-block file
    claiming four billion blocks must cost two blocks of work.

    Asserted by wall-clock-free means -- drive the walker to completion and
    check the count -- because a timing assertion on a loaded CI runner is a
    flake generator."""
    image = parse(build_uf2(blocks=2, wrong_num_blocks=0xFFFFFFFF))
    assert image.block_count == 2
    assert image.declared_blocks == 0xFFFFFFFF

Guardrails: Proving a Refusal

A policy test has three obligations, and most suites discharge one.

   1. the rule FIRES on a violating artifact        everyone does this
   2. the rule does NOT fire on a compliant one      half of everyone
   3. the rule fires for the RIGHT REASON            almost nobody

   why three rots:

   an oversized UNSIGNED artifact is rejected by TWO rules.
   a test asserting "rejected" passes forever after the size check is
   accidentally deleted, because the signature check is still there.

        ┌─────────────────────────────────────────────┐
          evaluate() returns EVERY hit                 → can state #3
          enforce()  raises the FIRST by precedence    → what a client sees
        └─────────────────────────────────────────────┘

Precedence is behaviour, not dict ordering

fwvault/policy.pyPRECEDENCE = (
    "MALFORMED",          # parser said no, before policy is consulted
    "OVERSIZE",
    "EMPTY_PAYLOAD",
    "UNKNOWN_FAMILY",
    "DENIED_MACHINE",
    "NOT_MAIN_FLASH",
    "REVOKED_KEY",        # a STRONGER statement than unsigned...
    "UNSIGNED",           # ...and must not be masked by it
    "TOO_MANY_WARNINGS",
)

# codes are a PUBLIC CONTRACT. Clients branch on `code`, never on
# `detail`, so the codes are frozen in test_contract.py while the
# wording of `detail` stays free to improve.
the assertion a first-hit design cannot makedef test_every_applicable_rule_fires_at_once(policy):
    """One artifact, five independent violations, five codes.

    If OVERSIZE stops firing, this list gets shorter and the test fails BY
    NAME -- rather than staying green because UNSIGNED still rejects the
    same file."""
    image = parse(build_uf2(blocks=2, family=UNKNOWN_FAMILY, payload_size=0))
    tight = replace(policy, max_bytes=100)

    assert codes(image, UNSIGNED, tight, flags=UF2_FLAG_NOT_MAIN_FLASH) == [
        "OVERSIZE", "EMPTY_PAYLOAD", "UNKNOWN_FAMILY",
        "NOT_MAIN_FLASH", "UNSIGNED",
    ]

The line the whole service is built around

   three kinds of bad thing, and conflating any two is how a service
   starts lying about its inputs

   ParseError        the blob is malformed        a fact about the ARTIFACT
                                                  → 422 MALFORMED, with an offset

   PolicyRejection   parsed fine, we refuse it    a DECISION
                                                  → 422 <CODE>

   VaultUnavailable  we could not tell            a fact about US
                                                  → 503 + Retry-After. NEVER a rejection.

   VaultUnavailable is not a subclass of PolicyRejection and never
   converts into one. A signing oracle that times out and is reported
   as "unsigned" turns an outage into a stream of confident refusals,
   and the refusals look exactly like the real ones.
tests/test_asgi_raw.py: the headline assertiondef test_an_oracle_outage_is_503_and_never_a_rejection(vault, policy, uf2):
    class DeadClient:
        def verify(self, digest):
            raise VaultUnavailable("oracle down")

    app = create_app(store=vault, client=DeadClient(), policy=policy)
    status, headers, body = call(app, "POST", "/artifact", uf2)

    assert status == 503
    assert body["error"] == "ORACLE_UNAVAILABLE"
    assert headers[b"retry-after"] == b"5"
    assert len(vault) == 0, "nothing is stored when we could not verify it"

The meta-test that keeps the guardrails complete

no code in PRECEDENCE may go untesteddef test_every_rejection_code_is_exercised_somewhere():
    """Implemented by RUNNING the suite's own specimens rather than by
    scraping source text, so it cannot be satisfied by a code appearing in a
    comment.

    When you add a rule, this fails until you add a case -- which is the only
    reliable way a guardrail suite stays complete as the policy grows."""
    seen = set()
    for image, verdict, pol, flags in specimens:
        seen.update(codes(image, verdict, pol, flags))

    untested = set(PRECEDENCE) - seen - {"MALFORMED"}
    assert not untested, (
        "these policy codes are never produced by any specimen in the suite, "
        "so nothing would notice if the rule stopped firing: "
        + ", ".join(sorted(untested)))

Path safety: regenerate, do not sanitise

   the traversal test everyone writes, and why it is not the fix

   "../../etc/passwd"    "..%2f..%2fetc"    ".../....//"    "\\..\\..\\"

   this list can never be complete. There is always another encoding.

   so the design does not rely on it:

     the key is not SANITISED, it is REGENERATED.
     `put()` hashes the bytes itself and ignores whatever the
     client called them. `has()` on a hostile string is just a miss.

   the parametrized sweep still exists -- but it documents the threat,
   it does not implement the defence. And it gets a companion:

     def test_the_traversal_test_is_not_vacuous(vault, uf2):
         # if has() returned False for EVERYTHING, the whole sweep
         # passes against a store that does not work.
         digest, _ = vault.put(uf2, manifest_for(uf2))
         assert vault.has(digest) is True

Differential & Oracle Tests

When a parser gets complicated enough that you cannot write down the expected output by hand, write a second, dumber implementation and compare them. The naive one is obviously correct and much too slow; the real one is fast and subtle.

   the wiring

                    ┌────────────────────┐
      specimen ──┬──►  walk_uf2()          ├──► blocks  ─┐
                    fast, subtle                   
                   └────────────────────┘            ├──► == ?
                   ┌────────────────────┐            
                 └──►  _reference_walk()   ├──► blocks  ─┘
                     naive, obvious     
                    └────────────────────┘

   both OUTCOMES are compared, not just both outputs.
   agreeing on the block list is not enough if one raises where the
   other does not.

   the ORACLE variant: same shape, but the reference is a trusted
   external tool -- your parser vs readelf, your decoder vs ffmpeg --
   and the corpus is real files instead of generated ones.
Rule 1: the reference must be independent If it shares helpers with the code under test, a bug in the shared part cancels out and the suite goes green against two identical mistakes. This repo asserts that structurally -- it reads its own source and checks the reference borrows nothing:

for borrowed in ("UF2_MAGIC", "from fwvault", "parse.", "Block("): assert borrowed not in source

Crude, and it catches the refactor where someone "removes the duplication" between the two implementations and silently deletes the reason the test exists.
tests/test_differential.py# one test per specimen, NAMED by specimen. Adding a corpus entry adds
# a test with no assertion to write -- the whole economics of this.

CORPUS = {
    "one-block":         build_uf2(blocks=1),
    "many-blocks":       build_uf2(blocks=17),
    "no-family":         build_uf2(blocks=2, family=None, flags=0),
    "zero-payload":      build_uf2(blocks=3, payload_size=0),
    "max-payload":       build_uf2(blocks=3, payload_size=476),
    "oversized-payload": build_uf2(blocks=2, oversized_payload=5000),
    "backwards-addr":    build_uf2(blocks=3, addr_step=-256),
    "bad-end-magic":     build_uf2(blocks=3, bad_end_magic=2),
    "ragged":            build_uf2(blocks=2) + b"\x00" * 3,
    "empty":             b"",
}

@pytest.mark.parametrize("name", sorted(CORPUS), ids=sorted(CORPUS))
def test_the_walker_agrees_with_the_reference(name):
    ours,   our_error   = _run(walk_uf2, CORPUS[name])
    theirs, their_error = _run(_reference_walk, CORPUS[name])

    assert (our_error is None) == (their_error is None), (
        "specimen {!r}: one implementation raised and the other did not "
        "(ours={!r}, reference={!r})".format(name, our_error, their_error))
    if our_error is None:
        assert ours == theirs, "specimen {!r}: block streams differ".format(name)
Rule 2: and the corpus needs a guard too A corpus where every specimen raises makes the differential test pass trivially -- both implementations agree that everything is broken. So test_the_corpus_contains_both_outcomes asserts the corpus straddles the boundary. Every meta-check on this page has this shape: the filter must be shown firing at least once.

Differential also works at a higher level than the walk. The reference computes the payload total the obvious way; parse accumulates it during the walk with a clamp. Two routes to one number, and no expected value written down anywhere:

folds agree toodef test_the_folds_agree_on_payload_total():
    blob = build_uf2(blocks=6, payload_size=300, family=RP2040)
    expected = sum(min(b["payload_size"], 476) for b in _reference_walk(blob))
    assert parse(blob).payload_bytes == expected

CLI & Subprocess

Two ways to test a command line, and you want both. The shape that makes the fast way possible costs nothing and is the highest-leverage decision in a CLI's design.

   def main(argv=None, stdout=None, stderr=None, transport=None) -> int

     · takes argv                    does not read sys.argv
     · returns an int                does not call sys.exit
     · writes to streams it was HANDED

   ────────────────────────────────────────────────────────────────

   IN-PROCESS    main(["inspect", path])          microseconds
                 every branch · real tracebacks · the coverage lives here

   SUBPROCESS    python -m fwvault inspect ...    milliseconds each
                 a handful of cases · "works on my machine" dies here

   in-process CANNOT see:
     · whether the package imports from a CLEAN interpreter
     · whether the console entry point exists
     · what the SHELL sees as the exit code
     · stream buffering, encoding, line endings
     · a stray print() somewhere in the import path corrupting your JSON

Exit codes are a contract

CodeMeansBecause
0OK
1ERRORcould not read the file, unexpected
2USAGEargparse's own convention
3REJECTEDpolicy said no
4MALFORMEDthe artifact is bad
5UNAVAILABLEour infrastructure is down
3, 4 and 5 have to be different numbers A build script must be able to tell "your firmware is bad" from "our signing oracle is down". One exit code for both means a red build teaches nobody anything, and the on-call person pages the wrong team. test_every_exit_code_is_distinct is a three-line test that catches the copy-paste.
in-process, with streams we owndef run(argv, transport=None):
    # explicit StringIOs rather than capsys: capsys also captures pytest's
    # own output and anything a library logs, which makes an exact-match
    # assertion on stdout fragile.
    out, err = io.StringIO(), io.StringIO()
    code = cli.main(argv, stdout=out, stderr=err, transport=transport)
    return code, out.getvalue(), err.getvalue()


def test_findings_go_to_stderr_and_data_to_stdout(tmp_path):
    # so `fwvault --json inspect x | jq` works while warnings stay visible.
    # mixing them means the pipe eats the warnings or the JSON parse
    # fails -- and which one it is depends on the day.
    _code, out, err = run(["--json", "inspect", str(path)])
    json.loads(out)                     # stdout is pure JSON
    assert "warning:" in err
the real process, four casesdef _module_run(args, **kwargs):
    env = dict(os.environ, PYTHONPATH=os.path.join(REPO, "src"))
    return subprocess.run(
        [sys.executable, "-m", "fwvault"] + args,
        capture_output=True, text=True, env=env, cwd=REPO,
        timeout=30,          # ← a test that CAN hang needs a timeout on the CALL
        **kwargs)

def test_stdout_stays_parseable_through_a_real_pipe(artifact):
    # catches a stray print() in the import path, which corrupts the JSON
    # for every downstream consumer and is completely invisible to capsys.
    result = _module_run(["--json", "inspect", artifact])
    assert json.loads(result.stdout)["kind"] == "uf2"
python -I implies -E Isolated mode ignores the user site directory and PYTHONPATH. A cold-start import test that passes the path via the environment silently gets an empty sys.path and fails looking exactly like a packaging bug. Put the path in the program instead:

[sys.executable, "-I", "-c", "import sys; sys.path.insert(0, SRC); import fwvault"]

The cheapest documentation test there is

walk the parser, assert every flag has helpdef test_every_subcommand_has_help_text():
    parser = cli.build_parser()
    subparsers = [a for a in parser._actions
                  if hasattr(a, "choices") and isinstance(a.choices, dict)]
    assert subparsers, "no subcommands found; did the parser shape change?"

    missing = ["{} {}".format(name, arg.dest)
               for a in subparsers for name, sub in a.choices.items()
               for arg in sub._actions
               if arg.dest != "help" and not arg.help]
    assert not missing, "undocumented flags: " + ", ".join(missing)

# a flag with no help is a flag nobody can use, and it is invisible
# to every behavioural test in the file.

Meta-Tests: Contract, Lean Install, Hygiene

A test suite is code, it rots like code, and it rots in a way ordinary code does not: silently, and in the direction of passing. A test that stops asserting anything still shows up green. A test that skips on every machine still counts toward "1,204 passed".

   the tier above the tests

   CONTRACT       the SHAPE you publish, pinned
                  __all__ · rejection codes · exit codes · schema version
                  · the route table · the response envelope
                  invisible to every behavioural test

   LEAN INSTALL   the dependency boundary, two ways
                  static: AST-walk for module-level optional imports
                  behavioural: sys.modules[name] = None, then run it

   HYGIENE        the suite as DATA
                  every test asserts · every skip explains itself
                  · no fixed paths · no path outside the repo
                  · no silently disabled test · no duplicate names

   and the rule that keeps this tier honest:
   every check must be SHOWN FIRING at least once.
   a filter with a broken pattern passes everything, forever.

Contract: what a behavioural test cannot see

tests/test_contract.pydef test_the_public_api_is_exactly_what_is_documented():
    # both directions matter. A name that disappeared breaks importers;
    # a name that APPEARED is a promise nobody meant to make, and it is
    # much harder to withdraw a year later.
    assert set(fwvault.__all__) == EXPECTED_API

def test_rejection_codes_are_frozen():
    # clients branch on these strings. Renaming one is a breaking change
    # whether or not anything else in the suite notices.
    #
    # `detail` is deliberately NOT pinned -- the wording should be free
    # to improve, and a test that asserts on prose fails every time
    # someone fixes a typo.
    assert PRECEDENCE == ("MALFORMED", "OVERSIZE", ...)

def test_the_shipped_doubles_match_the_transport_protocol():
    # a fake whose signature drifts from the real transport passes every
    # test in this repo and breaks in every consumer.
    real = inspect.signature(UrllibTransport.request)
    for double in (RecordingTransport, ScriptedTransport):
        assert inspect.signature(double.request) == real, double.__name__

Lean install: the boundary, asserted twice

static: reads source, so it reports EVERY offender in one rundef _module_level_imports(path):
    tree = ast.parse(open(path, encoding="utf-8").read())
    names = set()
    for node in tree.body:          # TOP LEVEL only -- lazy imports are the contract
        if isinstance(node, ast.Import):
            names.update(a.name.split(".")[0] for a in node.names)
        elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
            names.add(node.module.split(".")[0])
    return names

# a test that fails with "fix this, then run me again to find the next
# one" wastes an afternoon.
behavioural: one test per package, so a failure NAMES which one@pytest.mark.parametrize("absent", sorted(OPTIONAL), ids=sorted(OPTIONAL))
def test_the_core_imports_with_every_optional_package_missing(absent, monkeypatch):
    monkeypatch.delitem(sys.modules, absent, raising=False)
    monkeypatch.setitem(sys.modules, absent, None)
    for name in ("fwvault", "fwvault.parse", ...):
        monkeypatch.delitem(sys.modules, name, raising=False)

    assert importlib.import_module("fwvault").parse is not None

# the alternative is a second CI job with a different lockfile, which
# is real work and gets disabled the first time it goes red on a Friday.

Hygiene: treat the test tree as data

tests/test_suite_hygiene.pydef test_every_test_asserts_something():
    for path in TEST_FILES:
        for name, node in find_tests(path):
            has_assert  = any(isinstance(n, ast.Assert) for n in ast.walk(node))
            has_raises  = # .raises / .warns / .approx
            calls_check = # a helper named assert_*
            if not (has_assert or has_raises or calls_check):
                offenders.append("{}:{} {}".format(path.name, node.lineno, name))

    assert not offenders, (
        "these tests assert nothing, so they pass whatever the code does:\n  "
        + "\n  ".join(offenders))
Every check must name the file and line it objects to A hygiene test that fails with a bare assert not offenders is worse than no hygiene test: it tells you something is wrong and hides what. This one prints test_property.py:84 test_parse_never_raises_anything_but_parse_error -- and that is exactly how it caught two real assertionless tests in this repo while it was being written.

Scanning source without flagging your own prose

the false-positive fixdef code_lines(path):
    """Every line that is CODE: no comments, no docstrings.

    String LITERALS stay in, and that distinction is the whole point. The
    banned things these checks look for -- "/tmp/", "expanduser" -- appear in
    real code AS string literals, so a scanner that strips every string can
    never see the defect. It also has to strip docstrings, or it flags the
    paragraph explaining the rule."""
    skip = set()
    for token in tokenize.generate_tokens(io.StringIO(source).readline):
        if token.type == tokenize.COMMENT:
            skip.update(range(token.start[0], token.end[0] + 1))

    for node in ast.walk(ast.parse(source)):
        # a docstring is exactly an Expr whose value is a str Constant
        if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) \
                and isinstance(node.value.value, str):
            skip.update(range(node.lineno, (node.end_lineno or node.lineno) + 1))

    for lineno, line in enumerate(source.splitlines(), 1):
        if lineno not in skip:
            yield lineno, line
A checker whose false positives land on its own documentation gets disabled within a week The first version of this scanned raw lines and skipped anything starting with # or a quote. That is wrong for the second line of a docstring, and it flagged three passages of prose that were describing the thing being banned. The second version stripped all strings -- and could no longer see the path literals it was hunting. Neither is subtle in hindsight; both shipped green.

Distinguish a constant from shared state

mutable module-level state makes test ORDER load-bearing# flag only names something actually MUTATES. A frozen set of expected
# API names assigned once is a constant, and flagging it teaches people
# the check is noise.

MUTATORS = {"append", "add", "extend", "update", "insert",
            "pop", "remove", "clear", "setdefault"}

# CONSTANT = {'a', 'b'}   → not flagged
# STATE = []             → flagged, because STATE.append(1) exists

Coverage, Mutation & CI

Coverage answers "did this line run". It cannot answer the question you actually have.

   100% line coverage is compatible with ZERO assertions.

   def test_everything():
       parse(build_uf2(blocks=3))
       evaluate(image, verdict, policy)
       store.put(blob, manifest)
       # no asserts. every line green.

   ─────────────────────────────────────────────────────────────

   line coverage     did this line execute
   branch coverage   did BOTH sides of this `if` execute
                     --cov-branch. line coverage calls a half-tested
                     conditional covered, which is most of them.
   mutation score    if I BREAK this line, does anything go red

   only the third one is a question about your TESTS. the first two
   are questions about your test RUN.
Coverage is a map, not a score Read it to find the branch nobody exercises, then go and write a test with a real assertion about it. A --cov-fail-under threshold is a number people game, and the cheapest way to game it is a test with no assertions. There is no threshold in this repo's CI for exactly that reason.

Mutation testing: break it on purpose

   the loop

   for each small edit the tool can make:
           (x < y  →  x <= y,  return v  →  return None,  and  →  or)
        
     apply it to the source
        
     run the whole suite
        
     suite goes RED    mutant KILLED    the behaviour is checked
     suite stays GREEN  mutant SURVIVED  a hole. executed, unchecked.

   the tools are mutmut and cosmic-ray. both rewrite your source and
   run the suite once per mutant, so they take minutes to hours.
   run one occasionally. never in CI.

A mutation score is never 100%, and the reason has a name. Some edits cannot be detected because they do not change behaviour at all:

Equivalent mutants Writing this page's mutation file, the first attempt at "PRECEDENCE gets alphabetised" survived. Alphabetical order happens to put OVERSIZE first as well, for the specimen that was chosen, so the mutant changed nothing observable and looked like a hole in the suite. It was not: it was an equivalent mutant. OVERSIZE + EMPTY_PAYLOAD is the pair that actually distinguishes the two orderings. Real tools produce these constantly, and telling them apart from real holes is the manual work mutation testing costs you.

The cheap version, which does belong in the suite

Hand-written mutants, each paired with the assertion that should catch it. Milliseconds, lives next to the code, and it fails the day someone weakens a check. It cannot find the mutants nobody thought of, so it does not replace the real tool. It pins the checks that matter most.

tests/test_mutation.py# A mutant is KILLED if the suite goes red, and a test that ERRORS is red
# just like a test that fails. So any exception counts.
#
# pytest.fail.Exception has to be listed separately: a failing
# pytest.raises(...) raises Failed, which derives from BaseException
# rather than Exception, so a bare `except Exception` misses it.
CHECK_FAILURES = (Exception, pytest.fail.Exception)


def assert_caught(check, what):
    """Run a real suite assertion against a mutant and require it to go RED."""
    try:
        check()
    except CHECK_FAILURES:
        return
    raise AssertionError(
        "MUTANT SURVIVED: {}. The suite executes this behaviour but does "
        "not check it.".format(what))
the mutant this whole service is shaped arounddef test_an_outage_rendered_as_unsigned_is_caught(clock):
    # a "helpful" refactor catches VaultUnavailable so the pipeline does
    # not blow up. the service keeps running, the rejections look real,
    # and nobody finds out until someone asks why every build failed.
    class ForgivingClient(SigningClient):
        def verify(self, digest):
            try:
                return super().verify(digest)
            except VaultUnavailable:
                return Verdict(signed=False)          # ← the mutation

    dead = ScriptedTransport([TransportError("down")] * 3)
    client = ForgivingClient("https://x", transport=dead, clock=clock, retries=3)

    def the_suite_assertion():
        with pytest.raises(VaultUnavailable):       # from test_doubles.py
            client.verify("0" * 64)

    assert_caught(the_suite_assertion, "an outage rendered as Verdict(signed=False)")
MutantWhat survives itCaught by
OVERSIZE stops firingevery test, under a first-hit-only policy designthe whole-list assertion
evaluate returns hits[:1]every single-rule testthe five-code assertion
PRECEDENCE reorderedevery single-rule testthe enforce() code check
outage becomes UNSIGNEDthe 422 path, the store, the API shapethe raises(VaultUnavailable)
stale flag droppedthe availability testthe separate honesty test
magicEnd check deletedevery valid-file testthe located-offset test
clamp warning droppedevery arithmetic testthe paired warning assert
parse() refuses everythingthe entire hostile-input filethe positive companion
Read the last row again Every assertion in test_hostile.py passes against def parse(b): raise ParseError("no"). That is 40-odd tests, a truncation sweep, twelve bit flips and twenty seeds, all green, against a parser that does nothing. One positive assertion in the same file is what stands between you and that.

CI: two jobs, and the first one is the point

.github/workflows/tests.ymljobs:
  bare:                          # pytest and NOTHING else
    steps:
      - run: pip install pytest
      - run: python -m pytest -q

  full:
    strategy:
      fail-fast: false            # one red cell must not hide the others
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ["3.11", "3.13"]
    steps:
      - run: pip install -e ".[test]"
      - run: python -m pytest -q

  coverage:
    steps:
      - run: python -m pytest -q --cov=fwvault --cov-branch --cov-report=term-missing
Why bare exists It proves a clone with nothing but pytest still runs the suite, which is the claim test_lean_install.py asserts from the inside. If the extras were installed everywhere, that claim would be untested in the one environment that could disprove it. The same logic runs Windows in the matrix: expanduser, path separators and os.replace semantics all differ, and every one of them has produced a green local run and a red CI run in some project you have worked on.
The gap CI cannot close Your fake signing oracle encodes what you believe the real one does. When that belief goes stale, the fake keeps agreeing with you and every job above stays green against a service that no longer exists. Nothing in this pipeline can detect it. The answers are a contract test run against the real thing on a schedule (schemathesis against its OpenAPI document, or a consumer-driven contract), plus the net-marked test that at least keeps the gap named. Pick one, or accept the gap knowingly. Do not let a green matrix persuade you it is not there.

Testing an AI Author

The argument goes: nobody reads their compiler's assembly output, so nobody will read generated code either, and the objection is sentimental. That argument is better than its critics allow and worse than its advocates think. This section takes it seriously, then takes it apart, then says what the repo does about it.

   the position, in two moves

   Su          lights-out codebases: code no human reads or edits.
               volume makes review impossible, and the chess analogy
               says it will look like a category error to have tried.

   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 where it matters.

               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.
The distinction worth keeping "Nobody reads compiler output" is false. Compiler Explorer is beloved and compiler engineers live in it. The true claim is narrower: reading it is not the quality gate. Read generated code when you are curious or debugging. Do not make reading it the thing standing between you and production, because at volume that is not a gate, it is a queue.

Where the analogy breaks

PropertyCompilerAgent
determinismsame source, same flags, same binary. Reproducible builds are a security property people fought forsame prompt twice, different code. No artifact to pin, no hash to compare, nothing to bisect
who owns the specthe source is the specification, and a human wrote it. The compiler is a semantics-preserving translator with no licence to inventthe prompt is the specification. It is ambiguous natural language, and the agent's job is precisely to invent the parts you did not say
how it is verifiedCompCert has a machine-checked proof of semantic preservation. Alive2 does translation validation on LLVM passes. Csmith finds bugs by differential testing across toolchainsa suite somebody wrote, often the same somebody that wrote the code
trusting trustThompson's compiler can backdoor itself and hide the evidence in its own source. The answer is diverse double-compilinga model that writes both the code and the tests for that code has exactly that structure. The answer is provenance separation
The bill the analogy quietly runs up "We verify compilers with tests, not by reading" is true and expensive. It means CompCert, Alive2, Csmith, decades of formal methods, and a specification precise enough to prove against. Anyone invoking the compiler analogy is invoking that apparatus. Almost nobody invoking it has costed it.

The numbers

Every figure below is from a paper, and each was checked against the abstract rather than taken from a summary. Sources at the end of the section.

WhatFindingSource
coverage vs mutationA suite can reach 100% coverage at a 4% mutation score. Quoted in MUTGEN's abstract as the motivating example for mutation-guided generationarXiv 2506.02954
the benchmark gapOn 3,909 real-world Python functions, LLM test generation averages 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: a usable comparative signal in regression scenarios, unreliable when the code under test may already be buggy. Suite size is not the dominant confounder for LLM-generated suites that it is for human-written onesarXiv 2607.22880
self-preferenceEvaluators recognise and favour their own generations, scoring them higher than human annotators do. Self-recognition correlates linearly with the strength of the bias, and the causal reading survives the obvious confoundersarXiv 2404.13076
why it happensThe bias tracks perplexity: models rate low-perplexity text higher regardless of who wrote it. Familiar-looking, not betterarXiv 2410.21819
scale does not fix itAdvanced capability is uncorrelated, sometimes negatively correlated, with low self-preference biasarXiv 2604.22891
self-correctionModels struggle to self-correct without external feedback, and performance sometimes degrades after tryingarXiv 2310.01798
provenance effectModels correct errors from external sources but not the same errors in their own traces. Relabelling identical content as external restores the abilityarXiv 2606.05976
adversarial pairOne agent writes tests, one writes mutants, adversarially, gated on coverage and mutation scorearXiv 2602.08146
search plus LLMLLM-seeded population plus evolutionary search, about 10% better on both coverage and mutation score than either alonearXiv 2505.12424
The two that matter most, together The benchmark gap (92% on TestEval, 41% on real functions) says generated tests are much worse than the headline suggests. 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 or a larger model, 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,
   both halves share the                    but now a wrong belief
   same wrong belief                        has to be held twice

   tests/test_oracle_independence.py runs exactly this. A parser and
   its tests, both built on one plausible wrong number: the pair is
   self-consistent, green, and wrong. Only the spec catches it.

   no model is involved in that file. 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.

What this repo measures

tests/test_ai_authored.py puts two suites over the same twelve specimens. One is written in the shape a model produces: sweep the inputs, assert only that nothing escaped the declared exception. The other asserts what each specimen should actually produce. Coverage is counted with a twenty-line sys.settrace tracer so the number owes nothing to a plugin.

measured, not asserted from memory                 lines covered   mutants killed
ai-authored           93            0 / 5
asserting             93            5 / 5
                 # identical coverage. a --cov gate cannot
                 # tell these two files apart.

Three of the five mutants return a plausible Image with the wrong contents rather than raising. That is the failure an intake service actually ships: not a crash, an answer that is quietly false. A never-crash suite rewards it, because refusing to raise is exactly what it checks for.

Eight things to do about it

MitigationWhat it meansWhere it already lives
mutation gateAdmit a generated suite only if it kills a threshold of mutants. A test that kills nothing is not a testtest_mutation.py
positive companionEvery hostile-input assertion needs one, or a parser that refuses everything passes the filetest_hostile.py, §13
provenance separationWhatever wrote the code does not grade it, and ideally is not the same familytest_oracle_independence.py
differentialCheck against an independent implementation, not against the author's own expectationtest_differential.py, §16
properties over examplesA human can review five properties. A human cannot review five hundred generated examplestest_property.py, §13
meta-testsSuites rot in the direction of passing, and a generator will help themtest_suite_hygiene.py, §17
metamorphic relationsFor when you cannot state the answer but can state what must stay truetest_metamorphic.py, §12
equivalent mutantsThe honest cost. Telling them from real gaps is manual work that does not go away§18
Properties over examples is the strongest practical recommendation here Examples are cheap for a model to emit and expensive for a human to validate. Properties invert both costs. @given(blob=st.binary()) plus one stated invariant is reviewable in ten seconds and covers more than a page of generated cases. If you adopt one thing from this section, adopt that.

Where it runs out

   a test can check that the code does what the test says.
   nothing can check that the test says what was meant.

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

   the mitigations above all live to the RIGHT of the spec. every one
   of them assumes the tests encode the intent. none of them can check
   that they do.

   the only defence is that the spec is small enough to be read.

This is the one place the apparatus genuinely runs out, and it is 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 actually wanted.

The apparatus is the deliverable Reviewing generated code line by line is neither possible at volume nor the point. What replaces review has to be engineered on purpose, and that engineering is the work. This repo is one small worked example of it: the mutation gate, the independent oracle, the positive companion and the meta-tests are not fussiness, they are the parts of the apparatus that a generated suite most needs and least provides.

Sources

HandleReferenceWhere
SuNo More Code Reviews: Lights-Out Codebases Aheadmolochinations.substack.com
VenturiniTreat Agent Output Like Compiler Outputskiplabs.io/blog/codegen_as_compiler
MUTGENWang, Xu, Briand, Liu. Mutation-Guided Unit Test Generation with a Large Language Model, IEEE TSEarXiv 2506.02954
ULTHuang, Zhang, Harman, Zhang, Du, Ng. Benchmarking LLMs for Unit Test Generation from Real-World FunctionsarXiv 2508.00408
replicabilityZhao, Zhou, Cohen. Do Coverage and Mutation Scores of LLM-Generated Test Suites Correlate with Their Effectiveness?arXiv 2607.22880
self-preferencePanickssery, Bowman, Feng. LLM Evaluators Recognize and Favor Their Own GenerationsarXiv 2404.13076
perplexityWataoka, Takahashi, Ri. Self-Preference Bias in LLM-as-a-JudgearXiv 2410.21819
quantifying itYang, Hu, Qiu, Deng, Jiao, Zhou. Quantifying and Mitigating Self-Preference Bias of LLM JudgesarXiv 2604.22891
self-correctionHuang, Chen, Mishra, Zheng, Yu, Song, Zhou. Large Language Models Cannot Self-Correct Reasoning Yet, ICLR 2024arXiv 2310.01798
role relabelingChen, Su, Lin, Li, Chiang. The Self-Correction IllusionarXiv 2606.05976
AdverTestChang, Fang, Chen, Shi, Shen, Gu. Test vs Mutant: Adversarial LLM Agents for Robust Unit Test GenerationarXiv 2602.08146
EvoGPTBroide, Stern, Mordoch. EvoGPTarXiv 2505.12424
the apparatusThompson, Reflections on Trusting Trust; Leroy, CompCert; Lopes et al, Alive2; Yang et al, Csmithwhat the analogy borrows

Running It

the repo# everything a fresh clone can run -- no install, no dependencies
python -m pytest -q                     # 424 passed, 3 skipped

# plus hypothesis, httpx, starlette
pip install -e .[test]
python -m pytest -q                     # 433 passed, 2 skipped

# the tests that make real connections, off by default
python -m pytest --runnet

Flags worth muscle memory

FlagDoes
-q / -vquieter / one line per test with its ID
-xstop at the first failure
--lf / --fflast-failed only / failed-first. The debug loop
-k 'magic and not elf'select by name expression
-m 'not net'select by marker
--collect-onlywhat WOULD run. The first thing to try when a test does not
-rs / -raskip reasons / all non-pass reasons
-lshow local variables in tracebacks
--tb=short / =line / =notraceback verbosity
--pdbdrop into the debugger at the failure
--setup-showprint every fixture setup and teardown. The scope debugger
--durations=10the ten slowest tests. Run it monthly
-p no:randomlydisable a plugin for one run

Plugins, and whether you need them

PluginForVerdict
pytest-covcoverageyes, but read it as a map of what is untested, never as a score
pytest-xdist-n 4, parallelyes. Also the cheapest order-dependence detector you own
hypothesisproperty-basedyes, for parsers and anything with an inverse
anyio / pytest-asyncioasync testsone of them, if you have async code. Not both
pytest-randomlyshuffles orderuseful, occasionally infuriating, which is the point
freezegun / time-machinefreeze the clockonly where you cannot inject one. See §10
respx / responsesHTTP mockingonly if you cannot pass a transport. You almost always can
pytest-mocka mocker fixturea thin wrapper over unittest.mock. monkeypatch is already there
Coverage is a map, not a score 100% line coverage is compatible with zero assertions. Use coverage to find the branch nobody exercises, then go and write a test with a real assertion about it -- and remember the meta-test lesson: a test that only proves nothing crashed is satisfied by a function that refuses everything.

The order this repo would teach it in

   1  make the code have seams          every collaborator a parameter
   2  write specimen BUILDERS           not a committed corpus
   3  example tests, with good IDs      §04, §05
   4  a FAKE for each seam              §07, §09 -- ship it in the package
   5  the negative cases                the clean specimen that must NOT warn
   6  hostile input                     §12 -- truncation first, it is cheapest
   7  contract tests                    §16 -- before your first release
   8  hygiene tests                     §16 -- before the suite hits 100 files

   patching comes LAST, and only for things you do not own.
The whole page in four lines Inject what you own, patch what you do not. Prefer a fake to a mock. Every never-crash test needs a positive companion. And a check nobody has seen fire is a check nobody should trust.
// orientation · terms · collection · fixtures · parametrize · assertions · markers · doubles · monkeypatch · transports · asgi · parsers · hostile · guardrails · differential · cli · meta · mutation · ai · running //