Property-Based Testing — QuickCheck & Hypothesis Guide
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's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro