Test Template — the tests every surface must have
Companion to
TESTING-CONSTITUTION.md. The constitution says what the loop is (spec → approve → test → code); this is the checklist so you never have to remember which tests to write. Copy the “Per-surface matrix” into a spec’s Test matrix section, then create the files. Thecheck_specs_have_tests.pyguardrail keeps the spec and its tests bound together.
When you start a surface (a view, a feature, an endpoint — e.g. the Library icon view, the KG tables), you owe tests across the legs the surface touches. Not every surface touches every leg; the matrix says which, and each leg below is a fill-in-the-blank skeleton grounded in the real harness so writing it is mechanical.
Per-surface matrix (copy into the spec’s Test matrix section)
| Leg | Required when… | Pins | Lives in |
|---|---|---|---|
| Pure rule (Swift) | there is any non-trivial logic (a filter, a mapping, a state machine) | the rule, off-main, no UI | fichero/Tests/Unit/**/<Area>Tests.swift |
| Availability (Swift) | a capability must be REACHABLE in a surface | “this surface wires this capability” | same, source-string via AppSource.root() |
| Visual / A11y (Apple tools) | a surface has UI to verify | performAccessibilityAudit() passes + .xctestplan captures a screenshot (no pixel-diff) |
in the Click-around leg below (fichero/Tests/UI/**) |
| Backend (pytest) | the surface calls an endpoint / writes data | the endpoint’s contract + that data was DELIVERED | fichero-server/tests/** |
| MCP | the capability should be agent-reachable | the MCP tool maps + routes | fichero-mcp/tests/test_mcp_full.py |
| CLI | the capability should be scriptable | the CLI command wires the endpoint | fichero-cli/tests/test_*.py |
| Click-around (XCUITest, Mac) | a user drives it in the UI | the end-to-end path: click → effect | fichero/Tests/UI/** (subclass FicheroUISessionTests) |
| iPhone (iOS) | the surface ships on iPhone | the touch path exists on iPhone | fichero/Tests/UI/ios + the fichero-ui-ios plan |
| iPad | the surface ships on iPad | the touch path exists on iPad (the split/regular-width IA) | fichero/Tests/UI/ipad + the fichero-ui-ipad plan |
| Load (#4634) | the surface acts on many rows | bounded, no peg, timed | fichero-server/tests/perf/** |
Hard-gate (Testing Constitution Art. 4): the cross-surface invariant (same result from backend, MCP, CLI, UX) and capability availability are hard-gates. The rest is tracked debt — but the matrix is how you SEE the debt instead of forgetting it.
Skeletons
1. Pure rule (Swift, off-main)
@testable import Fichero
import XCTest
/// spec: <area> — `<behavior.id>`.
final class <Area>RuleTests: XCTestCase {
func testRule() {
XCTAssertEqual(<Type>.<pureFunc>(<input>), <expected>)
}
}
2. Availability (Swift — “the capability is present in this surface”)
private static func appSource(_ p: String) throws -> String {
try String(contentsOf: AppSource.root().appendingPathComponent(p), encoding: .utf8)
}
func testSurfaceWiresCapability() throws {
let src = try Self.appSource("Views/<Path>/<View>.swift")
XCTAssertTrue(src.contains("<the wiring token>"), "<surface> must wire <capability>")
}
2b. Visual + accessibility (Apple tools — no pixel-diff)
There is no pixel-diff snapshot layer — Apple ships no snapshot assertion and we don’t
hand-roll one (the old SnapshotSupport.swift was deleted; see
specs/testing/ui-testing-strategy.md). Visual/a11y verification uses Apple’s own tools, inside
the XCUITest leg (§ Click-around):
- Accessibility correctness: each XCUITest flow ends with try app.performAccessibilityAudit()
— Apple-first-party, catches missing labels/identifiers + contrast on every destination.
- Doc screenshots: the .xctestplan captures screenshots per run (Apple’s “keep all”), and
Xcode RenderPreview captures a surface on demand. Capture (for review-by-eye + the guides), NOT
auto-diff. Copy the capture into docs/assets/<milestone>/ to satisfy the Documentation
matrix’s user-manual screenshot — no separate screenshot step. See DOC-TEMPLATE.md → user-manual leg.
3. Backend (pytest — prove DELIVERY, not just a 200; see check_import_tests_prove_delivery)
def test_<verb>_persists(client, db):
r = client.post("/api/<route>", json={...})
assert r.status_code == 200
assert r.json()["id"] # evidence something was delivered
assert db.get(<Model>, r.json()["id"]) is not None
4. MCP (mirror test_mcp_full.py: a fake client records the call)
def test_<tool>_routes(monkeypatch):
fake = FakeClient(); ...
mcp_full.<tool>(<Input>(...))
assert fake.calls[-1][0] == "<client_method>"
5. CLI (typer runner; assert it dials the right endpoint)
def test_<command>_wires_endpoint(monkeypatch):
result = runner.invoke(cli_main.app, ["<group>", "<command>", ...])
assert result.exit_code == 0
assert recorded_path == "/api/<route>"
6. Click-around (XCUITest — the weak leg; this is the whole point of the template)
Subclass FicheroUISessionTests — it gives you a launched app over a seeded library
(auto-skips when no venv/seeder). Drive by accessibility identifiers, assert the effect.
@MainActor
final class <Area>FlowUITests: FicheroUISessionTests {
func testUserDoesThingAndSeesResult() {
waitForLibraryReady()
// 1. reach the surface (a mode button / sidebar row by its a11y id)
app.buttons["<surface.entry.id>"].firstMatch.tap()
// 2. act (select a seeded row, invoke the verb)
let row = app.descendants(matching: .any)
.matching(identifier: "<row.id.for(seeded.ids[...])>").firstMatch
XCTAssertTrue(row.waitForExistence(timeout: readyTimeout))
row.rightClick(); app.menuItems["<verb.id>"].tap()
// 3. assert the effect (the row is gone / a value changed)
XCTAssertFalse(row.waitForExistence(timeout: 5))
}
}
.accessibilityIdentifier("kg.entity.row.\(id)"),
"kg.menu.delete"). No a11y ids ⇒ the click-around test can’t find anything. Adding the
ids is part of building the surface, not an afterthought — list them in the spec.
7. iPad/iOS
Same as the click-around skeleton, but the test targets the fichero-ui-ipad / fichero-ui-ios
plans; keep it to the touch path (tap, not right-click) and the reduced first-run IA.
8. Load (#4634)
def test_<verb>_1k_is_bounded(client, benchmark):
ids = seed_many(1000)
# act on 1000; assert it completes within budget and concurrency stays bounded
The process — spec-lead development, every surface, every time
Everything is done systematically, the same way each time — spec-lead development. Run the
design half in plan mode with a ponytail lens (research → plan → approve before code; the
laziest design that meets the intent). Each step has a guardrail gate, so the process is enforced,
not remembered. The loop produces four bound artifacts for one <name>: the spec, the
GitHub milestone, the tests (tagged), and the docs (contributor AND user).
- Scaffold + survey prior art.
cp docs/contributor_manual/specs/_TEMPLATE.md docs/contributor_manual/specs/<surface>.md. Fill Intent, then the Prior art / best practices section — survey how the field already solves this (digital humanities standards, NLP/NLG pipelines, Hugging Face, relevant libraries) and cite what we adopt/reuse rather than invent. · Gate:_-prefixed scaffolds are skipped; real specs are tracked. - Behaviors. Write one line per behavior with a stable id (
<surface>.<behavior>) and a tag ([OK]/[MISSING]/[PARTIAL]). These ids are what tests cite. - Approve. The creative director approves the intent; flip
Status: DRAFT → APPROVED. · Gate: an APPROVED spec MUST be cited by ≥1 test, carry a Test-matrix section (check_specs_have_tests.py, Rules B + C), and declareMilestone: <name>matching a GitHub milestone of the same name (check_spec_milestones.py). On approval, create or rename the milestone to the spec name and point its description back at the spec — the link is bidirectional (spec name == milestone name == test tag). Tag the tests to the same area name: Swift@Tag(TestTags.swift), pytest markers (fichero-server/pyproject.toml). - Fill the matrix. Tick the legs this surface touches; list the accessibility identifiers the click-around leg needs (add them to the views as you build).
- Test-first, per leg. One file per ticked leg, from the skeletons above; the docstring cites the behavior id. Hard-gate legs (cross-surface invariant + availability) first.
- Reuse, don’t duplicate — THEN implement. BEFORE writing, search the codebase for an
existing path (jCodemunch
search_symbols/find_references/find_similar_symbols) — we too often grow a SECOND implementation of something that already exists. If a path exists, EXTEND it; never add a parallel one (one audited action layer, one endpoint per capability). After implementing, confirm no duplicate/parallel path was introduced. Then make the tests pass; add the a11y ids alongside the views. - Verify.
python scripts/check_specs_have_tests.py+ the full gate (verify_all.shruns everycheck_*.py, including this one). - Document each audience — by its owner. Fill the spec’s Documentation matrix. The agent
authors what it owns: the contributor docs (
docs/contributor_manual/…+ this spec), the reference (docs/reference_manual/…), and the MCP tool description / CLI--helpwhere the feature is agent- or script-reachable. The user manual (docs/user_manual/…) is the maintainer’s own — authored in Tinderbox, exported to the GitHub folder; the agent does NOT write it, but DOES produce the accurate raw material the maintainer needs (the.xctestplan/RenderPreviewscreenshot capture doubles as the doc screenshot, and the behavior list/a11y ids are current). A feature isn’t done until the reader who never opens the code can use it — and the facts that reader will rely on are pinned by tests, not by prose that can drift.
Weakest leg today is click-around — treat its skeleton as non-optional for any surface a user touches. A surface without a click-around test is not “done”, it’s “unproven in the one way the user actually experiences it”.
Worked reference
kg-interactions.md and kg-tables.md carry filled-in Test matrices. Use them as examples
when you spec the Library icon view (or any new surface).