Getting Started with Clojure Unit Testing: A Simple Tutorial

Getting Started with Clojure Unit Testing: A Simple Tutorial

Testing is one of the aspects of software development that you can not hide yourself from. At least if you work in the industry, you shouldn't. Testing is vital to battle against regression in the code, meaning that when you add new features, you are not breaking the previous functionality. Of course, over-testing can be harmful and slow you down if you need to spend more time updating tests than the feature code itself.

This post is not about what, when, and if you should test but a tutorial on getting started when you've decided that you need them and how to integrate the testing into your workflow.

How Clojure is Different From the Mainstream

In other dynamic languages, you often run tests in watch mode so that your test runner triggers a new run whenever you save the file. This offers a fast feedback loop on your changes. Clojure differs from the others in that it has the REPL that can be used to evaluate the code while developing and testing it without running all of the tests every time. Let's get back to the topic of running the tests after we've gone through how to write tests.

Writing Tests

Clojure has its unit testing framework core.test ; therefore, extra dependencies are unnecessary when writing the tests. At the core of testing are assertions, and we use the macro clojure.test/is that takes any predicate with an optional message describing it.

(require '[clojure.test :as t])

(t/is (true? true) "true is true")
;; => true

(t/is (= true false) "true equals false")
;; => false

;; REPL output
;;
;; FAIL in () (NO_SOURCE_FILE:4)
;; true equals false
;; expected: (= true false)
;;  actual: (not (= true false))

You can think of this as a function call to evaluate whether a given predicate (boolean or a function that produces a boolean) is true.

Assertions are used within deftests macro that returns a function to evaluate the assertions inside.

(t/deftest test-assertion
  (t/is (= true false) "true equals false")
  (t/is (true? true) "true is true"))
;; => #'core-test/test-assertion

(test-assertion)
;; REPL output
;;
;; FAIL in (test-assertion) (NO_SOURCE_FILE:17)
;; expected: (= true false)
;;  actual: (not (= true false))

Read the Clojure style guide on naming conventions

If you read the REPL outputs, you might have noticed we lost the message attached to our failing assertion. With deftests we can use testing to define the context for the test.

(t/deftest test-assertion
  (t/testing  "true equals false"
    (t/is (= true false)))
  (t/testing "true is true"
      (t/is (true? true))))
;; => #'core-test/test-assertion

(test-assertion)

;; REPL output
;;
;; FAIL in (test-assertion) (NO_SOURCE_FILE:25)
;; true equals false
;; expected: (= true false)
;;   actual: (not (= true false))

You can see that we got our descriptions back. And that's pretty much the basics of testing in Clojure. There are more tools in clojure.test like are and use-fixture. Let me know if you want to learn more about them. But now, back to test runners.

There are several test runner options to choose from. You could use the functions provided by core.test, Cognitect's test-runner, or Kaocha, to name a few. We'll be using Kaocha this time. It is an extendable, build-tool agnostic test runner.

Setup the project

Let's set up a project with Kaocha testing in mind so that we have a deps.edn alias for running the tests once and in watch mode.

Here's the file structure

├── deps.edn
├── src
│   └── core.clj
└── test
    └── core_test.clj

Let's fill in the deps.edn first and add Kaocha as a test dependency.

{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}}

 :aliases
 {:test
  {:extra-paths ["test"]
   :extra-deps  {lambdaisland/kaocha {:mvn/version "1.77.1236"}}
   :exec-fn     kaocha.runner/exec-fn
   :exec-args   {:skip-meta :slow}}

  :watch
  {:exec-args   {:watch?     true
                 :skip-meta  :slow
                 :fail-fast? true}}}}

Under aliases, we have keys :test and :watch and these work together. With the Clojure CLI tool, we can execute the :exec-fn from the alias configuration by using the -X command line option

-X is configured with an arg map with :exec-fn and :exec-args keys, and stored under an alias in deps.edn:

Deps and CLI Reference: Execute a Function

so by running clj -X:test we can run the tests once, and with clj -X:test:watch we can continuously run the tests whenever we save the source code file.

Let's write something to test in core.clj namespace.

(ns core)

(defn multiply [x y]
  (* x y))

And write the tests we saw earlier, plus a test for multiplication in core-test.

(ns core-test
  (:require [clojure.test :as t]
            [core :as sut]))

(t/deftest test-assertion
  (t/testing  "true equals false"
    (t/is (= true false)))
  (t/testing "true is true"
      (t/is (true? true))))

(t/deftest multiply-test
  (t/testing "multiplication works as expected"
    (t/is (= 4 (sut/multiply 2 2)))))

SUT (Software Under Test) I picked up this from Practicalli years ago and it stuck with me.

And now we are ready to test.

By default, Kaocha uses the src and tests paths so we can run the tests without explicit configuration. There are various configuration options with plugins, so check out the docs to see what's possible.

Testing in the REPL

Clojure developers usually use their REPL to evaluate the tests, and in my case, it would be in Emacs with CIDER using one of the cider-test-* commands that I have behind keyboard shortcuts.

The same can be done with VS code and Calva. Read from the docs how to run tests with Calva.

Conclusion

Testing in Clojure is pretty effortless. We do not need much ceremony to start testing; we need a namespace for the tests and a test runner of our choice.

I hope you found this helpful, and thank you for reading.

Feel free to reach out and let me know what you think—social links in the menu.