~ / track C / clojure ecosystem
clojure.test and test runners
Basicclojure.test is Clojure's bundled testing library. No dependency,
no DSL beyond what the language already gives you — tests are functions
registered by deftestclojure.test/deftestDefine a unit test.view on clojuredocs → and assertions are macros that read like data.
Most Clojure projects start here. Some teams add kaocha or eftest
for nicer output and parallel execution, but the assertion surface stays
the same.
The core API
(ns myapp.core-test
(:require [clojure.test :refer [deftest is testing are use-fixtures]]
[myapp.core :as sut])) ;; sut = system under test
(deftest add-works
(is (= 4 (sut/add 2 2)))
(is (= 0 (sut/add -1 1))))
(deftest add-edge-cases
(testing "with zero"
(is (zero? (sut/add 0 0))))
(testing "with negatives"
(is (neg? (sut/add -5 -3)))))- deftestclojure.test/deftestDefine a unit test.view on clojuredocs → registers a test under a var;
(run-tests)finds them by metadata. - isclojure.test/isAssert a single boolean expression inside a deftest.view on clojuredocs → is the universal assertion. It macroexpands the expression so failure messages include both sides.
- testingclojure.test/testingGroup assertions under a label.view on clojuredocs → adds a string to the failure path — handy for grouping.
areruns a template across rows:(are [in out] (= out (sut/inc in)) 1 2, 5 6, -1 0)— one row per case, columns named by the binding vector.
Fixtures
(defn with-db [test-fn]
(let [db (start-test-db)]
(try (binding [*db* db] (test-fn))
(finally (stop-test-db db)))))
(use-fixtures :each with-db) ;; run per deftest
(use-fixtures :once start-and-stop-system) ;; run once for the ns:each wraps every test; :once wraps the whole namespace. Fixtures
compose — multiple fixtures stack.
with-redefsclojure.core/with-redefsTemporarily replace root bindings — useful in tests.view on clojuredocs → for mocking
(deftest fetch-user-test
(with-redefs [http/get (fn [_] {:status 200 :body "{\"id\":1}"})]
(is (= 1 (:id (sut/fetch-user 1))))))with-redefsclojure.core/with-redefsTemporarily replace root bindings — useful in tests.view on clojuredocs → ([[dynamic-vars-and-binding]]) rebinds any var for the form's
extent. JVM-global, so tests using it must not run in parallel without
care. For purer alternatives, inject dependencies through a ctx map
([[component-systems]]).
Running tests
clj -X:test # cognitect-labs test-runner alias
bin/kaocha # kaocha test runner
lein test # LeiningenIn the REPL, (clojure.test/run-tests 'my.ns) or invoke from your
editor — Calva, CIDER, and Cursive all bind it to a key.
Test runners compared
| clojure.test (built-in) | kaocha | eftest | |
|---|---|---|---|
| Bundled | Yes | No | No |
| Parallel | No | Yes | Yes |
| Watch mode | No | Yes | Partial |
| Profiles | Manual | Yes (tests.edn) | Profile via deps aliases |
| Failure output | Plain | Pretty diff | Pretty diff + capture |
| Plugins (cloverage, junit) | Manual | First-class | Manual |
| Used by | Most libraries | Many app codebases | Some teams |
kaocha is the modern default for application code. Libraries usually
stick with clojure.test so they have one fewer dependency.
Property-based tests
clojure.test.check ([[test-check]]) integrates as defspec:
(require '[clojure.test.check :as tc]
'[clojure.test.check.properties :as prop]
'[clojure.test.check.clojure-test :refer [defspec]])
(defspec reverse-twice-is-identity 100
(prop/for-all [v (gen/vector gen/int)]
(= v (reverse (reverse v)))))defspec is a deftestclojure.test/deftestDefine a unit test.view on clojuredocs → that runs N random cases. Failures are
shrunk to a minimal counterexample.
Try it
What to test
| Layer | Test style |
|---|---|
| Pure functions | isclojure.test/isAssert a single boolean expression inside a deftest.view on clojuredocs → + defspec — fast, deterministic |
| HTTP handlers | Spin a real Ring stack with a test DB, hit it with ring-mock |
| Database queries | Real DB in a fixture (Postgres in Docker, in-memory XTDB) |
| External APIs | with-redefsclojure.core/with-redefsTemporarily replace root bindings — useful in tests.view on clojuredocs → or [[component-systems]] with a fake protocol impl |
core.async flows | Use <!!clojure.core.async/<!!Blocking take from a channel (outside go).view on clojuredocs → with a timeout in tests; avoid goclojure.core.async/goSpawn a parked-coroutine block backed by the async thread pool.view on clojuredocs → blocks blocking forever |
| Macros | Test the expansion and one runtime example each |
The prevailing style is integration tests over heavy mocking — wire a real DB, hit a real Ring server, and reserve with-redefsclojure.core/with-redefsTemporarily replace root bindings — useful in tests.view on clojuredocs → stubs for outside-world I/O (HTTP calls to third parties, time, randomness).
Real-world
| Tool | Use |
|---|---|
kaocha | Default runner for app code; --watch reload-on-save |
eftest | Parallel runner with capture; popular before kaocha |
cloverage | Code coverage |
ring-mock | Build fake request maps to call handlers directly |
matcher-combinators | Better assertion DSL: (is (match? {:a 1} actual)) |
etaoin | Browser automation for end-to-end tests |
clj-test-containers | Real Postgres/Kafka/etc. in tests via Docker |
Check yourself
? quiz
Inside a test you want to make `http/get` return a canned response. Which tool fits?
Exercise
Write clojure.test cases for the add function from earlier:
- Use isclojure.test/isAssert a single boolean expression inside a deftest.view on clojuredocs → for three sample inputs.
- Use
arefor the same three cases as a table. - Add a
defspecthat asserts(+ a b) = (+ b a)for any two ints.
Then add a :once fixture that prints "starting" before any test and
"done" after the last. Run the suite and observe the fixture firing
exactly once.