Property-Based Testing with test.check

Learn how to use `test.check` for property-based testing in Clojure, including generators, `defspec`, shrinking, and how to choose properties that protect real invariants.

Property-based testing: A style of testing where you describe a rule that should hold across many generated inputs instead of listing only a handful of fixed examples.

test.check is one of the most useful ways to expand coverage in Clojure without multiplying brittle example tests. It does not replace examples. It complements them by exercising invariants across a broader input space and then shrinking failures toward small, debuggable cases.

The Three Parts of a Property Test

A property test combines:

  • a generator for values or structures
  • a property describing what must stay true
  • a runner such as quick-check or defspec

The beginner guide from Clojure’s docs emphasizes this split clearly. Generators, properties, and runners are separate pieces, which makes the workflow easier to reason about.

defspec Lets Properties Live in the Normal Test Suite

1(require '[clojure.test.check.clojure-test :refer [defspec]])
2(require '[clojure.test.check.generators :as gen])
3(require '[clojure.test.check.properties :as prop])
4
5(defspec sort-is-idempotent 100
6  (prop/for-all [v (gen/vector gen/int)]
7    (= (sort v) (sort (sort v)))))

This is often the best starting point because the property runs as part of the ordinary clojure.test flow. You do not need a separate testing universe just to add broader input exploration.

Strong Properties Protect Real Invariants

Good properties usually express something durable:

  • parse then render preserves meaning
  • encoding then decoding round-trips correctly
  • sorting is idempotent
  • totals never become negative
  • a normalization step is stable when reapplied

Weak properties usually fail in subtler ways:

  • they restate the implementation instead of the contract
  • they are too trivial to catch a meaningful bug
  • they allow input shapes that the real system would reject immediately

If the property does not reflect the contract, thousands of passing trials still do not buy much confidence.

Generator Quality Determines Test Value

Generator design is where many property tests become either powerful or misleading.

Use generators that reflect the domain:

  • realistic shapes
  • edge-heavy values
  • boundary sizes
  • mixed valid and near-valid cases when appropriate
1(def email-gen
2  (gen/fmap
3    (fn [[name domain]]
4      (str name "@" domain ".example"))
5    (gen/tuple (gen/not-empty gen/string-alphanumeric)
6               (gen/not-empty gen/string-alphanumeric))))

A generator that produces every possible string is often too noisy. A generator that produces only already-perfect inputs is too forgiving. The useful middle ground teaches the most.

Shrinking Is Part of the Payoff

One of test.check’s biggest advantages is shrinking. When a property fails, the library tries to reduce the failing input to a minimal example. That matters because property testing is only useful if the failure report helps you debug, not just panic.

A failure on a giant nested value is hard to reason about. A shrunk case with one or two small elements is usually much more useful.

Where Property Tests Help Most

Property testing tends to be strongest for:

  • parsers and serializers
  • collection transforms
  • scheduling and ordering logic
  • normalization pipelines
  • pure domain rules
  • state transitions with crisp invariants

It is weaker when the contract is mostly visual, operational, or dominated by external systems. In those areas, example tests and integration checks usually carry more of the load.

Practical Heuristics

Start with one or two important invariants. Keep example tests for named business cases. Add property tests where the input space is large enough that examples alone will underrepresent the risk. If a property is hard to state cleanly, that is often a design signal worth paying attention to.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026