Learning path
Property-Based Testing — Status
Grounded in Wlaschin, Hughes, Goldstein et al. and the fast-check docs. Where property-based testing pays off in CRUD and command-based backends. See MISSION.md and RESOURCES.md.
Knowledge graph
graph TD V["Vocabulary
property · generator · shrinking · oracle"] W["Wlaschin's seven patterns
inverse · invariant · idempotence · commutativity · oracle …"] A["01 Model the resource
SUT vs. model · commands · CRUD vs. command endpoints"] V --> A W --> A A --> G["Generic harness
one model, a spec per resource · error contract"] A --> R["Generators that hit the edges
preconditions · valid by construction · fc.statistics"] V --> X["Reproducing a failure
seed · path · pinning the shrunk case"] W --> M["02 Relate two calls
source · follow-up · six MROPs"] A --> M A --> I["Choosing invariants
representation · cross-row · conservation · derived data"] A --> H["03 Five ways to specify it
validity · postcondition · metamorphic · inductive · model"] M --> H G --> C["Concurrency
linearizability · scheduledModelRun · racing creates"] G --> T["Tenancy & permissions in the model"] A --> U["Command endpoints in depth
undo · replay · rejected commands"] M --> S["Whole-API properties
Schemathesis · 'never 500'"] classDef mastered fill:#dcfce7,stroke:#16a34a,color:#14532d; classDef partial fill:#fef9c3,stroke:#ca8a04,color:#713f12; classDef inner fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:3px; classDef outer fill:#f5f3ff,stroke:#7c3aed,color:#4c1d95,stroke-dasharray:5 3; classDef locked fill:#fafaf9,stroke:#d6d3d1,color:#a8a29e; class V,W partial; class A,M,H inner; class G,R,U,I,X outer; class C,T,S locked; click A "lessons/01-model-the-resource.html" "Lesson 01" click M "lessons/02-relate-two-calls.html" "Lesson 02" click H "lessons/03-five-ways-to-specify-it.html" "Lesson 03"
Calibration — 2026-10-06
PARTIAL — vocabulary and patterns
Introduced in conversation, not yet tested. Raised the right objection unprompted: "how is this better than parameterised example tests?" The answer (unknown sequences, combinatorics, new commands tested against all old ones) is in conversation, not yet in a lesson.
PARTIAL — Q1: name an inverse pair in a CRUD app
Answered "create and delete". Holds at the API level
(list after create → delete equals list before), but with soft-delete the database
is not back where it started: the row, the reserved slug and the audit entries remain. That
mismatch is exactly where lesson 01's bugs live. Missed the higher-leverage round-trips Goldstein et al. found
in 11 of 30 interviews: serialise/parse, DTO ↔ entity mapping, export/import.
PARTIAL — Q2: generator rejected 97% of the time
Right that the value is low, and right instinct on the fix
(generate the valid case by default, the invalid case rarely). Missing the vocabulary: the generator must
satisfy the endpoint's preconditions, ideally by construction rather than by filtering, and
fc.statistics shows the distribution you actually get. Goldstein et al.: 17/30 participants named
precondition-satisfying generators as a main need.
GAP — Q3: reproducing a CI failure
Answered "the shrunk example". That's what you pin as a regression test, but reproduction needs the seed (and path), which make fast-check regenerate the same run. A failure that passes on retry also suggests the nondeterminism is outside the generator (DB state, time, ordering), which the seed can't reproduce.
PARTIAL — Q4: a property for applyDiscount
Gave an invariant (total ≥ 0) and idempotence (applying a code twice doesn't double the discount). Both valid. The invariant is weak on its own: an implementation that always returns 0 passes. Missing the stronger metamorphic relations that don't need the rules: the discounted total ≤ the original; reordering lines changes nothing; adding an item never lowers the total.
PARTIAL — Q5: two concurrent create(slug: "x")
First answered idempotence or a uniqueness error, then
correctly pointed out that the question didn't fix the semantics: if create is PUT /projects/x,
it is idempotent (RFC 9110). The rule that covers both cases is still missing: linearizability. The
outcome must match some sequential order of the two calls. POST: one 201 and one 409. PUT with identical
bodies: one row, both succeed. PUT with different bodies: one row holding one of the two bodies
(idempotence doesn't cover that case). A uniqueness error surfacing as a 500 is a bug under either semantics.
Gated behind the generic harness.
STRONG — generator coverage
From lesson 01 feedback: spotted that a command which only
targets existing rows can never generate the never-existed 404 path. That is the same coverage hole lesson 01's
bug hunt teaches, applied unprompted to a snippet the lesson had marked correct. Lesson 01 now uses a weighted
Target (mostly existing rows, sometimes an unknown UUID).
GAP — metamorphic relations (lesson gap, not learner gap)
Lesson 01 asked for "metamorphic" without defining it. Fixed with a definition in lesson 01 and a full lesson 02.
ASKED — which invariants are worth checking?
"Is it data-structure specific?" Answered briefly in conversation; queued as the next lesson.