Skip to content

Property-Based Testing — QuickCheck & Hypothesis Guide

DodaTech Updated 2026-06-24 4 min read

In this tutorial, you'll learn about Property. We cover key concepts, practical examples, and best practices.

Property-based testing flips traditional testing on its head — instead of writing specific input-output pairs, you define properties your code should always satisfy and let the testing framework generate hundreds of random inputs to verify them automatically. This approach uncovers edge cases you never thought to test, catching bugs that example-based unit tests routinely miss. Teams at DodaTech use Hypothesis to validate data parsing in Doda Browser's URL handling and QuickCheck to verify compression logic in DodaZIP's archive utilities.

Learning Path

flowchart LR
  A[Unit Testing] --> B[Example-Based Tests]
  B --> C[Property-Based Testing
You are here] C --> D[Mutation Testing] D --> E[Fuzzing Guide] style C fill:#f90,color:#fff

Property-Based Testing vs Example-Based Testing

Aspect Example-Based Property-Based
Inputs Hand-picked by developer Auto-generated by framework
Coverage What you think of What you didn't think of
Output Pass/Fail per test Minimal failing example
Maintainability Brittle — breaks on refactor Tests invariants, not values
Bug discovery Confirms known cases Finds unknown edge cases

Python with Hypothesis

Hypothesis is the most popular property-based testing library for Python:

from hypothesis import given, strategies as st

@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
    assert a + b == b + a

Expected output:

PASSED

JavaScript with fast-check

fast-check brings property-based testing to JavaScript:

const fc = require('fast-check');

describe('addition', () => {
  it('is commutative', () => {
    fc.assert(
      fc.property(fc.integer(), fc.integer(), (a, b) => {
        return a + b === b + a;
      })
    );
  });
});

Expected output:

✓ is commutative

Common Property Patterns

Round-Trip Property

Serialize then deserialize should return the original:

from hypothesis import given, strategies as st
import json

@given(st.dictionaries(st.text(), st.integers()))
def test_json_round_trip(data):
    serialized = json.dumps(data)
    deserialized = json.loads(serialized)
    assert deserialized == data

Invariant Property

Certain truths should always hold after an operation:

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sorted_list_invariants(lst):
    sorted_lst = sorted(lst)
    assert len(sorted_lst) == len(lst)
    for i in range(len(sorted_lst) - 1):
        assert sorted_lst[i] <= sorted_lst[i + 1]

Idempotence Property

Running an operation twice should match running it once:

from hypothesis import given, strategies as st

@given(st.text())
def test_strip_idempotent(s):
    assert s.strip().strip() == s.strip()

Finding Real Bugs with Property-Based Tests

Here is a buggy sorting implementation that looks correct at first glance:

def buggy_sort(lst):
    if not lst:
        return lst
    return [x for x in lst if x >= 0] + [x for x in lst if x < 0]
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_properties(lst):
    result = buggy_sort(lst)
    assert len(result) == len(lst)
    assert sorted(result) == sorted(lst)

Expected output:

Falsifying example: test_sort_properties(lst=[5, -3, 2])

The test found that buggy_sort([5, -3, 2]) returns [5, 2, -3] — it preserves length but the partitioning logic is wrong. Hypothesis shrinks the input from something complex to this minimal case automatically.

Shrinking — Why It Matters

When Hypothesis finds a failure, it automatically reduces the input to the smallest reproducing example. This is called shrinking. Without shrinking, you would see something like [5, -3, 2, 100, -7, 0, 42, -1] and have to debug the root cause manually. With shrinking, you get the minimal case directly.

Practice Questions

1. What is the main difference between example-based and property-based testing?

Example-based tests use specific predetermined inputs. Property-based tests define general properties and auto-generate random inputs.

2. What does shrinking mean in property-based testing?

When a failure is found, the framework reduces the input to the smallest case that still triggers the failure, making debugging easier.

3. Name three common property patterns.

Round-trip (serialize/deserialize), invariant (always-true conditions), and idempotence (doing it twice equals doing it once).

4. How does property-based testing relate to fuzz testing?

Both generate random inputs, but property-based testing verifies logical properties while fuzzing typically looks for crashes or assertion failures.

Challenge: Write property-based tests for a URL parsing function. Define properties like: parsing then unparsing returns the original URL, the scheme is always lowercase, and the hostname never contains invalid characters. Use Hypothesis (Python) or fast-check (JavaScript).

FAQ

What is property-based testing?

A testing technique where you define properties (invariants) your code must satisfy, and the framework generates random inputs to verify those properties hold.

When should I use property-based testing?

Use it for functions with clear mathematical properties — sorting, serialization, validation, compression, encoding/decoding. Avoid it for functions with complex side effects.

What is the difference between QuickCheck and Hypothesis?

QuickCheck originated in Haskell. Hypothesis is the Python equivalent with better shrinking and integration with pytest.

How many examples does Hypothesis generate by default?

100 random inputs per test. You can adjust this with @settings(max_examples=N).

What's Next

Mutation Testing — Assessing Test Quality
Fuzz Testing — Automating Input Validation
Unit Testing — Complete Guide

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro