Kaocha Test Runner: Advanced Configuration and Plugins

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 integration

Test 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 :db

Or 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 --watch

With 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-test

Plugins

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 42

Profiling — 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-randomize

To see the full test plan (what Kaocha will run without running it):

clojure -M:test --print-config

To see loaded test namespaces:

clojure -M:test --print-test-plan

Continuous 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.

Read more