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.
A property test combines:
quick-check or defspecThe 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 Suite1(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.
Good properties usually express something durable:
Weak properties usually fail in subtler ways:
If the property does not reflect the contract, thousands of passing trials still do not buy much confidence.
Generator design is where many property tests become either powerful or misleading.
Use generators that reflect the domain:
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.
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.
Property testing tends to be strongest for:
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.
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.