property-based-testing
About
This skill helps developers create property-based tests that verify code properties hold across automatically generated inputs rather than specific examples. It's particularly useful for testing algorithms with mathematical properties, verifying invariants, and finding edge cases in parsers, data transformations, and data structures. The approach automatically generates test cases to uncover bugs that traditional example-based testing might miss.
Documentation
Property-Based Testing
Overview
Property-based testing verifies that code satisfies general properties or invariants for a wide range of automatically generated inputs, rather than testing specific examples. This approach finds edge cases and bugs that example-based tests often miss.
When to Use
- Testing algorithms with mathematical properties
- Verifying invariants that should always hold
- Finding edge cases automatically
- Testing parsers and serializers (round-trip properties)
- Validating data transformations
- Testing sorting, searching, and data structure operations
- Discovering unexpected input combinations
Key Concepts
- Property: A statement that should be true for all valid inputs
- Generator: Creates random test inputs
- Shrinking: Minimizes failing inputs to simplest case
- Invariant: Condition that must always be true
- Round-trip: Encoding then decoding returns original value
Instructions
1. Hypothesis for Python
# test_string_operations.py
import pytest
from hypothesis import given, strategies as st, assume, example
def reverse_string(s: str) -> str:
"""Reverse a string."""
return s[::-1]
class TestStringOperations:
@given(st.text())
def test_reverse_twice_returns_original(self, s):
"""Property: Reversing twice returns the original string."""
assert reverse_string(reverse_string(s)) == s
@given(st.text())
def test_reverse_length_unchanged(self, s):
"""Property: Reverse doesn't change length."""
assert len(reverse_string(s)) == len(s)
@given(st.text(min_size=1))
def test_reverse_first_becomes_last(self, s):
"""Property: First char becomes last after reverse."""
reversed_s = reverse_string(s)
assert s[0] == reversed_s[-1]
assert s[-1] == reversed_s[0]
# test_sorting.py
from hypothesis import given, strategies as st
def quick_sort(items):
"""Sort items using quicksort."""
if len(items) <= 1:
return items
pivot = items[len(items) // 2]
left = [x for x in items if x < pivot]
middle = [x for x in items if x == pivot]
right = [x for x in items if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
class TestSorting:
@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(self, items):
"""Property: Every element <= next element."""
sorted_items = quick_sort(items)
for i in range(len(sorted_items) - 1):
assert sorted_items[i] <= sorted_items[i + 1]
@given(st.lists(st.integers()))
def test_sorting_preserves_length(self, items):
"""Property: Sorting doesn't add/remove elements."""
sorted_items = quick_sort(items)
assert len(sorted_items) == len(items)
@given(st.lists(st.integers()))
def test_sorting_preserves_elements(self, items):
"""Property: All elements present in result."""
sorted_items = quick_sort(items)
assert sorted(items) == sorted_items
@given(st.lists(st.integers()))
def test_sorting_is_idempotent(self, items):
"""Property: Sorting twice gives same result."""
once = quick_sort(items)
twice = quick_sort(once)
assert once == twice
@given(st.lists(st.integers(), min_size=1))
def test_sorted_min_at_start(self, items):
"""Property: Minimum element is first."""
sorted_items = quick_sort(items)
assert sorted_items[0] == min(items)
@given(st.lists(st.integers(), min_size=1))
def test_sorted_max_at_end(self, items):
"""Property: Maximum element is last."""
sorted_items = quick_sort(items)
assert sorted_items[-1] == max(items)
# test_json_serialization.py
from hypothesis import given, strategies as st
import json
# Define a strategy for JSON-serializable objects
json_strategy = st.recursive(
st.none() | st.booleans() | st.integers() | st.floats(allow_nan=False) | st.text(),
lambda children: st.lists(children) | st.dictionaries(st.text(), children),
max_leaves=10
)
class TestJSONSerialization:
@given(json_strategy)
def test_json_round_trip(self, obj):
"""Property: Encoding then decoding returns original."""
json_str = json.dumps(obj)
decoded = json.loads(json_str)
assert decoded == obj
@given(st.dictionaries(st.text(), st.integers()))
def test_json_dict_keys_preserved(self, d):
"""Property: All dictionary keys are preserved."""
json_str = json.dumps(d)
decoded = json.loads(json_str)
assert set(decoded.keys()) == set(d.keys())
# test_math_operations.py
from hypothesis import given, strategies as st, assume
import math
class TestMathOperations:
@given(st.integers(), st.integers())
def test_addition_commutative(self, a, b):
"""Property: a + b = b + a"""
assert a + b == b + a
@given(st.integers(), st.integers(), st.integers())
def test_addition_associative(self, a, b, c):
"""Property: (a + b) + c = a + (b + c)"""
assert (a + b) + c == a + (b + c)
@given(st.integers())
def test_addition_identity(self, a):
"""Property: a + 0 = a"""
assert a + 0 == a
@given(st.floats(allow_nan=False, allow_infinity=False))
def test_abs_non_negative(self, x):
"""Property: abs(x) >= 0"""
assert abs(x) >= 0
@given(st.floats(allow_nan=False, allow_infinity=False))
def test_abs_idempotent(self, x):
"""Property: abs(abs(x)) = abs(x)"""
assert abs(abs(x)) == abs(x)
@given(st.integers(min_value=0))
def test_sqrt_inverse_of_square(self, n):
"""Property: sqrt(n^2) = n for non-negative n"""
assert math.isclose(math.sqrt(n * n), n)
# test_with_examples.py
from hypothesis import given, strategies as st, example
class TestWithExamples:
@given(st.integers())
@example(0) # Ensure we test zero
@example(-1) # Ensure we test negative
@example(1) # Ensure we test positive
def test_absolute_value(self, n):
"""Property: abs(n) >= 0, with specific examples."""
assert abs(n) >= 0
# test_stateful.py
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
import hypothesis.strategies as st
class StackMachine(RuleBasedStateMachine):
"""Test stack data structure with stateful properties."""
def __init__(self):
super().__init__()
self.stack = []
@rule(value=st.integers())
def push(self, value):
"""Push a value onto the stack."""
self.stack.append(value)
@rule()
def pop(self):
"""Pop a value from the stack."""
if self.stack:
self.stack.pop()
@invariant()
def stack_size_non_negative(self):
"""Invariant: Stack size is never negative."""
assert len(self.stack) >= 0
@invariant()
def peek_equals_last_push(self):
"""Invariant: Peek returns the last pushed value."""
if self.stack:
# Last item should be the most recently pushed
assert self.stack[-1] is not None
TestStack = StackMachine.TestCase
2. fast-check for JavaScript/TypeScript
// string.test.ts
import * as fc from 'fast-check';
describe('String Operations', () => {
test('reverse twice returns original', () => {
fc.assert(
fc.property(fc.string(), (s) => {
const reversed = s.split('').reverse().join('');
const doubleReversed = reversed.split('').reverse().join('');
return s === doubleReversed;
})
);
});
test('concatenation length', () => {
fc.assert(
fc.property(fc.string(), fc.string(), (s1, s2) => {
return (s1 + s2).length === s1.length + s2.length;
})
);
});
test('uppercase is idempotent', () => {
fc.assert(
fc.property(fc.string(), (s) => {
const once = s.toUpperCase();
const twice = once.toUpperCase();
return once === twice;
})
);
});
});
// array.test.ts
import * as fc from 'fast-check';
function quickSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}
describe('Sorting Properties', () => {
test('sorted array is ordered', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = quickSort(arr);
for (let i = 0; i < sorted.length - 1; i++) {
if (sorted[i] > sorted[i + 1]) return false;
}
return true;
})
);
});
test('sorting preserves length', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
return quickSort(arr).length === arr.length;
})
);
});
test('sorting preserves elements', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = quickSort(arr);
const originalSorted = [...arr].sort((a, b) => a - b);
return JSON.stringify(sorted) === JSON.stringify(originalSorted);
})
);
});
test('sorting is idempotent', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const once = quickSort(arr);
const twice = quickSort(once);
return JSON.stringify(once) === JSON.stringify(twice);
})
);
});
});
// object.test.ts
import * as fc from 'fast-check';
interface User {
id: number;
name: string;
email: string;
age: number;
}
const userArbitrary = fc.record({
id: fc.integer(),
name: fc.string({ minLength: 1 }),
email: fc.emailAddress(),
age: fc.integer({ min: 0, max: 120 }),
});
describe('User Validation', () => {
test('serialization round trip', () => {
fc.assert(
fc.property(userArbitrary, (user) => {
const json = JSON.stringify(user);
const parsed = JSON.parse(json);
return JSON.stringify(parsed) === json;
})
);
});
test('age validation', () => {
fc.assert(
fc.property(userArbitrary, (user) => {
return user.age >= 0 && user.age <= 120;
})
);
});
});
// custom generators
const positiveIntegerArray = fc.array(fc.integer({ min: 1 }), { minLength: 1 });
test('sum of positives is positive', () => {
fc.assert(
fc.property(positiveIntegerArray, (arr) => {
const sum = arr.reduce((a, b) => a + b, 0);
return sum > 0;
})
);
});
// test with shrinking
test('find minimum failing case', () => {
try {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
// This will fail for arrays with negative numbers
return arr.every(n => n >= 0);
})
);
} catch (error) {
// fast-check will shrink to minimal failing case: [-1] or similar
console.log('Minimal failing case found:', error);
}
});
3. junit-quickcheck for Java
// ArrayOperationsTest.java
import com.pholser.junit.quickcheck.Property;
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck;
import com.pholser.junit.quickcheck.generator.InRange;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import java.util.*;
@RunWith(JUnitQuickcheck.class)
public class ArrayOperationsTest {
@Property
public void sortingPreservesLength(List<Integer> list) {
List<Integer> sorted = new ArrayList<>(list);
Collections.sort(sorted);
assertEquals(list.size(), sorted.size());
}
@Property
public void sortedListIsOrdered(List<Integer> list) {
List<Integer> sorted = new ArrayList<>(list);
Collections.sort(sorted);
for (int i = 0; i < sorted.size() - 1; i++) {
assertTrue(sorted.get(i) <= sorted.get(i + 1));
}
}
@Property
public void sortingIsIdempotent(List<Integer> list) {
List<Integer> onceSorted = new ArrayList<>(list);
Collections.sort(onceSorted);
List<Integer> twiceSorted = new ArrayList<>(onceSorted);
Collections.sort(twiceSorted);
assertEquals(onceSorted, twiceSorted);
}
@Property
public void reverseReverseIsIdentity(List<String> list) {
List<String> once = new ArrayList<>(list);
Collections.reverse(once);
List<String> twice = new ArrayList<>(once);
Collections.reverse(twice);
assertEquals(list, twice);
}
}
// StringOperationsTest.java
@RunWith(JUnitQuickcheck.class)
public class StringOperationsTest {
@Property
public void concatenationLength(String s1, String s2) {
assertEquals(s1.length() + s2.length(), (s1 + s2).length());
}
@Property
public void uppercaseIsIdempotent(String s) {
String once = s.toUpperCase();
String twice = once.toUpperCase();
assertEquals(once, twice);
}
@Property
public void trimRemovesWhitespace(String s) {
String trimmed = s.trim();
if (!trimmed.isEmpty()) {
assertFalse(Character.isWhitespace(trimmed.charAt(0)));
assertFalse(Character.isWhitespace(trimmed.charAt(trimmed.length() - 1)));
}
}
}
// MathOperationsTest.java
@RunWith(JUnitQuickcheck.class)
public class MathOperationsTest {
@Property
public void additionCommutative(int a, int b) {
assertEquals(a + b, b + a);
}
@Property
public void additionAssociative(int a, int b, int c) {
assertEquals((a + b) + c, a + (b + c));
}
@Property
public void absoluteValueNonNegative(int n) {
assertTrue(Math.abs(n) >= 0);
}
@Property
public void absoluteValueIdempotent(int n) {
assertEquals(Math.abs(n), Math.abs(Math.abs(n)));
}
@Property
public void divisionByNonZero(
int dividend,
@InRange(minInt = 1, maxInt = Integer.MAX_VALUE) int divisor
) {
int result = dividend / divisor;
assertTrue(result * divisor <= dividend + divisor);
}
}
Common Properties to Test
Universal Properties
- Idempotence:
f(f(x)) = f(x) - Identity:
f(x, identity) = x - Commutativity:
f(a, b) = f(b, a) - Associativity:
f(f(a, b), c) = f(a, f(b, c)) - Inverse:
f(inverse_f(x)) = x
Data Structure Properties
- Round-trip:
decode(encode(x)) = x - Preservation: Operation preserves length, elements, or structure
- Ordering: Elements maintain required order
- Bounds: Values stay within valid ranges
- Invariants: Class invariants always hold
Best Practices
✅ DO
- Focus on general properties, not specific cases
- Test mathematical properties (commutativity, associativity)
- Verify round-trip encoding/decoding
- Use shrinking to find minimal failing cases
- Combine with example-based tests for known edge cases
- Test invariants that should always hold
- Generate realistic input distributions
❌ DON'T
- Test properties that are tautologies
- Over-constrain input generation
- Ignore shrunk test failures
- Replace all example tests with properties
- Test implementation details
- Generate invalid inputs without constraints
- Forget to handle edge cases in generators
Tools & Libraries
- Python: Hypothesis
- JavaScript/TypeScript: fast-check, jsverify
- Java: junit-quickcheck, jqwik
- Scala: ScalaCheck
- Haskell: QuickCheck (original)
- C#: FsCheck
Examples
See also: test-data-generation, mutation-testing, continuous-testing for comprehensive testing strategies.
Quick Install
/plugin add https://github.com/aj-geddes/useful-ai-prompts/tree/main/property-based-testingCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
sglang
MetaSGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
