Kaocha Test Runner: Advanced Configuration and Plugins
Kaocha is the most flexible test runner in the Clojure ecosystem. While lein test and clojure -M:test get you started, Kaocha gives you fine-grained control over test selection, execution order, output formatting, and extensibility through plugins. This guide covers advanced Kaocha configuration for teams that need more than defaults.
Why Kaocha Over lein test
The standard Clojure test runners work, but they offer limited control:
- No built-in test filtering by metadata
- No watch mode for rapid development cycles
- Fixed output formats
- No plugin system for custom behavior
Kaocha addresses all of these with a data-driven configuration model and a hook system that lets you intercept any phase of test execution.
Installation
Add Kaocha to your deps.edn:
{:aliases
{:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
:main-opts ["-m" "kaocha.runner"]}}}Or for Leiningen, in project.clj:
:profiles {:dev {:dependencies [[lambdaisland/kaocha "1.91.1392"]]}}
:aliases {"kaocha" ["run" "-m" "kaocha.runner"]}Create a tests.edn file in the project root — this is the central configuration:
#kaocha/v1
{}An empty map uses all defaults: discovers tests in test/ directories, runs all suites, uses the documentation reporter.
Test Suites
Kaocha organizes tests into suites. A suite maps to one source tree with a specific test type. The default suite is named :unit:
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test"]
:source-paths ["src"]
:ns-patterns [#"-test$"]}]}You can define multiple suites for different test categories:
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test/unit"]
:ns-patterns [#"-test$"]}
{:id :integration
:test-paths ["test/integration"]
:ns-patterns [#"-integration$"]}
{:id :e2e
:test-paths ["test/e2e"]
:ns-patterns [#"-e2e$"]}]}Run a specific suite:
clojure -M:test --suite unit
clojure -M:<span class="hljs-built_in">test --suite integrationTest Filtering with Metadata
Kaocha supports filtering tests by metadata attached to deftest forms. Tag tests with keywords:
(deftest ^:slow large-dataset-test
(testing "processes 1M records"
...))
(deftest ^:unit basic-validation-test
(testing "validates email format"
...))
(deftest ^:integration ^:db database-query-test
(testing "fetches records from database"
...))Filter at the command line:
# Run only :unit tagged tests
clojure -M:<span class="hljs-built_in">test --focus-meta :unit
<span class="hljs-comment"># Skip :slow tests
clojure -M:<span class="hljs-built_in">test --skip-meta :slow
<span class="hljs-comment"># Multiple filters (AND logic)
clojure -M:<span class="hljs-built_in">test --focus-meta :integration --focus-meta :dbOr configure filters in tests.edn:
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test"]
:kaocha.filter/skip-meta [:slow :integration]}]}Reporters
Kaocha ships with several reporters. Configure the reporter in tests.edn:
#kaocha/v1
{:reporter kaocha.report/documentation}Available built-in reporters:
| Reporter | Description |
|---|---|
kaocha.report/documentation |
Verbose, shows test names hierarchically |
kaocha.report/progress |
Dots per test, summary at end |
kaocha.report/tap13 |
TAP version 13 format |
kaocha.report.junit/xml |
JUnit XML for CI systems |
For CI, use JUnit XML output:
#kaocha/v1
{:reporter kaocha.report.junit/xml}This writes to test-results/kaocha/results.xml by default — compatible with GitHub Actions, CircleCI, and most CI platforms.
For development, use a custom reporter that shows only failures:
(ns myapp.reporter
(:require [kaocha.report :as r]))
(defn only-failures [m]
(case (:type m)
:fail (r/fail-summary m)
:error (r/error-summary m)
:end-run (r/print-summary m)
nil))#kaocha/v1
{:reporter myapp.reporter/only-failures}Watch Mode
Kaocha's watch mode re-runs tests when source or test files change:
clojure -M:test --watchWith configuration for debounce timing:
#kaocha/v1
{:kaocha.watch/debounce 200}The debounce (in milliseconds) prevents multiple rapid re-runs when many files change at once (common with code formatters or save-on-focus).
Watch mode respects your test filters, so you can iterate quickly on a specific namespace:
clojure -M:test --watch --focus myapp.core-testPlugins
Kaocha's plugin system lets you hook into any phase of test execution. Plugins are just maps of lifecycle handlers.
Built-in Plugins
Randomize — runs tests in random order to catch order-dependent failures:
#kaocha/v1
{:plugins [:kaocha.plugin/randomize]
:randomize? true
:seed 42}Use --seed 42 to reproduce a specific failure order:
clojure -M:test --seed 42Profiling — reports the N slowest tests:
#kaocha/v1
{:plugins [:kaocha.plugin/profiling]
:kaocha.plugin.profiling/count 10}Output shows the 10 slowest tests with timing, helping you identify where to optimize.
Orchestra — integrates orchestra spec instrumentation with test runs:
;; deps.edn
{:aliases
{:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}
orchestra/orchestra {:mvn/version "2021.01.01-1"}}}}}#kaocha/v1
{:plugins [:kaocha.plugin/orchestra]}With orchestra enabled, all functions with specs are instrumented during test runs — spec violations in function calls become test failures.
Pause — pauses test execution on failure and waits for keypress, useful for interactive debugging:
#kaocha/v1
{:plugins [:kaocha.plugin/pause]}Writing Custom Plugins
A plugin is a map of lifecycle hooks. All hooks receive and return a test plan or test result:
(ns myapp.kaocha-plugins
(:require [kaocha.plugin :as plugin]))
(defmethod plugin/resolve-reporter :myapp/reporter
[_]
my-reporter-fn)
(defn timing-plugin []
{:kaocha.hooks/pre-test
(fn [test]
(assoc test ::start-time (System/currentTimeMillis)))
:kaocha.hooks/post-test
(fn [test]
(let [elapsed (- (System/currentTimeMillis) (::start-time test))]
(when (> elapsed 1000)
(println "SLOW TEST:" (:kaocha.testable/id test) elapsed "ms"))
test))})Register the plugin:
#kaocha/v1
{:plugins [myapp.kaocha-plugins/timing-plugin]}Available hook points:
:kaocha.hooks/pre-load— before namespace loading:kaocha.hooks/post-load— after namespace loading:kaocha.hooks/pre-run— before entire test run:kaocha.hooks/post-run— after entire test run:kaocha.hooks/pre-test— before each test:kaocha.hooks/post-test— after each test:kaocha.hooks/wrap-run— wraps the entire run (middleware style)
Fixtures and Setup
Kaocha uses standard clojure.test fixtures. For global setup across all suites, use the :kaocha.hooks/pre-run hook in a plugin:
(defn db-setup-plugin []
{:kaocha.hooks/pre-run
(fn [test-plan]
(db/migrate! test-db)
test-plan)
:kaocha.hooks/post-run
(fn [test-plan]
(db/drop-all! test-db)
test-plan)})For namespace-level setup, use use-fixtures as normal:
(use-fixtures :each
(fn [f]
(db/clear-tables! test-db)
(f)))Parallel Test Execution
Kaocha supports parallel test execution at the namespace level:
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test"]
:kaocha.testable/parallelism 4}]}Note: parallel execution requires your tests to be stateless. Tests sharing mutable state (global atoms, database tables without isolation) will produce flaky results.
CI Integration
A complete tests.edn for CI:
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test/unit"]
:kaocha.filter/skip-meta [:integration :e2e]}
{:id :integration
:test-paths ["test/integration"]
:kaocha.filter/focus-meta [:integration]}]
:reporter kaocha.report.junit/xml
:plugins [:kaocha.plugin/randomize
:kaocha.plugin/profiling]
:kaocha.plugin.profiling/count 5
:color? false
:randomize? true}GitHub Actions workflow:
- name: Run tests
run: clojure -M:test
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/Debugging Failing Tests
When a specific test fails intermittently, focus on it and disable randomization:
clojure -M:test --focus myapp.core-test/my-flaky-test --no-randomizeTo see the full test plan (what Kaocha will run without running it):
clojure -M:test --print-configTo see loaded test namespaces:
clojure -M:test --print-test-planContinuous Testing with HelpMeTest
Kaocha runs unit and integration tests in your development loop. For continuous production monitoring — verifying your application behaves correctly after deployment — HelpMeTest provides plain-English test scenarios that run on a schedule without requiring source code access.
Where Kaocha catches regressions before merge, HelpMeTest catches them after deploy. Both complement each other in a complete testing strategy.
Summary
Kaocha brings professional-grade test runner features to Clojure:
- Test suites separate unit, integration, and e2e concerns
- Metadata filtering lets you skip slow tests during development
- Plugins hook into any execution phase
- Watch mode tightens the feedback loop
- JUnit XML output integrates with every CI platform
The data-driven configuration model — everything in tests.edn — makes Kaocha easy to reason about and version-control alongside your tests.