Skip to content

Clojure Unit Testingλ︎

REPL Driven development and Unit testing in Clojure

The function is the unit under test in Clojure. All public functions that form the API of their respective namespace should have a matching test, i.e. (deftest) definition.

clojure.test namespace provides functions for defining and running unit tests and is available in the Clojure library for any project to use.

Clojure test unit test example - status monitor dashboard function and unit test

Unit Test Principlesλ︎

  • A test namespace for each src namespace under test
  • A deftest function for each function under test, named after the function its testing with -test at the end of the name
  • Multiple assertions (is are) for one function
  • is defines an assertion returning true (test pass) or false (test fail), typically a comparison between a known value and the result of a function call
  • are to testing similar functionality with different data sets (or use generative testing)
  • testing to logically group assertions and provide a meaningful description of that grouping (easier to identify tests when they fail)
  • use-fixtures to call fixture functions that setup and tear down any state required for test(s) to run
  • Test API rather than implementation
  • test generic helper or private functions through public functions of each namespace (minimise test churn and time to run all tests)
  • ^:helper meta-data on deftest for more generic functions, to skip those tests via a test selector
  • Use generative testing to create more maintainable test code with more extensive range of data
  • Use test selectors with a test runner to selectively run tests and optimise speed of test runs
  • Limit mocking of systems to integration tests (although mocking data is good everywhere)
Code should evaluate or have line comments

All Clojure code should be valid syntax and able to be evaluated (compiled), even code within a (comment ) expression or after a #_ reader comment.

Code commented with a line comment, ;;, will not be read by Clojure and cannot cause compilation errors when evaluated

Running testsλ︎

Test runners can run be run in the REPL used for development or run separately via the command line and continuous integration tasks.

Clojure unit testing approach - editor and command line

Run tests in Editor connected REPLλ︎

Using an editor connected REPL keeps the workflow in one tool and helps maintain focus. Using editor commands to run the tests and navigable error reports provides an effective flow to run and debug issues.

Ensure test directory is on the class path when evaluating tests in the REPL, otherwise the (deftest) test definitions may not be found.

If functions or their associated tests are changed, they should be evaluated in the REPL before running tests to ensure those changes are loaded into the REPL.

If renaming a function or deftest, the original name should be removed from the REPL to avoid phantom tests (older definitions of tests that were evaluated in the REPL and still run, even though those tests are no longer in the source code).

Editors may include a command to remove function or test definitions, e.g. CIDER has undef command

The original name can also be removed using Clojure (ns-unmap 'namespace 'name), where namespace is where the name of the function or test is defined and name is the name of the function or test.

(ns practicalli.system-monitor)                ; namespace definition
(defn dashboard [] ,,,)                        ; original function name
(defn dashboard-page [] ,,,)                   ; new function name
(undef 'practicalli.system-monitor 'dashboard) ; remove original function name

Stop and start the REPL process ensures all function and tests are correctly loaded

Command line test runnersλ︎

Command line test runners (i.e. koacha, Cognitect Labs) load function and test definitions from the source code files each time, ensuring tests are run and a clean REPL state is created on each run. This clearly defined REPL state is especially valuable for running repeatable integration tests.

Automate running the tests using a watch process, giving instant fast feedback, especially when displaying both the editor and test runner command line.

test runner can be configure to run only selective tests (i.e kaocha)

Run all tests (including integration tests) via the command line before pushing commits to ensure all changes to the code have been tested.

If tests are not running in the REPL or are returning unexpected errors, a command line test runner is a useful way to diagnose if it is the test code or test tools causing the error.

The CLI approach is also more robust for longer running tests than running within an editor.

Avoid stale tests

Running tests via a command line test runner will never experience stale tests, as long as all relevant changes are saved to the source code files.

Run tests in the REPLλ︎

clojure.test includes the run-tests function that runs tests (deftest definitions) in given namespaces and run-all-tests which runs all tests in all namespaces.

(run-all-tests)  ; run all tests in all namespaces

(run-tests 'practicalli.system-monitor-test) ; run all tests in practicalli.system-monitor-test

run-tests and run-all-tests are a less common approach as the command line and editor driven test runners provide a rich set of features

Project structure with testsλ︎

For each source code file in src there should be a corresponding file in test directory with the same name and _test postfix.

For example, code to test the src/codewars/rock_paper_scissors.clj is saved in the file src/codewars/rock_paper_scissors_test.clj file.

Clojure project structure - src and test branches

Example project: CodeWars: Rock Paper Scissors

Source and Test Namespacesλ︎

As with file names, the namespaces for each test code file is the same as the source code it is testing, with a -test postfix.

codewars/rock-paper-scissors source code namespace will have a matching codewars/rock-paper-scissors-test namespace.

Create Projects from templates

Templates typically include a parallel test and src directory structure. The clj-new tool has build it templates (app, lib) and will create src and test directories in the projects it creates.

clojure -T:project/new :template app :name practicalli/rock-paper-scissors-lizard-spock

Project Examples: Code challenges with unit testsλ︎