skills/property-based-testing/SKILL.md
Provides guidance for property-based testing across multiple languages and smart contracts. Use when writing tests, reviewing code with serialization/validation/parsing patterns, designing features, or when property-based testing would provide stronger coverage than example-based tests. For Go code, uses pgregory.net/rapid as the primary PBT framework.
npx skillsauth add roasbeef/claude-files property-based-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill proactively during development when you encounter patterns where PBT provides stronger coverage than example-based tests.
For Go code, always use pgregory.net/rapid as the property-based testing library. Do NOT use testing/quick -- it is limited, lacks proper shrinking, and has poor generator support.
IntRange, SliceOf, Map, Custom, OneOf, Ptr, SampledFrom.rapid.StateMachine for testing stateful systems.*testing.T via rapid.Check.func TestRoundtrip(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
msg := genMessage().Draw(t, "msg")
encoded := Encode(msg)
decoded, err := Decode(encoded)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(msg, decoded) {
t.Fatalf("roundtrip failed: got %v, want %v", decoded, msg)
}
})
}
| Generator | Description | Example |
|-----------|-------------|---------|
| rapid.Int() | Any int | rapid.Int().Draw(t, "n") |
| rapid.IntRange(lo, hi) | Bounded int | rapid.IntRange(0, 100).Draw(t, "n") |
| rapid.Int64() | Any int64 | rapid.Int64().Draw(t, "n") |
| rapid.Uint64() | Any uint64 | rapid.Uint64().Draw(t, "n") |
| rapid.Float64() | Any float64 | rapid.Float64().Draw(t, "f") |
| rapid.Bool() | true or false | rapid.Bool().Draw(t, "b") |
| rapid.String() | Any string | rapid.String().Draw(t, "s") |
| rapid.StringN(minLen, maxLen, maxRunes) | Bounded string | rapid.StringN(1, 50, -1).Draw(t, "s") |
| rapid.Byte() | Single byte | rapid.Byte().Draw(t, "b") |
| rapid.SliceOf(gen) | Slice of elements | rapid.SliceOf(rapid.Int()).Draw(t, "xs") |
| rapid.SliceOfN(gen, min, max) | Bounded slice | rapid.SliceOfN(rapid.Int(), 1, 10).Draw(t, "xs") |
| rapid.MapOf(keyGen, valGen) | Map | rapid.MapOf(rapid.String(), rapid.Int()).Draw(t, "m") |
| rapid.SampledFrom(slice) | One of values | rapid.SampledFrom([]string{"a","b"}).Draw(t, "s") |
| rapid.OneOf(gens...) | One of generators | rapid.OneOf(rapid.Int(), rapid.Int()).Draw(t, "n") |
| rapid.Just(val) | Constant value | rapid.Just(42).Draw(t, "n") |
| rapid.Ptr(gen, allowNil) | Pointer | rapid.Ptr(rapid.Int(), true).Draw(t, "p") |
| rapid.Map(gen, fn) | Transform | rapid.Map(rapid.Int(), func(n int) uint { return uint(n&0xff) }) |
| rapid.Custom(fn) | Custom generator | See below |
func genMessage() *rapid.Generator[Message] {
return rapid.Custom(func(t *rapid.T) Message {
return Message{
ID: rapid.IntRange(1, 10000).Draw(t, "id"),
Content: rapid.StringN(0, 1000, -1).Draw(t, "content"),
Priority: rapid.IntRange(1, 10).Draw(t, "priority"),
Tags: rapid.SliceOfN(rapid.StringN(1, 50, -1), 0, 20).Draw(t, "tags"),
}
})
}
For testing stateful systems (data structures, databases, protocol state machines):
type queueMachine struct {
queue Queue // System under test.
model []int // Reference model.
}
func (m *queueMachine) Init(t *rapid.T) {
m.queue = NewQueue()
m.model = nil
}
func (m *queueMachine) Push(t *rapid.T) {
v := rapid.Int().Draw(t, "v")
m.queue.Push(v)
m.model = append(m.model, v)
}
func (m *queueMachine) Pop(t *rapid.T) {
if len(m.model) == 0 {
t.Skip("empty queue")
}
got := m.queue.Pop()
want := m.model[0]
m.model = m.model[1:]
if got != want {
t.Fatalf("got %d, want %d", got, want)
}
}
func (m *queueMachine) Check(t *rapid.T) {
if m.queue.Len() != len(m.model) {
t.Fatalf("length mismatch: got %d, want %d", m.queue.Len(), len(m.model))
}
}
func TestQueue(t *testing.T) {
rapid.Check(t, rapid.Run[*queueMachine]())
}
Roundtrip (encode/decode):
func TestCodecRoundtrip(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
original := genMessage().Draw(t, "msg")
encoded, err := Encode(original)
if err != nil {
t.Fatal(err)
}
decoded, err := Decode(encoded)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(original, decoded) {
t.Fatalf("roundtrip failed: %v != %v", original, decoded)
}
})
}
Idempotence (normalization):
func TestNormalizeIdempotent(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
s := rapid.String().Draw(t, "s")
once := Normalize(s)
twice := Normalize(once)
if once != twice {
t.Fatalf("not idempotent: Normalize(%q) = %q, Normalize(%q) = %q",
s, once, once, twice)
}
})
}
Sorting properties:
func TestSort(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
xs := rapid.SliceOf(rapid.Int()).Draw(t, "xs")
result := MySort(xs)
// Length preserved.
if len(result) != len(xs) {
t.Fatalf("length changed: %d -> %d", len(xs), len(result))
}
// Ordered.
for i := 1; i < len(result); i++ {
if result[i-1] > result[i] {
t.Fatalf("not sorted at index %d: %d > %d", i, result[i-1], result[i])
}
}
// Idempotent.
result2 := MySort(result)
if !reflect.DeepEqual(result, result2) {
t.Fatal("sort not idempotent")
}
})
}
Oracle (reference implementation):
func TestOptimizedMatchesReference(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
input := rapid.SliceOf(rapid.Int()).Draw(t, "input")
got := OptimizedImpl(input)
want := ReferenceImpl(input)
if !reflect.DeepEqual(got, want) {
t.Fatalf("mismatch: optimized=%v, reference=%v", got, want)
}
})
}
Invoke this skill when you detect:
encode/decode, serialize/deserialize, toJSON/fromJSON, pack/unpack, Marshal/Unmarshalnormalize, sanitize, clean, canonicalize, formatis_valid, validate, check_* (especially with normalizers)add/remove/get operationsrapid.StateMachine)Priority by pattern:
| Pattern | Property | Priority | |---------|----------|----------| | encode/decode pair | Roundtrip | HIGH | | Pure function | Multiple | HIGH | | State machine | Invariants via StateMachine | HIGH | | Validator | Valid after normalize | MEDIUM | | Sorting/ordering | Idempotence + ordering | MEDIUM | | Normalization | Idempotence | MEDIUM | | Builder/factory | Output invariants | LOW | | Smart contract | State invariants | HIGH |
Do NOT use this skill for:
| Property | Formula | When to Use |
|----------|---------|-------------|
| Roundtrip | decode(encode(x)) == x | Serialization, conversion pairs |
| Idempotence | f(f(x)) == f(x) | Normalization, formatting, sorting |
| Invariant | Property holds before/after | Any transformation |
| Commutativity | f(a, b) == f(b, a) | Binary/set operations |
| Associativity | f(f(a,b), c) == f(a, f(b,c)) | Combining operations |
| Identity | f(x, identity) == x | Operations with neutral element |
| Inverse | f(g(x)) == x | encrypt/decrypt, compress/decompress |
| Oracle | new_impl(x) == reference(x) | Optimization, refactoring |
| Easy to Verify | is_sorted(sort(x)) | Complex algorithms |
| No Exception | No crash on valid input | Baseline property |
Strength hierarchy (weakest to strongest): No Exception -> Type Preservation -> Invariant -> Idempotence -> Roundtrip
Based on the current task, read the appropriate section:
TASK: Writing new tests
-> Read [{baseDir}/references/generating.md]({baseDir}/references/generating.md) (test generation patterns and examples)
-> Then [{baseDir}/references/strategies.md]({baseDir}/references/strategies.md) if input generation is complex
TASK: Designing a new feature
-> Read [{baseDir}/references/design.md]({baseDir}/references/design.md) (Property-Driven Development approach)
TASK: Code is difficult to test (mixed I/O, missing inverses)
-> Read [{baseDir}/references/refactoring.md]({baseDir}/references/refactoring.md) (refactoring patterns for testability)
TASK: Reviewing existing PBT tests
-> Read [{baseDir}/references/reviewing.md]({baseDir}/references/reviewing.md) (quality checklist and anti-patterns)
TASK: Test failed, need to interpret
-> Read [{baseDir}/references/interpreting-failures.md]({baseDir}/references/interpreting-failures.md) (failure analysis and bug classification)
TASK: Need library reference
-> Read [{baseDir}/references/libraries.md]({baseDir}/references/libraries.md) (PBT libraries by language, includes smart contract tools)
When you detect a high-value pattern while writing tests, offer PBT as an option:
"I notice
Encode/Decodeis a serialization pair. Property-based testing with a roundtrip property usingrapid.Checkwould provide stronger coverage than example tests. Want me to use that approach?"
If codebase already uses a PBT library (rapid, Hypothesis, fast-check, proptest, Echidna), be more direct:
"This codebase uses rapid. I'll write property-based tests for this serialization pair using a roundtrip property."
If Go codebase uses testing/quick, suggest migration:
"I see this codebase uses
testing/quick. Therapidlibrary (pgregory.net/rapid) provides better shrinking, richer generators, and stateful testing support. Want me to use rapid for the new tests?"
If user declines, write good example-based tests without further prompting.
Do not accept these shortcuts:
rapid.Custom makes custom ones trivialdevelopment
Adversarial review→triage→fix loop until a cold verifier signs off. Fans out lens-specific reviewer subagents, verifies every finding against the code (killing false positives), auto-applies confirmed fixes as fixup commits, and repeats until a fresh verifier approves. Prefers a deterministic dynamic workflow when available; falls back to in-instance Task dispatch. Use when the user types /review-loop or asks to adversarially review-and-fix a change set, branch, or commit range until clean.
development
Run GitHub Actions CI locally with Agent CI to validate changes before pushing. Use when testing, running checks, or validating code changes.
development
Clear-writing guide distilled from Steven Pinker's "The Sense of Style." Use when writing or revising prose that must be clear to a reader — documentation, design docs, specs, explanations, essays, emails, reports, RFCs, release notes — or when asked to make writing clearer, tighter, less academic, or less jargon-laden. Activate for "make this clearer", "tighten this", "why is this hard to read", "edit this for clarity", or any prose-quality pass.
development
Interactively debug Go programs in a single context using Delve (dlv) driven through tmux. Use when a bug requires runtime inspection — stepping through code, examining variables, walking goroutines, attaching to a live process, or debugging a hanging integration test — rather than just reading the source. Triggers include "step through this", "set a breakpoint", "attach to the running server", "why is this goroutine stuck", "debug this failing test".