ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Property-Based Testing

Find edge cases your unit tests miss with property-based testing. Covers generators, shrinking, stateful testing, and integration with conventional test suites.

Property-based testing inverts the testing model. Instead of writing specific inputs and expected outputs, you describe properties that should always hold and let the framework generate hundreds of random inputs to find violations. It discovers edge cases humans never think of — empty strings, integer overflows, Unicode sequences, negative numbers, lists with duplicate elements.


Example-Based vs Property-Based

Example-based:
    test("sort returns sorted list"): 
        expect(sort([3,1,2])).toEqual([1,2,3])
        expect(sort([5,4])).toEqual([4,5])
        expect(sort([])).toEqual([])
        // Did you test negative numbers? Duplicates? Very long lists?

Property-based:
    property("sort returns list of same length"):
        for any list of integers:
            expect(sort(list).length).toBe(list.length)
    
    property("sort returns elements in ascending order"):
        for any list of integers:
            sorted = sort(list)
            for each consecutive pair (a, b) in sorted:
                expect(a <= b).toBe(true)
    
    property("sort preserves all elements"):
        for any list of integers:
            expect(sort(list)).toContainExactly(list)

Properties to Test

Property TypeExampleApplicable To
Roundtripdecode(encode(x)) === xSerialization, encryption, compression
Idempotencef(f(x)) === f(x)Cache operations, normalization, formatting
Invariantsorted list length === input list lengthAny transformation
Commutativityf(a, b) === f(b, a)Set operations, mathematical functions
Associativityf(f(a, b), c) === f(a, f(b, c))String concat, set union
Oraclefast_impl(x) === slow_reference(x)Optimized algorithms vs known-correct
No-crashf(random_input) does not throwInput validation, API handlers

Tool Comparison

ToolLanguageGeneratorsShrinkingBest For
fast-checkJavaScript/TypeScriptRich built-inAutomaticJS/TS projects
HypothesisPythonExtensiveAutomaticPython (industry standard)
QuickCheckHaskellOriginal PBT libraryAutomaticHaskell, inspired all others
jqwikJava/JVMJUnit 5 integrationAutomaticJava projects
proptestRustDerive macrosAutomaticRust projects
ScalaCheckScalaComprehensiveAutomaticScala/JVM
RapidGoSimple APIAutomaticGo projects

Generators

GeneratorProducesUse Case
integer()Random integers in rangeNumerical operations
string()Random strings (ASCII, Unicode, empty)Text processing
array()Random-length arrays of valuesCollection operations
record()Objects with typed fieldsDomain entity testing
oneof()One of several generatorsUnion types, enums
constant()Fixed valueControl values, sentinels
tuple()Fixed-length arrays of generatorsMulti-argument functions

Shrinking

When a property violation is found, the framework shrinks the failing input to the minimal reproduction case:

Property violation found with input: [847, -23, 0, 445, 12, -1, 99, 0, 7]

Shrinking...
    [847, -23, 0, 445, 12, -1, 99, 0, 7] → fails
    [847, -23, 0, 445] → fails
    [-23, 0, 445] → fails
    [-23, 0] → fails
    [-1, 0] → fails
    [0, -1] → passes
    [-1, 0] → MINIMAL FAILING CASE

Result: Property fails for input [-1, 0]

Stateful Testing

For testing systems with state (databases, caches, state machines), property-based testing generates sequences of operations and verifies invariants after each step:

Model: SimpleMap (in-memory reference implementation)
System Under Test: CacheService

Generated command sequence:
    1. put("key1", "value1")
    2. get("key1") → should return "value1"
    3. put("key2", "value2")
    4. delete("key1")
    5. get("key1") → should return null
    6. get("key2") → should return "value2"

After each command:
    Verify SUT state matches Model state

Integration with Conventional Tests

StrategyApproach
Alongside unit testsProperty tests in the same test file, tagged
Separate directorytests/property/ for heavy generators
CI configurationRun with fewer iterations on PR, more on nightly
Seed managementStore failing seeds for deterministic regression tests

Anti-Patterns

Anti-PatternProblemFix
Properties that are too weakOnly testing “doesn’t crash” — misses logic bugsWrite properties that verify specific behaviors
Too many iterations on CISlow test runs (100K iterations per property)Use 100-1,000 on PR, 10,000+ on nightly
Not saving failing seedsCan’t reproduce failures deterministicallySave seed in test, commit to version control
Custom generators too complexGenerator itself has bugsTest generators separately, keep them simple
Ignoring shrunk outputUsing the original large input to debugAlways debug with the shrunk minimal case

Checklist

  • Property-based testing framework added to project
  • Core algorithms have property tests (sort, search, transform, encode/decode)
  • Roundtrip properties tested for all serialization (JSON, protobuf, etc.)
  • Stateful tests for cache, database, and state machine components
  • Generators cover edge cases (empty, null, Unicode, negative, MAX_INT)
  • CI runs PBT with moderate iterations (1,000 per property on PR)
  • Nightly runs PBT with high iterations (10,000+ per property)
  • Failing seeds committed as regression tests
  • Team trained on identifying useful properties

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For quality engineering consulting, visit garnetgrid.com. :::

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →