~ / track C / clojure ecosystem

clojure.test and test runners

Basic

clojure.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)))))
  • deftest registers a test under a var; (run-tests) finds them by metadata.
  • is is the universal assertion. It macroexpands the expression so failure messages include both sides.
  • testing adds a string to the failure path — handy for grouping.
  • are runs 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)kaochaeftest
BundledYesNoNo
ParallelNoYesYes
Watch modeNoYesPartial
ProfilesManualYes (tests.edn)Profile via deps aliases
Failure outputPlainPretty diffPretty diff + capture
Plugins (cloverage, junit)ManualFirst-classManual
Used byMost librariesMany app codebasesSome 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

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

What to test

LayerTest style
Pure functionsis + defspec — fast, deterministic
HTTP handlersSpin a real Ring stack with a test DB, hit it with ring-mock
Database queriesReal DB in a fixture (Postgres in Docker, in-memory XTDB)
External APIswith-redefs or [[component-systems]] with a fake protocol impl
core.async flowsUse <!! with a timeout in tests; avoid go blocks blocking forever
MacrosTest 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

ToolUse
kaochaDefault runner for app code; --watch reload-on-save
eftestParallel runner with capture; popular before kaocha
cloverageCode coverage
ring-mockBuild fake request maps to call handlers directly
matcher-combinatorsBetter assertion DSL: (is (match? {:a 1} actual))
etaoinBrowser automation for end-to-end tests
clj-test-containersReal 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:

  1. Use is for three sample inputs.
  2. Use are for the same three cases as a table.
  3. Add a defspec that 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.

 status: new