Property-Based Testing
What it is
Section titled “What it is”A property-based test does not check one hard-coded example. You state a property — an invariant that must hold for all valid inputs — and the framework generates hundreds of random inputs, hunting for a counterexample.
When it finds one, it shrinks it to the smallest failing input, so you get
[] or 0 instead of a 200-character string.
property + generator │ ▼ ┌─────────────┐ pass ┌──────────┐ │ generate N │─────────►│ done │ │ inputs │ └──────────┘ └──────┬──────┘ │ fail ▼ ┌─────────────┐ │ shrink to │ → minimal counterexample │ smallest │ └─────────────┘Example properties: reverse(reverse(xs)) == xs, sorting is idempotent, a
round-trip encode/decode returns the original, length(concat(a, b)) == length(a) + length(b).
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Pure functions with a checkable invariant (parsers, codecs, sorters, math)
- Round-trip properties: encode/decode, serialize/deserialize, parse/print
- Finding edge cases you would never write by hand (
NaN, empty, unicode, overflow) - Complementing example-based tests, not replacing them
Avoid it when
- The expected output is just as complex as the input (no oracle to compare against)
- The system is stateful, slow or non-deterministic (prefer model-based or a real harness)
- You cannot describe a generator for valid inputs — garbage in, garbage out
- A handful of examples already covers the risk
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| fast-check | JS/TS | Rich arbitraries, integrated with Vitest/Jest |
| Hypothesis | Python | Best-in-class shrinking; @given, strategies |
| jqwik | Java/JVM | JUnit 5 platform engine; @Property, @ForAll |
| proptest | Rust | Generator + shrinker, proptest! macro |
| gopter | Go | Property testing on top of testing |
| QuickCheck | Haskell | The original that inspired them all |
| FsCheck / CsCheck | .NET | Idiomatic properties for C# and F# |
| QuickTheories | Java | Lightweight alternative to jqwik |
Quick setup
Section titled “Quick setup”npm install -D fast-check vitestpip install hypothesis pytestgo get github.com/leanovate/gopter<!-- Maven: jqwik (JUnit 5 platform) --><dependency> <groupId>net.jqwik</groupId> <artifactId>jqwik</artifactId> <version>1.9.1</version> <scope>test</scope></dependency>dotnet add package FsCheck.XunitExample
Section titled “Example”import { describe, it, expect } from 'vitest';import fc from 'fast-check';import { encode, decode } from './codec';
describe('codec', () => { it('round-trips any string', () => { fc.assert( fc.property(fc.string(), (s) => { expect(decode(encode(s))).toBe(s); }), ); });});from hypothesis import given, strategies as stfrom codec import encode, decode
@given(st.text())def test_round_trip(s): assert decode(encode(s)) == simport ( "testing"
"github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop")
func TestRoundTrip(t *testing.T) { params := gopter.DefaultTestParameters() properties := gopter.NewProperties(params) properties.Property("decode(encode(s)) == s", prop.ForAll( func(s string) bool { return Decode(Encode(s)) == s }, gen.AnyString(), )) properties.TestingRun(t)}import net.jqwik.api.ForAll;import net.jqwik.api.Property;import static org.assertj.core.api.Assertions.assertThat;
class CodecProperties { @Property void roundTripsAnyString(@ForAll String s) { assertThat(Codec.decode(Codec.encode(s))).isEqualTo(s); }}using FsCheck;using FsCheck.Xunit;
public class CodecProperties{ [Property] public Property RoundTripsAnyString(NonNull<string> input) { var s = input.Get; return (Codec.Decode(Codec.Encode(s)) == s).ToProperty(); }}Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Start from a real example. Write the example-based test first, then generalise it into a property: it is the “most valuable first test”.
- Keep generators small but honest. A generator that only produces valid input is fine; one that produces nothing useful tests nothing.
- Do not assert the exact same logic you are testing. A tautological property
(
f(x) == f(x)) always passes and proves nothing. - Seed and reproduce. Log the generated seed so a rare failure can be replayed deterministically in CI.
- Watch runtime. Randomised tests are slower than examples — bound the number of runs and keep the property pure.
- Treat every counterexample as a gift. Add it as an explicit regression example once fixed.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.