Property-based testing checks that a general rule, called a property, holds true across a large number of automatically generated inputs, instead of checking one hand-picked example at a time. You describe an invariant — "encoding then decoding returns the original value," "the output list is always sorted," "the total never goes negative" — and the framework throws hundreds of randomized inputs at it, actively hunting for the one that breaks the rule.
What changed in 2026
- fast-check (JavaScript/TypeScript) and Hypothesis (Python) matured into default additions for teams testing parsers, serializers, and financial logic, not just niche functional-programming shops.
- AI-assisted property generation appeared in several testing tools, suggesting candidate properties by reading a function signature and its existing example tests — useful as a starting draft, unreliable as a final answer.
- Shrinking algorithms got faster and smarter, reliably reducing a 200-element failing input down to the 2-3 elements that actually matter within seconds.
- Property-based testing started pairing with mutation testing in some CI pipelines, since generated cases are effective at killing mutants that hand-written examples miss.
How a property-based test actually runs
- You write a property function that takes generated input and asserts something that should always be true.
- You declare generators for the input shape: arbitrary integers, strings, arrays, or custom domain objects.
- The framework runs the property against many generated inputs, by default 100 or more per run.
- On failure, it shrinks the input automatically, replaying smaller and simpler versions until it finds the minimal case that still fails.
import fc from "fast-check";
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = [...arr].sort((a, b) => a - b);
return sorted.length === arr.length;
})
);
Property-based vs example-based testing
| Aspect |
Example-based test |
Property-based test |
| Input |
One hand-picked value |
Hundreds of generated values |
| What it checks |
A specific expected output |
A general rule across all inputs |
| Edge cases |
Only the ones you thought of |
Found automatically, including odd ones |
| Readability |
Very clear intent |
Requires understanding the property |
| Best for |
Documentation, simple cases |
Invariants, parsers, math, serialization |
Neither replaces the other. A healthy suite keeps a few example tests as readable documentation and adds properties where a true invariant exists.
Where property-based testing pays off most
- Serialization and parsing — encode then decode should always return the original value.
- Sorting, searching, and data structure code — order, length, and containment invariants are easy to state and hard to satisfy accidentally.
- Financial and numeric logic — totals should never go negative, rounding should never lose more than a cent, conversions should be reversible.
- Anywhere you already write a loop of hand-picked edge cases — three, four, or five similar example tests are usually a property in disguise.
Common mistakes
Writing a property that is really just re-implementing the function. If your property recomputes the same logic to check the result, a bug in the logic will be a bug in the property too. State the invariant a different way than the implementation does.
Generating inputs that are too unconstrained. Unrestricted generators waste runs on inputs that can never occur in production. Constrain generators to realistic domains.
Ignoring a shrunk failure because the minimal case looks contrived. A shrunk input is still a real input the property does not hold for — contrived-looking cases often reveal a genuine boundary bug.
FAQ
Is property-based testing the same as fuzzing?
They are related. Fuzzing typically hunts for crashes and security issues with minimal structure. Property-based testing checks a specific, stated invariant and is more common in application-level test suites.
Do I need a property-based test for every function?
No. It is most valuable for functions with a real mathematical or structural invariant. Simple glue code with no meaningful property rarely benefits.
What is shrinking?
The process of automatically reducing a failing generated input to the smallest, simplest version that still fails, so you can debug the actual cause instead of a 500-element array.
Which tools should I use?
fast-check for JavaScript and TypeScript, Hypothesis for Python, QuickCheck for Haskell (the original), and jqwik for Java. Most modern languages have a mature option by now.
Where to go next