CONCEPT 06 · DESIGN
Property-Based Testing
"Write rules, not examples — let the machine find bugs."
Plain English
Instead of writing specific test cases ("given input X, expect output Y"), you describe properties — invariants that must hold for any input. A framework then generates hundreds of random inputs and tries to find one that breaks your property. You're describing what should always be true, not what happens in one example.
Analogy
Instead of testing "does my sort work on [3,1,2]?", you say: "for any array, sorting should always produce an ordered result with the same elements as the input." The computer runs this rule against 10,000 randomly generated arrays — including edge cases you'd never think to write: empty arrays, duplicates, negatives, huge numbers.
You already know this
Hypothesis (Python): @given(st.lists(st.integers())) def test_sort(lst): ...fast-check (JavaScript): fc.assert(fc.property(fc.array(fc.integer()), arr => ...))QuickCheck (Haskell): the original, invented the techniqueQA's stress/fuzz testing: property tests automate exactly what QA does manuallySpec-by-example in BDD: properties are the "rules" behind the examples
Interactive Demo
Property: reverse(reverse(xs)) === xs for ALL lists xs
Instead of writing specific test cases, we write a rule and let the machine test it with 100 random inputs.
// Press "Run 100 Tests" to generate random test cases...
Code Example
JavaScript (fast-check)
import fc from 'fast-check';
// EXAMPLE-BASED TESTS (traditional)
test('sort [3,1,2]', () => {
expect(mySort([3,1,2])).toEqual([1,2,3]); // only tests this one case
});
// PROPERTY-BASED: describes rules, runs 1000s of random cases
test('sort: result is always ordered', () => {
fc.assert(fc.property(
fc.array(fc.integer()),
(arr) => {
const sorted = mySort([...arr]);
// Property 1: adjacent elements are ordered
for (let i = 1; i < sorted.length; i++) {
if (sorted[i] < sorted[i-1]) return false;
}
return true;
}
));
});
test('sort: same elements, same count', () => {
fc.assert(fc.property(
fc.array(fc.integer()),
(arr) => {
const sorted = mySort([...arr]);
// Property 2: sorting doesn't add or remove elements
return sorted.length === arr.length &&
[...arr].sort().join() === sorted.join();
}
));
});
// Classic: serialize/deserialize round-trip
test('JSON round-trip', () => {
fc.assert(fc.property(
fc.jsonValue(),
(data) => JSON.parse(JSON.stringify(data)) === data
));
});Apply when
▸Parsers & serializers: encode(decode(x)) === x should hold for any x (round-trip property)
▸Sorting & ordering: result is ordered, has same elements, same length
▸Mathematical operations: commutativity (a+b === b+a), associativity ((a+b)+c === a+(b+c))
▸Compression: decompress(compress(x)) === x
▸Any invariant that should hold universally — "the output always satisfies condition Y"
Check Your Understanding
You have a serialize(data) and deserialize(string) pair. What is the most powerful property-based test for this?