cloverage measures Clojure coverage by instrumenting the source, not by watching it
Clojure test coverage tool
At a glance
- What is it?
- cloverage is a Leiningen plugin that reports line and form coverage for Clojure code, and its approach is to instrument the namespaces before the tests run rather than to trace them afterwards. That choice is what produces the form coverage metric, which is the one that catches untested branches in a language where line coverage is nearly meaningless.
- Who is it for?
- Adopt cloverage if you build Clojure with Leiningen and want form coverage rather than line coverage, since the two differ sharply in a language where one expression can span many lines. Skip it if your project builds with the Clojure CLI tools only, because there is no Maven plugin and the CLI invocation is a workaround rather than a first-class path.
- Can I use it commercially?
- Yes, with conditions. EPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Clojure, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A plugin, installed two ways
cloverage describes itself as a simple Clojure coverage tool, and that word simple is doing more work than it might seem.
The tool ships as two things on Clojars. The library itself is the cloverage artifact, and the Leiningen plugin is lein-cloverage, which is what you actually invoke. The Clojars badge in the documentation points at the plugin artifact.
There are two ways to install it, and the choice is about scope rather than about version.
For a single project, you add the plugin to your project.clj metadata:
:plugins [[lein-cloverage "1.2.2"]]For every Leiningen-managed project on the machine, you add it to your user-wide profiles file at ~/.lein/profiles.clj:
{:user {:plugins [[lein-cloverage "1.2.2"]]}}That second option is the Leiningen idiom. The user profile applies to all projects, so a tool you want everywhere is installed once and does not have to be added to every project.clj. It also means the plugin version is pinned in one place, which is what you want for a tool whose output is used to gate a build.
The version in both examples is 1.2.2. There is also a way to override the version at invocation time by setting the CLOVERAGE_VERSION environment variable to a desired value, and the documentation gives a snapshot build as the example. By default the plugin uses the latest release version.
That last point is worth pausing on for a continuous integration setup. A coverage plugin that silently changes version between the branch build and the release build is a source of confusing diffs in the report, and the environment variable is the escape hatch for pinning it.
The project is Clojure, licensed under EPL-2.0, with the default branch master and the last push on 2026-10-14. There are no GitHub releases, which is normal for a Clojure library, since the release artifact is published to Clojars and the version is tracked in the build configuration rather than in release tags.
Test runner support, and a compatibility detail worth knowing
Cloverage uses clojure.test by default, and the other runners are opt-in through the runner flag.
If you prefer midje, pass the --runner :midje flag. For eftest, pass --runner :eftest. And you can configure a runner by passing the :runner-opts option with a map in the project settings. The documentation also notes that other test libraries may ship with their own support for cloverage external to this library, and to see their documentation for details.
That last sentence is worth reading, because it points at a general pattern. A coverage tool has to know which namespaces contain tests and how to run them, and that knowledge lives in the test framework. Rather than trying to be compatible with everything, cloverage supports the three common ones and lets the others bring their own integration.
The compatibility detail is the sentence about midje. In older versions of cloverage, you had to wrap your midje tests in clojure.test's deftest. This is no longer necessary.
That is a small change with a big effect on how the tool feels to use. Wrapping every midje test in clojure.test's deftest was a tax on every test you wrote, and it existed because the coverage tool needed to see tests through clojure.test's reporting machinery. Once that coupling was removed, the tool stopped interfering with how you write your tests, which is the difference between a tool you tolerate and a tool you do not notice.
The runner choice is also the reason the runner-opts option exists. :runner-opts is a map you configure in the project settings rather than on the command line, and the options table notes it is not allowed as a command line argument. That is a deliberate restriction, because the options are structured data and a command line argument would have to encode a map as a string.
The options table and the three thresholds
The options table is long, and the documentation describes it as a set of project options with matching command line switches and defaults.
The three threshold flags are the part that needs explaining, and the documentation gives the reason directly. Sometimes, line coverage is high while form coverage isn't as high, so setting a high line coverage requirement can help maintaining an existing standard.
The three are:
:fail-threshold --fail-threshold 0 Sets the percentage threshold for both line and form coverage at which cloverage will abort the build. Default: 0%
:line-fail-threshold --line-fail-threshold 0 Sets the percentage threshold for line coverage at which cloverage will abort the build. Ignored if --fail-threshold is non-zero. Default: 0%
:form-fail-threshold --form-fail-threshold 0 Sets the percentage threshold for form coverage at which cloverage will abort the build. Ignored if --fail-threshold is non-zero. Default: 0%
:low-watermark --low-watermark 50 Sets the low watermark percentage (valid values 0..100). Default: 50%
:high-watermark --high-watermark 80 Sets the high watermark percentage (valid values 0..100). Default: 80%The fail-threshold sets the percentage for both line and form coverage at which cloverage will abort the build. The line-fail-threshold is for line coverage alone and is ignored if fail-threshold is non-zero. The form-fail-threshold is for form coverage alone, and is likewise ignored if fail-threshold is non-zero.
The precedence rule is worth understanding rather than just reading. The general threshold wins if you set it, and the two specific ones only apply when the general one is zero. So a project that wants one combined number sets one property, and a project that wants to hold one metric to a higher bar while letting the other float sets the general one to zero and the specific one to the value it cares about.
All three default to zero, which means coverage never fails the build unless you ask it to. That is the right default for a tool people are introducing to an existing codebase, where the first run would otherwise fail on a threshold nobody has looked at yet.
Then there are the two watermark properties, and they are a different idea from the thresholds. The low watermark defaults to 50 and the high watermark to 80, with valid values 0 to 100. A watermark is not a failure condition, it is a band. The reporting can distinguish coverage that is below the low watermark, which means the code is essentially untested, from coverage in the band between the two, which means partial, and coverage above the high watermark.
That distinction is what lets a report say something more informative than a single percentage. A project sitting at 45 percent and a project sitting at 78 percent are both below the usual expectations and they are in completely different states, and the watermarks encode that.
There is one more piece of policy in the table worth noting. The project value is used for non-list options, and command line arguments are merged in. List options are merged by concatenation.
That behaviour is a small thing that saves real friction. It means you can set your defaults in project.clj and then override individual flags on the command line for a one-off run, and if you set a list option in the project, adding the flag on the command line adds to it rather than replacing it.
Report formats, and what each one is for
The reporting options cover a wide range of outputs, and each has an obvious audience.
The HTML report is on by default and the text report is off, with a summary printed by default. The raw option outputs the raw coverage data for debugging.
Then there are the machine-readable formats. The EMMA XML report, the lcov and gcov report, a JSON report for Codecov.io, a Cobertura XML report for line coverage, and a JSON report sent to Coveralls if you are on a CI server. And there is the junit option, which outputs test results as a junit xml file, supported in the clojure.test runner.
The lcov and gcov report is the one that matters most if you use a code coverage service, because that is the format those services consume. The Codecov and Coveralls options are conveniences on top of it, and the Cobertura report is the XML format that other tools in the Java ecosystem expect, which matters if your coverage tooling is not Clojure-aware.
The junit option is a different thing entirely and it is worth not confusing it with coverage. It outputs the test results, not the coverage data, in the format continuous integration systems use to display which tests passed and failed. The note that it is supported in the clojure.test runner is a limitation of the other runners rather than of the format.
All of these default to off except the HTML report and the summary, so a default run produces one HTML report and a printed summary. Turning on more is a deliberate choice per project, and the fact that each is a separate switch rather than a single report format means you can have HTML for humans and lcov for the service without either being mandatory.
There is also a custom report option, which loads and runs a custom report writer that should be a namespaced symbol. The function is passed the project options, an args map, the output directory and the forms.
That signature tells you what a custom reporter gets: the configuration, whatever extra arguments you passed, where to write, and the coverage data itself. So a custom reporter is a function rather than a template, and you can emit anything you want from it.
The remaining options are about what gets instrumented and which tests run. There are regexes for the instrumented namespaces and for the namespaces not to be instrumented, a regex for the test namespaces, paths to the source and test directories, extra test namespaces to add, and a test selector. The exclude-call option names a function or macro whose call sites are not to be instrumented, which is the escape hatch for a macro that expands into code the instrumenter cannot handle.
Running it outside Leiningen
The tool is a Leiningen plugin first, and the documentation is candid about the other build tools.
For Maven there is no plugin right now. The workaround is to import this library in the project being tested and then run:
`mvn exec:java -Dexec.classpathScope=test -Dexec.mainClass='clojure.main' -Dexec.args='--main cloverage.coverage *args-to-coverage*'`Where the arguments to coverage will usually be something like a namespace regex for the source and a namespace regex for the tests.
The important part of that command is the classpathScope set to test. Without it, the exec plugin runs with the compile classpath only, and the coverage tool cannot see the test namespaces it is supposed to run. That is the failure mode you get if you copy the command and change one thing.
For the Clojure CLI tools there is a first-class invocation:
`clj -Sdeps '{:deps {cloverage {:mvn/version "RELEASE"}}}' -m cloverage.coverage *args-to-coverage*`Where the arguments to coverage will usually be something like the source path and the test path.
That command is worth reading carefully, because the dependency is injected on the command line rather than being in the project file. The -Sdeps flag adds a dependency map for that one invocation only, so the tool is pulled in without being written into deps.edn. The main class is the coverage namespace run directly, which is the same entry point the Maven workaround uses.
So the two approaches differ in where the dependency lives. The Maven route needs the library imported into the project, which means your build file changes. The CLI route needs nothing changed, because the dependency is supplied for that command only. For a project that does not want a coverage dependency in its build file, the CLI route is cleaner, and it is the one that works today without a plugin.
Neither of the other two routes has the convenience of lein cloverage, where the plugin is configured in project.clj and the options are merged from there. That is the practical reason the Leiningen path is the primary one, even in a world where Leiningen is no longer the default for new Clojure projects.
The repository and the troubleshooting entry
The repository layout is small and tells you how the project is split.
There is a cloverage/ directory for the library, a lein-cloverage/ directory for the plugin, and scripts/ for the build tasks. There is a CHANGELOG.md, a CONTRIBUTING.md, a CODE_OF_CONDUCT.md, a TODO.md and an .editorconfig.
The epl-v10.html file at the root is the Eclipse Public License version 1.0 text, which is what the EPL-2.0 classification refers to. Putting the licence text in the repository as a rendered HTML document is a small detail, but it means the terms are readable without leaving the source tree.
The .dir-locals.el file is an Emacs project file, which tells you something about the maintainer's editor, and the .editorconfig sets the formatting conventions for everyone else.
There is a CI workflow badge for continuous integration, and the project publishes to Clojars rather than to GitHub releases, which is why the repository has no release tags.
The troubleshooting section in the documentation has one entry, and it is a good one to include because it is the kind of problem everybody hits. The symptom is an IllegalArgumentException with the message No matching field found for a field name on a class.
That error comes from the instrumentation rather than from your code. The coverage tool has to rewrite the forms in your source, and when it rewrites a form that accesses a record field or a class property by name, it has to look that member up in the class definition. When the lookup fails, the error is about a missing field rather than about instrumentation, which makes it look like a problem with your own record definition when it is not.
That is the whole reason it is worth documenting. The failure is confusing precisely because it points at the wrong layer, and a project that has hit it once will recognise it immediately.
The project is worth understanding in the context of Clojure coverage generally. Form coverage is the metric that matters in a language where a single expression can span many lines and where line coverage can be high while a whole branch is never exercised. A tool that reports both, and that lets you hold the form threshold higher than the line threshold, is measuring the right thing.
Editorial conclusion
Adopt cloverage if you build Clojure with Leiningen and want form coverage rather than line coverage, since the two differ sharply in a language where one expression can span many lines. Skip it if your project builds with the Clojure CLI tools only, because there is no Maven plugin and the CLI invocation is a workaround rather than a first-class path. Verify first which test runner you use, since the runner flag changes how tests are discovered, and that you set the threshold flags deliberately, because a coverage requirement that aborts the build is a policy decision rather than a default.
Frequently asked questions
How do I install cloverage and run it?
You can add the lein-cloverage plugin to a single project in project.clj, or install it user-wide in ~/.lein/profiles.clj so every Leiningen-managed project has it. Then you run `lein cloverage` in your project, and cloverage/coverage.clj documents the further options. To pin a version for one run, set the CLOVERAGE_VERSION environment variable, for example CLOVERAGE_VERSION=1.0.4-SNAPSHOT lein cloverage, because the plugin otherwise uses the latest release.
Which test runners does cloverage support?
It uses clojure.test by default. Midje is supported by passing the --runner :midje flag and eftest by passing --runner :eftest, and a runner can be configured with the :runner-opts option as a map in the project settings, which is not allowed as a command line argument. Older versions of cloverage required midje tests to be wrapped in clojure.test's deftest, and that is no longer necessary.
What is the difference between the coverage threshold options in cloverage?
The fail-threshold sets the percentage for both line and form coverage at which cloverage aborts the build, while line-fail-threshold and form-fail-threshold set the thresholds for one metric each and are ignored if fail-threshold is non-zero. All three default to zero, so coverage never fails a build unless you configure it. There are also low and high watermark properties, 50 and 80 percent by default, which mark the bands rather than failing anything, and the documentation notes that line coverage can be high while form coverage is not.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cloverage-cloverage)Was this useful?