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.
deftestdefines a test;ischecks an expression;testingadds a description to groups of assertions.areruns the same assertion over a table of values.thrown?/thrown-with-msg?check for exceptions.run-testsruns 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.ednprojects. - Editors run the test under the cursor through the REPL — the fastest loop of all.