The Lineage of Property-Based Testing — From QuickCheck to Hypothesis
In 2000, Claessen and Hughes unveiled QuickCheck for Haskell, opening up a new way of thinking about testing: writing properties instead of examples. We trace generators and shrinking, the spread to Hypothesis, fast-check, and proptest, and the relationship to fuzzing.
The mocks we saw last time (Article 6) were a technique for verifying specific behavior — whether a particular call happened correctly. Most unit tests work the same way: they write, one at a time, examples of “for this input, this output.” But there’s an entirely different approach: define up front a property that the code should satisfy — “the length of a list doesn’t change after sorting,” “serializing and then deserializing returns the original value” — and have a tool verify that property against a huge number of randomly generated inputs. This is property-based testing (PBT).
The QuickCheck Paper — ICFP 2000
In 2000, at Chalmers University of Technology in Gothenburg, Sweden, Koen Claessen and John Hughes presented a paper titled “QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs” at the ACM SIGPLAN International Conference on Functional Programming (ICFP 2000). This paper is the starting point that spread the concept of property-based testing to the world.
The QuickCheck implementation shown in the paper was, at first, a simple library of only about 300 lines — but the idea was powerful.
-- The property "reversing a list twice returns the original list"
prop_reverseReverse :: [Int] -> Bool
prop_reverseReverse xs = reverse (reverse xs) == xs
Rather than individual input-output examples, the programmer writes a function expressing the property that must hold. QuickCheck then generates a large number of random inputs based on type information and repeats the test until it finds a counterexample.
Generators and Shrinking
Two core concepts established by QuickCheck are what make property-based testing work as a practical technique.
A generator is a mechanism that produces random values appropriate to the type under test (integers, strings, lists, custom data types). Through a type class (Arbitrary), QuickCheck automatically supplies generators for standard types, while letting users define their own for custom types.
Shrinking is the mechanism that, once a counterexample is found, automatically reduces that input to a simpler form. If a failure reproduces with a 47-element list, presenting it as-is is hard for a human to read. QuickCheck keeps shrinking the input while the property remains broken, ultimately presenting “the smallest counterexample that can no longer be simplified.” This combination of “generator plus shrinking” is the architecture shared by every QuickCheck-family tool.
Generated failing input: [3, -17, 0, 256, -1, 42]
↓ shrink
[0, -1]
↓ shrink
[-1] ← the smallest possible counterexample
Quviq QuickCheck — Commercialization and Concurrent Systems
In 2006, John Hughes founded Quviq to commercialize QuickCheck’s technology, offering a commercial version of QuickCheck for Erlang. It has been applied to real systems in the telecom and automotive industries to detect complex bugs in concurrent and distributed systems — race conditions, inconsistencies during node failures — and is known for having found many serious bugs in practice.
The Erlang version is also notable for developing stateful property testing: modeling a stateful system (a protocol implementation, a database) as a state machine, and applying a randomly generated sequence of operations to both the model and the real system to check that they stay consistent. Because the randomly generated order of operations often triggers bugs by itself, this approach can find race conditions and timing-dependent bugs that a single-input test would never catch.
Spread Across Languages
QuickCheck’s idea spread rapidly beyond Haskell, and today a “QuickCheck-family” library exists for nearly every major language.
| Language | Library | Notes |
|---|---|---|
| Haskell | QuickCheck | The original (Claessen & Hughes, 2000) |
| Erlang | Quviq QuickCheck | Commercial, for concurrent/distributed systems |
| Scala | ScalaCheck | Developed by Rickard Nilsson |
| Python | Hypothesis | Developed by David R. MacIver |
| JavaScript/TypeScript | fast-check | Used alongside Jest and similar tools |
| Rust | proptest / quickcheck | proptest adopts Hypothesis’s design philosophy |
| Java | jqwik | Emphasizes integration with JUnit 5 |
| C#/F# | FsCheck | For .NET |
What Makes Hypothesis Distinctive
Python’s Hypothesis is not simply a port of QuickCheck — it has undergone several important evolutions of its own. It’s distinguished by an integrated mechanism that stores the history of generated values in a database and preferentially reproduces previously found counterexamples on subsequent test runs, and by the sophistication of its shrinking.
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_reverse_twice_is_identity(xs):
assert list(reversed(list(reversed(xs)))) == xs
This test doesn’t run against a single example input but against the entire space of “any list of integers”; if a failing input is found, it’s reported after shrinking as a minimal counterexample. Rust’s proptest explicitly acknowledges being inspired by Hypothesis, inheriting its “strategy” vocabulary and its integrated-shrinking design. JavaScript’s fast-check is written in the same spirit.
import fc from "fast-check";
fc.assert(
fc.property(fc.array(fc.integer()), (xs) => {
const roundTripped = JSON.parse(JSON.stringify(xs));
return JSON.stringify(roundTripped) === JSON.stringify(xs);
})
);
In both examples, no concrete input value ever appears in the test code — this is the biggest difference from an ordinary unit test.
Two Styles of Shrinking
There are broadly two implementation styles for shrinking.
- Type-based shrinking: the style adopted by the original QuickCheck, where “how to shrink” is defined per type (move integers toward 0, remove elements from lists, and so on). Simple to implement, but shrinking rules must be written individually for each type.
- Integrated shrinking: the style adopted by Hypothesis and Rust’s proptest, where the process of generating a value — the history of random-number consumption — is itself the target of shrinking. Even when generator composition is complex, this lets shrinking stay consistent without changing the generation logic.
This design difference bears directly on usability when handling complex custom data types, and is a practical consideration when choosing a library.
Types of Properties — What Should You Write?
The hardest part of property-based testing isn’t learning to use the tool — it’s figuring out what property to write. Commonly used types in practice include:
- Round-trip: verifying that transforming and then transforming back returns the original, as in
decode(encode(x)) == x. Common when testing serializers, parsers, and encodings. - Invariant: a property that must hold before and after an operation — “the element count doesn’t change after sorting,” “pushing then popping a stack returns the same value,” and so on.
- Model-based: prepare a simple but slow, obviously-correct “model implementation” and check its output against the real implementation’s. Often also called differential testing.
- Relation to the oracle problem: even when there’s no available criterion (a test oracle) for judging “what the correct output is,” it’s often still possible to define “a relationship between input and output that must hold.” Property-based testing is positioned as a technique that enables verification even without a strict oracle.
Converging with Fuzzing
The idea of verifying a program with automatically generated input is also deeply related to fuzzing. Where PBT verifies logical correctness — whether a meaningful property holds — fuzzing mainly verifies robustness: does the program crash, or violate memory safety? Coverage-guided fuzzing, which became mainstream from the 2010s onward, measures which code paths were exercised at runtime and preferentially mutates inputs that exercised new code paths, making exploration far more efficient than blind generation.
| Tool | Characteristics |
|---|---|
| AFL (American Fuzzy Lop) | Developed by Michal Zalewski, released in 2013. The pioneering tool that brought coverage-guided fuzzing into everyday practice |
| libFuzzer | An in-process fuzzing engine built into the LLVM project |
| OSS-Fuzz | Google’s continuous fuzzing service for open-source projects, announced in December 2016 |
go-fuzz / go test -fuzz |
Developed by Dmitry Vyukov. Natively integrated into the language’s standard toolchain in Go 1.18 (2022) |
// Native fuzzing in Go 1.18 and later
func FuzzParseURL(f *testing.F) {
f.Add("https://example.com/path")
f.Fuzz(func(t *testing.T, input string) {
u, err := url.Parse(input)
if err != nil {
return // parse failures are acceptable
}
if _, err := url.Parse(u.String()); err != nil {
t.Errorf("re-parse failed for %q: %v", u.String(), err)
}
})
}
The two are not mutually exclusive; in recent years, hybrid approaches — structure-aware fuzzing that generates structured inputs while also using coverage feedback — have spread, and the boundary between them is gradually blurring.
Adoption in Practice
Property-based testing is powerful, but its penetration into practice hasn’t reached the level of xUnit-style unit testing. Reasons cited include that formulating a “property” itself demands design skill and is a higher bar than writing examples, and that runtime tends to be longer, requiring seed-pinning and reproducibility management in CI environments. On the other hand, it’s widely recognized as highly effective for domain logic with clear invariants — parsers, serializers, cryptographic code, concurrency — and adoption has expanded, especially in the Python ecosystem, since Hypothesis became popular.
From This Article to the Next
Property-based testing and fuzzing were techniques that increased test coverage by automatically generating the “input side.” Next, we shift that same concern to the “outside” of the system — the perspective of an actual user operating a browser. We trace the generational shift in E2E testing, starting with Selenium in 2004 and continuing through Puppeteer, Cypress, and Playwright.
References
- Koen Claessen, John Hughes, “QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs” (ICFP 2000)
- Koen Claessen, John Hughes, “Testing Monadic Code with QuickCheck” (2002)
- Michal Zalewski, american fuzzy lop (released 2013)
- Google, “Announcing OSS-Fuzz: Continuous Fuzzing for Open Source Software” (Google Testing Blog, December 2016)
- The Go Programming Language, “Go Fuzzing” (go.dev)
- Hypothesis documentation (hypothesis.works)