Skip to content
AgileLogics
7 min read

Testing with clojure.test

Prerequisites: Namespaces, Error handling

clojure.test ships with Clojure and is the standard testing library. Pure functions on plain data make Clojure code unusually easy to test.

Writing tests

(ns inventory.core-test
  (:require [clojure.test :refer [deftest is are testing run-tests]]))

(defn restock [inventory item qty]
  (when-not (pos? qty)
    (throw (ex-info "Quantity must be positive" {:qty qty})))
  (update inventory item (fnil + 0) qty))

(deftest restock-test
  (testing "adds to an existing item"
    (is (= {:apple 5} (restock {:apple 2} :apple 3))))
  (testing "creates a missing item"
    (is (= 4 (:pear (restock {} :pear 4)))))
  (testing "rejects invalid quantities"
    (is (thrown-with-msg? clojure.lang.ExceptionInfo #"positive"
                          (restock {} :apple 0)))))

(deftest restock-table-test
  (are [start qty expected] (= expected (:x (restock {:x start} :x qty)))
    0 1 1
    5 5 10
    1 99 100))

(run-tests)

Testing inventory.core-test

Ran 2 tests containing 6 assertions.
0 failures, 0 errors.
  • deftest defines a test; is checks an expression; testing adds a description to groups of assertions.
  • are runs the same assertion over a table of values.
  • thrown? / thrown-with-msg? check for exceptions.
  • run-tests runs the tests in the current namespace — in a project, your test runner (clj -X:test, lein test, or the editor) does this.

Reading failures

(require '[clojure.test :refer [deftest is run-tests]])

(defn average [xs] (/ (reduce + xs) (count xs)))

(deftest average-test
  (is (= 2 (average [1 2 3])))
  (is (= 2.5 (average [2 3]))))

(run-tests)

Testing user

FAIL in (average-test) (main.clj:7)
expected: (= 2.5 (average [2 3]))
  actual: (not (= 2.5 5/2))

Ran 1 tests containing 2 assertions.
1 failures, 0 errors.

The failure shows the expression, the expected value, and the actual result: (average [2 3]) returns the ratio 5/2, which isn't = to the double 2.5. A good reminder that ratios and doubles differ.

Fixtures

(require '[clojure.test :refer [deftest is use-fixtures run-tests]])

(def db (atom nil))

(defn with-test-db [test-fn]
  (reset! db {:users {}})           ; set up
  (test-fn)
  (reset! db nil))                  ; tear down

(use-fixtures :each with-test-db)

(deftest starts-empty
  (is (empty? (:users @db))))

(deftest can-add
  (swap! db assoc-in [:users 1] "Ada")
  (is (= "Ada" (get-in @db [:users 1]))))

(run-tests)

Testing user

Ran 2 tests containing 2 assertions.
0 failures, 0 errors.

use-fixtures wraps each test (:each) or the whole namespace (:once) with setup and teardown code.

Property-based tests with test.check

(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

(def sort-is-idempotent
  (prop/for-all [v (gen/vector gen/small-integer)]
    (= (sort v) (sort (sort v)))))

(def reverse-twice
  (prop/for-all [v (gen/vector gen/any-equatable)]
    (= v (reverse (reverse v)))))

(println (:pass? (tc/quick-check 200 sort-is-idempotent)))
(println (:pass? (tc/quick-check 200 reverse-twice)))
true
true

Instead of hand-picked examples, a property states something that must hold for all inputs, and test.check tries hundreds of random ones — shrinking any failure to the smallest counterexample.

Tooling

  • Kaocha — a popular test runner with watch mode and nice reports.
  • cognitect test-runner — minimal runner for deps.edn projects.
  • Editors run the test under the cursor through the REPL — the fastest loop of all.