Build useful Clojure test suites with focused example tests, property-based checks, and clear boundaries between unit, integration, and generative testing.
Testing in Clojure works best when the suite reflects the shape of the code. Pure functions deserve small, direct example tests. Stateful or integration-heavy code needs fixtures and boundary control. test.check becomes valuable when there is a property worth exploring across many inputs, not just because generative testing sounds more advanced.
For ordinary application code, clojure.test is usually the default starting point. It is lightweight, readable, and fits well with REPL-driven development.
1(ns myproject.pricing-test
2 (:require [clojure.test :refer [deftest is testing]]
3 [myproject.pricing :as pricing]))
4
5(deftest discount-is-capped
6 (testing "discount never exceeds the configured maximum"
7 (is (= 30 (pricing/cap-discount 30 50)))
8 (is (= 15 (pricing/cap-discount 30 15)))))
The test is valuable because it states one behavior clearly. It does not need a large framework, and it does not pretend to test more than it actually does.
Different kinds of code need different tests.
test.checkThe mistake is not using “too little testing technology.” The mistake is blurring unit, integration, and generative tests until nobody knows what a failing test actually means.
flowchart TD
CODE["Code under test"] --> DECIDE{"What kind of behavior?"}
DECIDE -->|Pure function| UNIT["Focused example tests"]
DECIDE -->|Stateful or I/O boundary| INTEG["Fixtures and integration tests"]
DECIDE -->|Rule over many inputs| PROP["Property-based tests"]
clojure.test for Clarityclojure.test works well because it keeps assertions close to the behavior being described.
1(use-fixtures
2 :each
3 (fn [test-fn]
4 ;; setup
5 (test-fn)
6 ;; teardown
7 ))
Fixtures are useful when a test needs controlled mutable state or external resources. They are not a substitute for isolating dependencies in the design itself.
test.check for Properties, Not for EverythingProperty-based testing shines when there is a law or invariant that should hold across a broad input space.
1(ns myproject.collections-test
2 (:require [clojure.test.check.clojure-test :refer [defspec]]
3 [clojure.test.check.generators :as gen]
4 [clojure.test.check.properties :as prop]))
5
6(defspec reverse-twice-returns-original 100
7 (prop/for-all [xs (gen/vector gen/int)]
8 (= xs (reverse (reverse xs)))))
That is a strong property because it expresses something deeper than one example.
Weak uses of test.check look like this:
The goal is not to make testing more abstract. The goal is to discover bugs that example tests would probably miss.
A practical mix for many codebases is:
That gives breadth without turning the suite into a slow, confusing pile of overlapping checks.
The strongest test suites are readable enough that a failing test immediately tells the team what kind of contract broke.