Agent skill
nw-pbt-fundamentals
Property-based testing core concepts, property taxonomy, and strategy selection (language-agnostic)
Install this agent skill to your Project
npx add-skill https://github.com/nWave-ai/nWave/tree/main/nWave/skills/nw-pbt-fundamentals
SKILL.md
PBT Fundamentals
Core property-based testing knowledge, language-agnostic. Load language-specific skills for framework syntax.
What PBT Is
Instead of specifying individual test cases, define properties (rules for all valid inputs) and let the framework generate hundreds of random inputs to attempt falsification. On failure, framework automatically shrinks input to minimal reproducing case.
PBT complements example-based tests. Use both.
Property Types Taxonomy
Decision Table: Choosing Property Types
| Property Type | Use When | Example Domain |
|---|---|---|
| Invariant | Output has structural guarantees regardless of input | sort preserves length, tree stays balanced |
| Idempotency | Applying operation twice equals applying once | formatting, deduplication, HTTP PUT |
| Round-trip | Two operations are inverses | encode/decode, serialize/deserialize, compress/decompress |
| Oracle | Simpler reference implementation exists | optimized algo vs naive, new parser vs stdlib |
| Metamorphic | Can't verify single output but can relate multiple outputs | ML classifiers, search engines, numeric computation |
| Commutativity | Operation order shouldn't matter | set operations, independent migrations |
| Inductive | Property holds for base case and each incremental step | recursive data structures, incremental builds |
| Hard-to-find, easy-to-verify | Solution expensive to compute but cheap to check | pathfinding, factorization, compilation |
Decision Tree: Given a Function, Which Properties Apply?
Start: What does the function do?
|
+-- Has an inverse? (encode/decode, serialize/deserialize)
| -> Round-trip: decode(encode(x)) == x
|
+-- Has a simpler equivalent? (stdlib, naive version)
| -> Oracle: my_impl(x) == reference(x)
|
+-- Repeated application should be stable? (formatting, normalization)
| -> Idempotency: f(f(x)) == f(x)
|
+-- Output has structural guarantees? (length, type, range, ordering)
| -> Invariant: check structural properties of output
|
+-- Hard to verify single output but can relate inputs?
| -> Metamorphic: f(transform(x)) relates to f(x) predictably
|
+-- Order of operations should not matter?
| -> Commutativity: f(g(x)) == g(f(x))
|
+-- Works on recursive/incremental data?
| -> Inductive: base case holds, step preserves property
|
+-- Output is hard to compute but easy to verify?
-> Hard-to-find/easy-to-verify: verify(solve(x)) == true
Most functions have 2-4 applicable property types. Start with easiest to express, then add more.
Property Type Details
Invariant: len(sort(xs)) == len(xs). Most common starting point. Look for structural guarantees of output.
Idempotency: f(f(x)) == f(x). Applies to normalization, formatting, caching, PUT requests.
Round-trip: decode(encode(x)) == x. Highest-value property when applicable -- tests two functions simultaneously.
Oracle: my_sort(xs) == sorted(xs). Use when trusted reference exists. Oracle should be "so simple it's obviously correct."
Metamorphic: Define relations between executions. Example: sin(pi - x) == sin(x). Valuable when single-output verification is impossible.
Commutativity: f(g(x)) == g(f(x)). Applies when operations should be order-independent.
Inductive: Verify base case + step preservation. Natural for recursive structures.
Hard-to-find, easy-to-verify: product(factorize(n)) == n. Generate input, run algorithm, verify output cheaply.
Generator Composition Patterns
Pattern Hierarchy (simplest first)
- Built-in primitives: integers, strings, booleans, lists, dicts
- Map: Transform generated values:
integers().map(x => x * 2)for even numbers - Filter: Reject invalid values (use sparingly -- prefer constraining generators)
- FlatMap/Chain: When one value determines shape of next (dependent generation)
- Recursive: For tree-like structures -- requires base case + recursive case with size control
- Frequency/Weighted: Control distribution to ensure edge case coverage
Generator Design Rules
- Prefer constraining generators over filtering.
integers(min=1)beatsintegers().filter(x > 0) - Use map for simple transformations, flatmap for dependent generation
- Always verify generator distribution with statistics/collect
- Recursive generators need explicit size control to prevent infinite loops
- Weighted generation ensures rare-but-important cases get tested
Shrinking Strategies
| Strategy | How It Works | Used By | Trade-off |
|---|---|---|---|
| Type-based | Separate shrink function per type; returns list of simpler candidates | QuickCheck, PropEr, FsCheck | Explicit control but manual composition |
| Integrated | Generators produce shrink trees (rose trees); composition automatic | Hedgehog, fast-check, jqwik | Automatic but poor with dependent generators |
| Internal | Shrinks underlying byte stream, replays strategies | Hypothesis, rapid | Fully automatic, works with monadic bind |
Shrinking transforms [847, -23, 0, 445, 12] into [0, -1] -- minimal failing case. Without shrinking, PBT finds bugs but makes them hard to understand.
When NOT to Use PBT
- Highly specified I/O: Functions with single correct output per input (use example-based tests)
- UI/visual testing: Properties of visual output hard to express mathematically
- Integration tests with external services: Non-determinism confuses shrinking
- Simple CRUD with no business logic: Overhead exceeds benefit
- Performance testing: PBT tests correctness, not speed
When PBT Adds the Most Value
- Serialization/deserialization (round-trip)
- Data structure implementations (invariants)
- Parsers and compilers (oracle, round-trip)
- Mathematical/financial calculations (metamorphic, oracle)
- State machines and protocols (stateful PBT -- see pbt-stateful skill)
- Any function with complex input space and clear correctness rules
Adoption Guidance
- Start with round-trip properties -- easiest to write and highest-value
- Add invariant checks for data structure operations
- Introduce oracle properties when reference implementations exist
- Move to stateful PBT for mutable state systems (load pbt-stateful skill)
- Monitor generator distributions -- tests that never exercise edge cases provide false confidence
Round-trip Property Example
property "encode/decode round-trip" {
for_all input in generate(valid_document):
encoded = encode(input)
decoded = decode(encoded)
assert decoded == input
}
Collect statistics over 1000 runs; edge cases should appear in at least 5% of generated inputs.
For sequential operations or mutable state, see pbt-stateful skill.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
nw-research
Gathers knowledge from web and files, cross-references across multiple sources, and produces cited research documents. Use when investigating technologies, patterns, or decisions that need evidence backing.
nw-distill
Acceptance test creation methodology for the DISTILL wave. Domain knowledge for the acceptance designer agent: port-to-port principle, prior wave reading, wave-decision reconciliation, graceful degradation, and document back-propagation.
nw-review-output-format
YAML output format and approval criteria for platform design reviews. Load when generating review feedback.
nw-ddd-tactical
Tactical DDD — aggregate design rules, entities, value objects, domain events, repositories, domain services, and anti-pattern detection
nw-infrastructure-and-observability
Infrastructure as Code patterns (Terraform, Kubernetes), observability design (SLOs, metrics, alerting, dashboards), and pipeline security stages. Load when designing infrastructure, observability, or security scanning.
nw-par-critique-dimensions
Platform design review critique dimensions and severity levels. Load when reviewing CI/CD pipelines, infrastructure, deployment strategies, observability, or security designs.
Didn't find tool you were looking for?