Clojure test system
I recently found myself working on a Clojure codebase and needed to resurrect a pattern that's served me well in large Clojure codebases for close to a decade.
When testing a Component-based system, being able to consistently start and stop your stateful components is foundational in accomplishing repeatability. Opening database connections, reading from or creating files, and other messy outside-world concerns elevate us beyond simply heating a room.
I start with a macro that approximates Clojure’s let
with the addition of
reliably starting and stopping a given system.
(ns example.test.system
(:require
[com.stuartsierra.component :as component]
[example.system :as system]))
(defmacro with-system
{:arglists ['(system-binding system-map & body)]
:style/indent 1}
[& more]
(let [[[system-binding system-map] & body] more]
`(let [running# (component/start-system ~system-map)
~system-binding running#]
(try
~@body
(finally
(component/stop-system running#))))))
;; We'll need this too!
(defn system
[]
(-> (system/system)
(assoc-in [:database :uri] (str "mem://" (random-uuid)))
;; etc.
))
With this macro and a function that creates a system map, writing tests becomes as simple as:
(deftest hello-world
(test.system/with-system [{:keys [database]} (test.system/system)]
(is (= 1 (database/inc database 0)))))
Now modifying our system configuration is as simple as associating values into our system map, which colocates the relevant configuration with our tests. Functions for building ‘mocked’ systems for things like authentication or error states are also trivial.
If you’re using clj-kondo to lint your project, you’ll additionally need to add
a :lint-as
rule to your .clj-kondo/config.edn
.
{:lint-as
{home.test.system/with-system clojure.core/let}}