volley is a test runner for clojure.test on babashka and the JVM. It runs test
namespaces in parallel worker processes, shows each namespace as it finishes,
and has a watch mode that reruns only the tests affected by a change. It reads
Kaocha's tests.edn and command-line flags, so a project that runs Kaocha today
can switch by changing one command. It was built for suites that are slow enough
that waiting for a single process to finish is noticeable, and for babashka
projects that already use Kaocha and want to keep their existing
configuration.
volley is alpha software (0.1.0-alpha.2). See CHANGELOG.md.
That is bb test on volley's own suite, in a terminal.
This is a run of a small fixture project with failures, with output piped (not a terminal). On a terminal volley redraws one line per running namespace instead and prints the same failure reports and summary.
✓ shop.passing-test (2 tests) 2ms
✗ shop.cart-test (2/3 failed) 4ms
FAIL shop.cart-test/add-item-shape test/shop/cart_test.clj:10
testing: adding to an empty cart
expected: {:items [{:sku 1}], :count 1}
actual: {:items [{:sku 1}]}
diff (- expected, + actual):
{
- :count 1
:items [{:sku 1}]
}
FAIL shop.cart-test/prints-then-fails test/shop/cart_test.clj:15
expected: 5
actual: 4
output:
| computing the discount
✗ shop.errors-test (2/3 failed) 8ms
ERROR shop.errors-test/throws-inside-is test/shop/errors_test.clj:9
expected: (= 1 (Long/parseLong "one"))
actual: java.lang.NumberFormatException: For input string: "one"
java.lang.NumberFormatException: For input string: "one"
at shop.errors-test (test/shop/errors_test.clj)
ERROR shop.errors-test/throws-outside-is test/shop/errors_test.clj:4
Uncaught exception, not in assertion.
expected: nil
actual: clojure.lang.ExceptionInfo: inventory unavailable
clojure.lang.ExceptionInfo: inventory unavailable
data: {:sku 42}
at shop.errors-test (test/shop/errors_test.clj:6)
Randomized with --seed 1604350068
8 tests | 4 passed | 2 failed | 2 errors | 0 skipped (10 assertions, 45ms)
Slowest tests:
7ms shop.errors-test/throws-inside-is
3ms shop.cart-test/add-item-shape
1ms shop.errors-test/throws-outside-is
0ms shop.passing-test/two
0ms shop.cart-test/prints-then-fails
Rerun failures:
bb -m volley.main --focus shop.cart-test/add-item-shape --focus shop.cart-test/prints-then-fails --focus shop.errors-test/throws-inside-is --focus shop.errors-test/throws-outside-is
The run exited with code 4, the number of failures, as Kaocha does. A
failed run prints its seed, and --seed 1604350068 runs each namespace's
tests in that order again.
volley is on Clojars as
com.kirahowe/volley. Add it
to bb.edn, and a task to run it:
{:paths ["src" "test"]
:deps {com.kirahowe/volley {:mvn/version "0.1.0-alpha.2"}}
:tasks {test {:doc "Run the tests"
:requires ([volley.main])
:task (apply volley.main/-main *command-line-args*)}}}
Run bb test, or bb -m volley.main directly.
If your project has no tests.edn, volley uses Kaocha's defaults: one
suite with test/ as its test path, running the namespaces whose names
end in -test. You need a tests.edn only to change them, and it can be
a plain map: volley reads it the same with or without Kaocha's
#kaocha/v1 tag.
If the test task above replaces one you wrote yourself, check which
namespaces it ran. volley runs only namespaces whose names end in -test
unless the suite sets :ns-patterns, so a namespace such as
my.app.checks stops running when you switch.
For a bin/volley binstub like Kaocha's bin/kaocha, copy
bin/volley into your project and make it executable with
chmod +x bin/volley. It runs bb -m volley.main "$@" with your
bb.edn.
Workers start with the parent's classpath, so your bb.edn :paths and
:deps apply to the tests. Like Kaocha, volley adds each suite's
:test-paths to the classpath. It does not add :source-paths; put those
on the classpath through bb.edn.
Change the :test alias in deps.edn that runs Kaocha: swap
lambdaisland/kaocha for volley in :extra-deps and kaocha.runner for
volley.main in :main-opts. Keep the alias's other :extra-paths,
:extra-deps and :jvm-opts; the tests need them as before.
{:aliases
{:test {:extra-paths ["test" "test-resources"]
:extra-deps {com.kirahowe/volley {:mvn/version "0.1.0-alpha.2"}
org.clojure/test.check {:mvn/version "1.1.1"}}
:jvm-opts ["-Xmx2g"]
:main-opts ["-m" "volley.main"]}}}
Run clojure -M:test. A bin/kaocha binstub that runs
clojure -M:test "$@" keeps working unchanged. On the JVM volley depends
on borkdude/edamame, babashka/fs and babashka/process, which come in
with it. babashka has all three built in.
To try an unreleased commit, depend on the repository instead, on either runtime:
io.github.kirahowe/volley {:git/sha "<full commit sha>"}
Replace
bb -m kaocha.runner/-main
with
bb -m volley.main
and remove lambdaisland/kaocha from your dependencies if nothing else
uses it. A bin/kaocha binstub becomes bin/volley (see above).
Change the alias from kaocha.runner to volley.main and replace
lambdaisland/kaocha with volley, as in the install example. volley does
not need Kaocha, so drop the dependency unless something else uses it.
A bin/kaocha binstub that runs clojure -M:test "$@" keeps working
unchanged. The parent runs on the JVM, so the workers are JVM processes
too:
:test-paths.-D properties, --add-opens, GC flags, agents), except ones that
would collide with the parent: debugger agents, JMX remote ports, socket
REPL properties, JFR and -Xlog, -XX:ErrorFile and -XX:HeapDumpPath.
Workers also get -XX:TieredStopAtLevel=1. With -Xmx2g and four
workers, the tests can use up to four times that heap.:test-paths added to its classpath, and starts worker JVMs
only if a run outlasts that with work still waiting.
The first run, with no .volley/ yet, always uses worker JVMs. A
70-test suite took 5.65s that way against Kaocha's 5.71s (7.6s with
worker JVMs). Large suites gain the most: a 244-test suite took 40s
with volley against 104s with Kaocha. See
doc/benchmarks.md.To keep the parent on babashka and run tests on the JVM, use
bb -m volley.main --runtime jvm. volley runs clojure -Spath for the
aliases in --aliases (default: :test if your deps.edn defines it) and
starts workers with java -cp. The classpath is cached in
.volley/classpath-<hash>.txt, keyed on deps.edn, the aliases, the
deps.edn of every :local/root dependency and your user-level deps.edn
($CLJ_CONFIG, $XDG_CONFIG_HOME/clojure or ~/.clojure). --runtime bb
from a JVM parent starts babashka workers.
tests.edn, including #kaocha/v1, #profile, #env, suite ids,
:test-paths, :source-paths, :ns-patterns, :focus, :skip,
:focus-meta, :skip-meta, :kaocha/bindings and :kaocha/fail-fast?.--focus, --skip, --focus-meta, --skip-meta,
--fail-fast, --config-file, --[no-]color, --[no-]capture-output,
--reporter, --watch, --plugin, --profile, --[no-]randomize,
--seed.--print-config, --print-test-plan and --print-result print
Kaocha's shapes (:kaocha/tests, :kaocha.test-plan/tests,
:kaocha.result/tests), but not the same maps: volley's own settings sit
under :volley/* keys, the test plan lists only the tests the parser
can see and holds no namespace or var objects, the result has counts
but no events, and --print-result prints one line. The
divergences list
the details.^:kaocha/pending and :kaocha.testable/skip, plus the
metadata your --focus-meta and --skip-meta filters use.:kaocha.plugin/hooks). Most hooks run in every worker;
post-summary runs once in the parent. See "Hooks" in
doc/design.md for what runs where.kaocha.plugin/junit-xml, built in: add it to :plugins, then use
--junit-xml-file FILE or :kaocha.plugin.junit-xml/target-file. You do
not need the kaocha-junit-xml library.Kaocha options volley does not support print a warning instead of being
ignored silently. Kaocha's other plugins (notifier, gc-profiling,
cloverage and the like) are accepted with their options, and one warning
line names the ones volley does not run. A plugin that cannot be loaded at
all stops the run with exit 248, and one whose namespace defines no such
plugin with exit 249, as in Kaocha. Kaocha's reporters map to
volley's output without a warning (documentation to its documentation
layout), and a reporter of your own, such as --reporter my.app/report,
runs in the workers and gets every clojure.test event.
The list of deliberate behaviour differences is in the
divergences section of doc/design.md.
In short: a namespace that fails to load is one error rather than failing
the whole suite, a test that calls System/exit crashes one worker rather
than the run (in volley's own JVM it ends the run, which volley reports
with exit code 1), fail-fast finishes the failing test before stopping, a
namespace is loaded in the worker that runs it unless you pass
--preload, and a project without tests.edn gets no warning about it.
^:synchronized or ^:volley/serial
(it then runs alone, after the others), or run everything in one
worker with --workers 1.:volley/worker-setup run in every worker, so setup that
starts a server on a fixed port starts one per worker.--no-randomize, or
:kaocha.plugin.randomize/randomize? false in tests.edn, runs them
in name order. Namespaces still start slowest first, as last time's
timings say, so --seed N reproduces the order within each namespace
but not which namespaces run side by side.assert-expr
method, a spec) needs --preload.--watch (or :watch? true in tests.edn) runs everything once and then
keeps running. volley polls the modification times of every file under the
suites' :test-paths and :source-paths, and of tests.edn, deps.edn and
bb.edn, every 200ms.
When files change it works out which test namespaces depend on them (from
the ns forms) and reruns just those in fresh worker processes, so no
state survives between runs. New and deleted test files are picked up. A
change to tests.edn reloads the config and reruns everything. A change to
the project's deps.edn or bb.edn does the same and also computes the
workers' classpath again, so new dependencies and paths reach the next run.
The file that counts is the one the workers' classpath comes from: bb.edn
for babashka workers under a babashka parent, deps.edn for a JVM parent or
--runtime jvm. Removed dependencies and paths stay on the classpath until
you restart. volley watches only the project's own two files, not the
deps.edn of a :local/root dependency or your user-level one. If a run is
still going when files change, volley cancels it and starts a new one.
Each run ends with the counts line of the summary, without the slowest
tests or the rerun command (f reruns the failures), and a status line
such as PASS Waiting for file changes... press h to show help, q to quit.
When something fails, the line says FAIL 2 failures; failures from
earlier runs stay counted until their namespace passes again. On a
terminal:
| Key | Action |
|---|---|
a | rerun all |
f | rerun only the failures |
p | filter namespaces by regex (Enter applies, Esc cancels, empty clears) |
t | filter test names by regex |
Enter | rerun the last selection |
c | clear filters and rerun |
h | help |
q | quit |
Filters combine with the config's :focus and :skip; they never widen
them. When stdin is not a terminal, keys are off and watch mode reacts to
file changes only; stop it with Ctrl-C or a signal.
Tests run in several worker processes, so setup that changes the process
(pointing code at a temporary home directory, say) has to run in each one.
List the functions in tests.edn:
#kaocha/v1
{:volley/worker-setup [my.test-support/isolate!]
:tests [{:id :unit}]}
Each worker calls them with no arguments before it loads its first test
namespace. A suite can have its own :volley/worker-setup too, called
before the worker's first namespace of that suite. If one throws, that
worker runs no tests and each namespace it was given is reported as an
error.
--workers N: number of worker processes. Default: the CPU count, at
most 8 on babashka and at most 4 on the JVM. Setting it, here or as
:workers in tests.edn, also keeps a short JVM run out of volley's
own JVM.--[no-]isolate: start a fresh worker for every namespace. It is slow and
prints a warning.--runtime bb|jvm: the worker runtime. Default: the runtime volley runs on.--aliases :a:b: deps.edn aliases for the classpath of JVM workers
started from babashka.--[no-]preload (or :preload? true in tests.edn): each worker loads
every test namespace before running any, as Kaocha does. Use it when a
test namespace relies on something another one defines without requiring
it, such as a custom assert-expr. Since every namespace is loaded
first, the live view's test total is exact from the first test.A namespace marked ^:synchronized or ^:volley/serial (on the ns form or
on any of its test vars) runs after the parallel batch, alone.
bb -m volley.main --help lists every flag.
On a terminal volley redraws a live view: one line per running namespace, a
line for each namespace as it finishes, and failures as they happen. Its
test total includes the tests that haven't started yet. Until every
namespace has loaded, part of that number comes from reading the source,
which misses tests defined by macros other than deftest and defspec,
so the total shows as ~n until it's exact. When stdout is not a terminal
it prints append-only lines instead, one per finished namespace followed
by its failures. A test that has run for 30 seconds gets a still running
line in that mode. Both end with a summary, the slowest tests and a
--focus command to rerun the failures. That command starts the way you
ran volley: bb test inside a babashka task, bin/volley through the
binstub, clojure -M:test on the JVM. Set VOLLEY_COMMAND to choose it
yourself. It keeps --config-file when you used one.
--reporter documentation prints one line per test var, grouped by
namespace, in the order the tests ran.
A failing test's report includes what the test printed, cut down to the
25 lines before each failure and the 5 after it. The rest is counted as
earlier or later lines. When the output goes on past a failure, a
▲ FAIL file:line line marks where in it the failure happened.
Output that a test or a namespace prints outside capture (while loading, say) appears as it happens, in full. When several workers load the same namespace and print the same line while loading it, the line appears once.
The summary counts the tests that ran and the pending ones, which it
calls skipped. Tests that --focus, --skip or the metadata filters
leave out are not counted, as in Kaocha: --focus my.app-test/one
reports 1 test. When the filters leave nothing to run, volley prints
Kaocha's "All N tests were skipped" warning.
volley saves namespace durations in .volley/timings.edn, and the next run
starts the slowest namespaces first. On the JVM it also keeps recent run
times in .volley/history.edn, to decide whether a run goes in its own
JVM, and the parsed ns forms of your test files in
.volley/ns-infos.edn. When it creates .volley/ it puts a .gitignore
containing * inside, so git and jj leave the directory alone.
--color forces it on, and
NO_COLOR always turns it off.--junit-xml-file report.xml (with kaocha.plugin/junit-xml in
:plugins) writes an Ant JUnit XML report.CI=true for the #profile reader tag to pick :ci, as Kaocha
does.clojure.test only. No ClojureScript and no coverage.--randomize shuffles the vars of each namespace, not the order of
the namespaces, which start slowest first..volley/ yet (in CI,
unless you cache it), uses worker JVMs and is slower than Kaocha. See
doc/benchmarks.md.The full list, with reasons, is in doc/design.md.
volley ran a 507-test babashka suite in 27.5s with 8 workers, against 153.8s for that project's own runner. On a 26-test babashka suite it took 0.43 to 0.50s against 0.50 to 0.65s. On the JVM it ran a 244-test suite in 40s against Kaocha's 104s, and a 70-test suite in 5.65s against 5.71s. The runs were on a 16-core Mac with other work on it. Numbers, conditions and caveats are in doc/benchmarks.md.
bb test: volley's own suite, run by volley (tests.edn).bb test:jvm: the same suite with volley itself running on the JVM. It
also fails when volley's source causes a reflection warning.bb test:bootstrap: the same suite with plain clojure.test, for when
volley is too broken to run itself.bb test:diff: compares volley with Kaocha 1.91.1392 itself (downloads
Kaocha).bb lint: clj-kondo over src and test. It must report no errors or
warnings.bb demo: replays a scripted run through the live view.To release, set the version in src/volley/version.edn, give it a dated
heading in CHANGELOG.md, and tag the commit v<version>. Then
clojure -T:build deploy builds the jar and pushes it to Clojars, with
CLOJARS_USERNAME and CLOJARS_PASSWORD (a deploy token) set.
clojure -T:build install puts the jar in ~/.m2 to try it locally first.
Read AGENTS.md and doc/design.md before changing anything. The design doc defines the architecture and the data contracts between namespaces.
MIT. See LICENSE.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |