clojure.test is the default unit testing framework included in the Clojure programming language. It provides a simple, macro-based system for writing test cases, assertions, and test suites. Designed to leverage Clojure's functional and data-oriented nature, it integrates seamlessly with the REPL and build tools, offering features like test fixtures, property-based testing via conjunction with other libraries, and custom assertion definitions. Its lightweight design makes it a foundational tool for Clojure developers to ensure code reliability.
1 Core Concepts and Syntax
1.1 Defining Tests with deftest
The deftest macro defines a named test function. It wraps a body of assertions and is automatically recognized by the test runner. For example:
(deftest addition-test
(is (= 4 (+ 2 2))))
deftest creates a var with the given name and associates metadata indicating it is a test. Tests can be placed anywhere in the source tree, typically in files under test/ mirroring the source namespace.
1.2 Assertion Macros
1.2.1 is
The is macro is the primary assertion form. It takes an expression and optionally a message. If the expression evaluates to a truthy value, the test passes; otherwise it reports a failure with details about the expected and actual values (when the expression involves = or other known predicates). Example:
(is (= 3 (inc 2)) "Increment of 2 should be 3")
1.2.2 are
The are macro simplifies repetitive assertions by specifying a template with placeholders and a sequence of data tuples. It expands into multiple is forms. For instance:
(are [x y] (= x y)
1 1
2 2
3 3)
This generates three separate tests, each checking equality between the two values. are supports any predicate expression.
1.2.3 thrown? and thrown-with-msg?
thrown? asserts that a given expression throws an exception of a specific class. It takes the expected exception class and the expression, and returns the exception if caught. thrown-with-msg? additionally verifies the exception's message matches a regular expression or string. Example:
(is (thrown? ArithmeticException (/ 1 0)))
(is (thrown-with-msg? Exception #"Index" (nth [] 0)))
These macros allow precise testing of error conditions.
1.3 Test Metadata and Documentation
Tests defined with deftest can carry arbitrary metadata, which influences how they are run. Common metadata includes :test (automatically set), :focus (to run only marked tests), and :timeout. Documentation strings are attached using the standard ^{:doc "..."} metadata. This metadata can be queried by custom runners or reporting tools.
2 Running Tests
2.1 Using run-tests
run-tests runs all tests in one or more namespaces. When called without arguments, it runs tests in the current namespace. It returns a summary map containing counts of tests, assertions, failures, and errors. Example:
(run-tests 'my.namespace)
2.2 Using run-all-tests
run-all-tests scans all loaded namespaces for tests and runs them. It is typically used at the end of a test suite or build script. The function can accept namespaces to include or exclude via options. It prints a comprehensive report to *out*.
2.3 Integration with Leiningen and tools.deps
2.3.1 Leiningen :test-selectors
Leiningen, a popular build tool for Clojure, supports :test-selectors in project.clj. This allows defining named sets of tests based on metadata. For example:
:test-selectors {:default (constantly true)
:integration :integration}
Then lein test :integration runs only tests with ^:integration metadata.
2.3.2 deps.edn :aliases
For tools.deps (Clojure’s official dependency manager), test execution is configured via aliases. A typical alias adds test paths and the test runner:
{:aliases {:test {:extra-paths ["test"]
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}
:main-opts ["-m" "user" "run-tests"]}}}
User-defined aliases can also incorporate metadata filters.
2.4 Reporting and Output Formats
2.4.1 Default Reporter
The default test reporter prints a line for each namespace and a summary at the end. It uses ANSI colors for failures and errors. Assertions that pass are shown as dots (.), failures as F, and errors as E. The output includes details such as the expected vs actual values for each failing is expression.
2.4.2 Custom Reporter Implementation
The reporting mechanism is extensible. A custom reporter is a function that takes a map of type :begin-test-ns, :end-test-ns, :pass, :fail, :error, etc., and outputs as desired. Developers can replace the default reporter by binding clojure.test/report to their own function. This enables integration with CI systems, logging, or custom dashboards.
3 Test Organization and Fixtures
3.1 Grouping Tests with use-fixtures
use-fixtures defines functions that run around tests. It accepts two keywords: :each (runs before and after each test in the namespace) and :once (runs once for the entire namespace). The fixture function takes a thunk (a function of no arguments) that must be called to proceed.
3.1.1 :each Fixtures
:each fixtures wrap every individual test. They are useful for setting up per-test state, such as creating a fresh database connection or clearing temporary data. Example:
(use-fixtures :each (fn [f] (println "Before") (f) (println "After")))
3.1.2 :once Fixtures
:once fixtures wrap the entire suite of tests in the namespace. They are called before any test runs and after all tests complete. Common uses include starting a web server or loading a large dataset. The fixture must call its argument to execute the tests.
3.2 Nested Test Suites
clojure.test does not natively support nested test suites at the framework level. However, tests can be organized by naming conventions or by using Leiningen’s :test-paths and multiple namespaces. Each clojure namespace corresponds to a flat test group. For deeper hierarchy, developers often create auxiliary namespaces (e.g., myapp.core-test, myapp.core.parsing-test) and run them with run-all-tests.
3.3 Test Hooks and Lifecycle Management
Beyond fixtures, lifecycles can be managed using clojure.test/with-test (deprecated) or by combining fixtures with finally blocks. The clojure.test API also includes test-vars for programmatic execution of specific vars. Advanced lifecycle management is typically handled through build tools (e.g., Leiningen’s :test-hooks) or by integrating with state management libraries.
4 Advanced Features
4.1 Property-Based Testing with test.check
test.check is a separate Clojure library for property-based testing (PBT). It is not part of clojure.test but integrates smoothly.
4.1.1 Integration Methods
The most common integration uses clojure.test to wrap test.check properties. The defspec macro (from clojure.test.check.clojure-test) generates deftest-compatible tests that run a given property with random data. Alternatively, properties can be embedded inside regular deftest blocks using is with a tc/quick-check call.
4.1.2 defspec and similar patterns
defspec takes a name, options (like number of trials), and a property expression. It automatically registers the property as a test. Example:
(defspec reverse-twice-is-identity 100
(prop/for-all [v (gen/vector gen/int)]
(= v (reverse (reverse v)))))
This runs 100 random trials of the property and reports failures through the standard clojure.test infrastructure.
4.2 Custom Assertion Macros
4.2.1 Extending is with custom predicates
The is macro uses a protocol (clojure.test/assert-expr) to handle different assertion forms. Developers can extend this protocol by defining methods for new symbols. For example, to add a between? predicate:
(defmethod clojure.test/assert-expr 'between? [msg form]
;; custom handling...
)
Then (is (between? 1 5 3)) works with specialized reporting.
4.2.2 Writing new assertion helpers
New assertion macros are typically written using is internally to preserve failure reporting. For instance:
(defmacro assert-contains [expected coll]
`(is (some #{~expected} ~coll) (str "Expected " ~coll " to contain " ~expected)))
These helpers can be collected in a separate namespace and reused across projects.
4.3 Asynchronous Testing
4.3.1 Testing core.async channels
clojure.test does not natively handle asynchronous operations. Testing core.async channels requires manual blocking or using clojure.core.async/<!! to synchronously extract values. Common patterns involve wrapping async code in a deftest and using (is ...) after obtaining results from channels with timeouts.
4.3.2 Handling timeouts
To avoid hanging tests, developers can use clojure.core.async/timeout and alts!! to set a maximum wait. Example:
(let [ch (some-async-op)
[val port] (alts!! [ch (timeout 1000)])]
(is (= port ch) "Operation timed out"))
Alternatively, dedicated libraries like clojure.test.async provide macros that integrate with clojure.test for cleaner async testing.
5 Best Practices and Patterns
5.1 Naming Conventions
Test namespaces mirror their source counterparts by suffixing -test to the source namespace (e.g., myapp.core-test). Test functions use descriptive names that indicate the scenario being tested, often following the pattern test-<function>-<behavior> or using deftest with a short phrase (e.g., deftest addition). Metadata like ^:slow or ^:unit helps with selective test execution.
5.2 Test-Driven Development (TDD) in Clojure
The interactive nature of Clojure’s REPL makes TDD natural. Developers typically write a failing deftest, implement the minimal code to pass it, and refactor. The tight feedback loop of run-tests in the REPL encourages frequent testing. Many teams use clojure.test in combination with expect-style libraries for finer-grained assertions.
5.3 Managing Test Data and State
Pure functions are easiest to test; side effects are isolated using fixtures or with-redefs. Test data is often constructed using plain maps and vectors, leveraging Clojure’s immutable data structures. For stateful resources (databases, files), use :once fixtures to set up and tear down shared resources, and :each fixtures to reset per-test state.
5.4 Continuous Integration Strategies
In CI pipelines, tests are run with build tool commands (e.g., lein test or clojure -M:test). Reporting must be machine-readable; custom reporters can output JUnit XML or JSON for CI systems. Parallel test execution is possible by splitting tests across namespaces and using CI parallelism. Code coverage tools (e.g., Cloverage) integrate with clojure.test to measure test coverage.
6 Comparison and Interoperability
6.1 Differences from clojure.test in ClojureScript
clojure.test is also available in ClojureScript, but it operates at the ClojureScript compiler level. Certain features like thrown? behave differently due to the lack of Java exceptions. Asynchronous testing in ClojureScript often requires special handling with cljs.test and async macros. The reporting formatting may differ based on the JavaScript environment.
6.2 Using clojure.test with other test libraries (Midje, Speclj)
clojure.test is compatible with libraries like Midje (which provides a fact syntax) and Speclj (a BDD-style library). These libraries often replace the assertion macros but can run alongside clojure.test tests. Some libraries convert their own tests to clojure.test-compatible forms for unified reporting. Hybrid usage is possible by including both frameworks in the classpath, though it is not common.
6.3 Migration from clojure.test to other frameworks
Migrating away from clojure.test to another framework (e.g., Midje, Speclj, or ScalaCheck-styled test.check) typically involves rewriting deftest blocks into the target framework’s syntax. Because clojure.test is simple and non-opinionated, the migration path often involves composing new macros around existing test functions. Property-based tests from test.check can be kept if the new framework supports them. Team preferences and project requirements guide migration decisions, with many choosing to stay with clojure.test due to its ubiquity and low overhead.