brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
quality-property-based-testingUse when the user asks about property-based testing, fuzzing, generative testing, QuickCheck, fast-check, invariant testing, or random testing. Do NOT use for: example-based unit testing (quality-unit-testing), or integration testing (quality-integration-testing).
Independent GitHub snapshot · Automated classification · Original documentation governs
Use property-based testing to define invariants and properties, generate random inputs, shrink failing cases, and discover edge cases that example-based testing misses. This skill covers generator design, property formulation, shrinking optimization, stateful testing, and CI integration.
User mentions property-based testing, fuzzing, generative testing, QuickCheck, fast-check, Hypothesis, invariant testing, random testing, or asks about finding edge cases automatically.
Property-based test suite with generators, property definitions, and CI integration configuration.
Structured property test suite with:
Inputs: language, team expertise, existing tooling
├── TypeScript/JavaScript → fast-check
│ ├── Stateful testing needed? → Use fc.Command + fc.modelRun
│ └── Async code? → Use fc.asyncProperty
├── Python → Hypothesis
│ ├── Stateful testing needed? → RuleBasedStateMachine
│ └── Pandas/NumPy? → pandas strategies
├── Java/Kotlin → jqwik
│ ├── Spring Boot? → Combine with Spring tests
│ └── Stateful testing? → StateMachine
└── Haskell → QuickCheck
Can you express a relation between input and output?
├── YES → Is the function an involution?
│ ├── YES → Reflective property: f(f(x)) == x
│ └── NO → Is the function idempotent?
│ ├── YES → Idempotence property: f(f(x)) == f(x)
│ └── NO → Round-trip property: decode(encode(x)) == x
└── NO → Can you express an invariant?
├── YES → Invariant property (e.g., output is sorted)
└── NO → Metamorphic property: related inputs → related outputs
fc.integer({ min: 1 }) instead of fc.integer().filter(n => n > 0))| Aspect | Property-Based | Example-Based | Fuzzing |
|---|---|---|---|
| Input generation | Random, constrained | Hand-picked | Random, mutation-based |
| Expected output | Invariant relation | Fixed value | Any output (crash/no crash) |
| Bug discovery | Edge cases, contract violations | Known scenario validation | Security vulnerabilities, crashes |
| Test maintenance | Medium (property may need update) | Low (examples are stable) | Low (no assertions) |
| Documentation value | High (documents contracts) | Very High (documents behavior) | Low |
| False positives | Low (properties are precise) | None | High (crashes may be benign) |
| Shrinking | Automatic to minimal case | N/A | Manual or automatic (no structure) |
numRuns: process.env.CI ? 100 : 1000 for different environmentsimport * as fc from "fast-check";
// Property: encode/decode is a round-trip
test("URL encoding and decoding round-trips correctly", () => {
fc.assert(
fc.property(fc.string({ minLength: 1 }), (raw: string) => {
const encoded = encodeURIComponent(raw);
const decoded = decodeURIComponent(encoded);
return decoded === raw;
}),
);
});
// Property: sort always returns elements in non-decreasing order
test("sort returns a sorted array", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr: number[]) => {
const sorted = [...arr].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
if (sorted[i - 1] > sorted[i]) return false;
}
return true;
}),
);
});
// Property: removing duplicates is idempotent
test("uniq is idempotent", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr: number[]) => {
const once = uniq(arr);
const twice = uniq(once);
return JSON.stringify(once) === JSON.stringify(twice);
}),
);
});
from hypothesis import given, strategies as st
from src.pricing import calculate_discount, PriceBreak
@given(
quantity=st.integers(min_value=0, max_value=1000),
breaks=st.lists(
st.builds(PriceBreak,
quantity=st.integers(min_value=1, max_value=100),
discount_percent=st.decimals(min_value=0, max_value=100)
),
min_size=0,
max_size=10,
),
)
def test_discount_percent_is_never_negative(quantity, breaks):
discount = calculate_discount(quantity, breaks)
assert discount >= 0
@given(
quantity=st.integers(min_value=0, max_value=1000),
breaks=st.lists(
st.builds(PriceBreak,
quantity=st.integers(min_value=1, max_value=100),
discount_percent=st.decimals(min_value=0, max_value=100)
),
min_size=0,
max_size=10,
),
)
def test_discount_percent_never_exceeds_100(quantity, breaks):
discount = calculate_discount(quantity, breaks)
assert discount <= 100
# Custom strategy for valid email addresses
email_strategy = st.emails()
@given(email_strategy)
def test_email_validation(email):
assert is_valid_email(email)
assert "@" in email
# Custom strategy for structured data
user_strategy = st.fixed_dictionaries({
"name": st.text(min_size=1, max_size=100),
"age": st.integers(min_value=0, max_value=150),
"email": st.emails(),
"role": st.sampled_from(["admin", "user", "viewer"]),
})
@given(user_strategy)
def test_user_creation(user):
result = create_user(user)
assert result["name"] == user["name"]
assert result["role"] in ["admin", "user", "viewer"]
import * as fc from "fast-check";
class CounterModel {
value: number = 0;
}
class IncrementCommand implements fc.Command<CounterModel, Counter> {
check = () => true;
async run(model: CounterModel, real: Counter): Promise<void> {
model.value += 1;
await real.increment();
expect(await real.get()).toBe(model.value);
}
}
test("counter behaves correctly under random commands", async () => {
await fc.assert(
fc.asyncProperty(
fc.commands([fc.constant(new IncrementCommand())], { size: "+1" }),
async (cmds) => {
const real = new Counter();
const model = new CounterModel();
const runner = fc.asyncModelRun(() => ({ model, real }));
await runner(cmds);
},
),
);
});
# .github/workflows/pbt.yml
name: Property-Based Tests
on:
pull_request:
branches: [main]
schedule:
- cron: "0 3 * * *" # Nightly thorough run
jobs:
fast:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vitest run --testPathPattern="\.pbt\.test\.tsquot;
env:
PBT_RUNS: 100 # Fast: 100 runs per property
- name: Upload failing seeds
if: failure()
uses: actions/upload-artifact@v4
with:
name: failing-seeds
path: seeds/*.txt
thorough:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vitest run --testPathPattern="\.pbt\.test\.tsquot; --reporter=junit
env:
PBT_RUNS: 10000 # Thorough: 10000 runs per property
- uses: actions/upload-artifact@v4
if: always()
with:
name: pbt-results
path: junit.xml
- name: Check shrinking quality
run: node scripts/check-shrinking-quality.js
Using .filter() that rejects more than 50% of generated values. This kills performance and breaks shrinking (shrinker gets confused by rejected values). Replace filters with constrained generators: fc.integer({ min: 1 }) instead of fc.integer().filter(n => n > 0).
Writing properties that can never fail. Example: "the sum of two positive integers is positive" when the generator only produces positive integers. The property is a tautology — it encodes the generator constraint, not a system invariant.
Running only 10-20 iterations is not property-based testing — it's just random sampling with extra steps. The statistical power of PBT comes from many random tests. Minimum 100 runs for CI. Use 1000+ for thorough validation.
If the counterexample is still complex (100+ items instead of 1-3), the shrinker is not working effectively. Constrained generators shrink better: prefer fc.integer({ min: 1, max: 100 }) over unbounded. Avoid fc.anything().
When a property fails, the framework generates a seed that can reproduce the failure. If you don't log the seed, you cannot reproduce the failure deterministically. Always capture and store the failing seed alongside the counterexample.
Stateful property tests that share mutable state between command sequences produce irreproducible failures. Each test run must start from a clean state. Reset all state in beforeEach.
| Level | Characteristics | Practices |
|---|---|---|
| 1: Initial | No PBT | Example-based tests only, no property testing knowledge in team |
| 2: Defined | Basic PBT adoption | Round-trip properties for serialization, basic generators, one or two team members proficient |
| 3: Managed | Systematic PBT usage | All data transformation functions have properties, custom generators, shrinking optimization, stateful models for complex systems |
| 4: Measured | PBT-integrated quality | Properties alongside examples for all critical logic, regression suite includes all counterexamples, CI has fast+thorough PBT stages |
| 5: Optimized | Property-first development | Properties defined before implementation (PBT-driven development), mutation testing validates property quality, automated generator tuning |
maxLength, maxDepth bounds.numRuns: process.env.CI ? (process.env.PBT_RUNS || 100) : 1000.After property-based testing, hand off to:
quality-unit-testing — for example-based tests that complement propertiesquality-integration-testing — for verifying properties at system boundariesquality-regression-testing — for adding discovered counterexamples to regression suitesquality-load-testing — for performance property validation under load` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
| Decision Point | Option A | Option B | Decision Criteria |
|---|---|---|---|
| Language support | Haskell/QuickCheck (mature) | JavaScript/fast-check | Team expertise, ecosystem maturity |
| Stateful vs pure | Pure functions (stateless properties) | Stateful systems (model-based) | Application architecture |
| Property type | Algebraic (commutative, associative) | Invariant (always true after op) | Domain logic, data structure complexity |
| Integration | Dedicated property test suite | Mixed with example-based tests | Team familiarity, gradual adoption |