~ / 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 deftest and assertions are macros that read like data.
Most Clojure projects start here. Many graduate to 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)))))
deftestregisters a test under a var;(run-tests)finds them by metadata.isis the universal assertion. It macroexpands the expression so failure messages include both sides.testingadds a string to the failure path — handy for grouping.areruns a template across rows:(are [a b] (= a b) 1 1, 2 2, 3 3).
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-redefs for mocking
(deftest fetch-user-test
(with-redefs [http/get (fn [_] {:status 200 :body "{\"id\":1}"})]
(is (= 1 (:id (sut/fetch-user 1))))))
with-redefs ([[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 # Leiningen
In 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 deftest that runs N random cases. Failures are
shrunk to a minimal counterexample.
Try it
What to test
| Layer | Test style |
|---|---|
| Pure functions | is + 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-redefs or [[component-systems]] with a fake protocol impl |
core.async flows | Use <!! with a timeout in tests; avoid go blocks blocking forever |
| Macros | Test the expansion and one runtime example each |
The Clojure community leans toward integration tests over heavy mocking — wire a real DB, hit a real Ring server. Mocks are reserved for outside-world I/O.
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
isfor 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.