sbt-scoverage: Scala coverage with thresholds that can fail the build
sbt plugin for scoverage
At a glance
- What is it?
- sbt-scoverage wires the scoverage instrumenter into sbt so Scala test runs produce HTML and XML coverage reports, with per package and per file minimums that can break the build. Its rough edges are version specific: several exclusion features only work on certain Scala versions, and instrumentation is sticky once switched on.
- Who is it for?
- sbt-scoverage suits a Scala team that wants coverage measured as part of an sbt build rather than bolted on afterwards, and the minimum coverage keys are the strongest feature here because they turn a report into a gate.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly Scala, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One command instruments the build, and cleaning is not optional
sbt-scoverage is an sbt plugin that offers support for Scala code coverage using scoverage, and it supports Scala 2.12, 2.13 and 3.
The setup is two lines in project/plugins.sbt:
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "x.x.x")The version requirement is sbt 1.2.8 or above, and there is an alternative for enterprise environments where the plugin resolution above does not work:
libraryDependencies += "org.scoverage" % "sbt-scoverage_2.12_1.0" % "x.x.x"Then the tests are run with coverage enabled:
sbt clean coverage testAnd if the project has integration tests as well:
sbt clean coverage it:testThe clean in that command is the important part, and so is the note that follows it. The plugin instruments standard build output, so you have to remember to clean again with coverage disabled before publishing your build, to avoid including instrumentation. That is a real footgun, because the natural instinct is to stop caring about the build directory once the tests pass, and the instrumented classes are in exactly the place you would publish from.
There is also a build setting, coverageEnabled := true, but the documentation warns that it will result in code being always instrumented, which is unlikely what you want. It is there for setups where coverage is unconditional rather than for a one-off measurement.
Sticky coverage inside a console session, and the command that fixes it
There is a behaviour in this plugin that catches people out, and it is documented in a note that is easy to skim past.
If you run the coverage reports from within an sbt console session, as opposed to one command per sbt launch, then the coverage command is sticky. Once you have run it, it stays on for the rest of that session. To turn it back off when you are done running reports, you use the coverageOff command, or you reset coverageEnabled with set coverageEnabled := false.
The distinction between an interactive session and separate sbt launches is the whole difference. In a scripted CI build where each invocation is a fresh process, the setting does not survive, so a coverage run does not contaminate the next command. In an interactive shell where you are iterating, it does, and the next thing you compile is instrumented whether you intended it or not.
The reports themselves are generated with:
sbt coverageReportand land in the target/scala-<scala-version>/scoverage-report directory. There are two formats. The HTML report is for a human, and the XML report is for a program, useful if you need to consume the results or you are writing a tool.
The location is not fixed, and coverageDataDir overrides where both the data and the report are written. In a build that separates test output, for example:
coverageDataDir := target.value / "custom-test"And it can be set ad hoc through the sbt set directive:
set coverageDataDir := file("/tmp")Overriding this is a common need in CI, where the report has to be picked up by an upload step after the JVM exits.
Exclusion by package, by file, and by comment, each with its own version rule
Excluding code from measurement is where the version matrix becomes visible, and the documentation is careful about it.
Classes can be excluded with a semicolon separated list of regular expressions:
coverageExcludedPackages := "<empty>;Reverse.*;.*AuthService.*;models\\.data\\..*"The regular expressions are matched against the fully qualified class name and must match the entire string to take effect. Any matched class will not be instrumented and will not appear in the coverage report. The full match requirement is worth internalising, because a pattern that matches a substring silently does nothing.
Files and file paths can be excluded as well:
coverageExcludedFiles := ".*\\/two\\/GoodCoverage;.*\\/three\\/.*"The .scala extension has to be omitted from the filename if one is given. Both of these options work only for Scala 2, Scala 3.3.4 and newer, or Scala 3.4.2 and newer. That is a narrow window, and a project on Scala 3.3.0 falls between the two supported ranges.
The third mechanism is comment based, marking a region of code inline:
// $COVERAGE-OFF$Disabling highlighting by default until a workaround for https://issues.scala-lang.org/browse/SI-8596 is found
...
// $COVERAGE-ON$Anything between the two markers is neither instrumented nor included in the report. The example shows a real use, disabling highlighting until a workaround for a scala-lang issue is found, which is the kind of temporary carve-out that accumulates in a long-lived codebase. Comment based exclusion works only for Scala 2.
So on Scala 3 you have two of the three mechanisms, and only on recent patch versions.
Minimum coverage keys are the feature worth stealing
The most useful part of this plugin is not the reporting, it is the ability to fail a build on a coverage threshold.
The keys are set in the build:
coverageFailOnMinimum := true
coverageMinimumStmtTotal := 90
coverageMinimumBranchTotal := 90
coverageMinimumStmtPerPackage := 90
coverageMinimumBranchPerPackage := 85
coverageMinimumStmtPerFile := 85
coverageMinimumBranchPerFile := 80coverageFailOnMinimum is the switch, and the rest are the thresholds. There are three granularities, total, per package and per file, and two measures, statements and branches.
The per file thresholds are the interesting ones for a real codebase. A total threshold of 90 percent can be satisfied by well tested packages next to untested ones, and a per file threshold of 85 statements forces the gap to be closed everywhere rather than averaged out. A branch minimum is harder to satisfy than a statement minimum, because an if with no else counts as a partially covered branch, which is exactly the kind of gap a statement metric misses.
These settings are enforced when the reports are generated, and if you generate an aggregate report with coverageAggregate then they apply to that aggregated report. That is the behaviour you want in a multi project build, where the aggregate is what represents the codebase as a whole.
One caution on the per file setting. In a large codebase with many generated or trivial files, a per file minimum can be frustrating to introduce, and the usual approach is to start with the total and per package keys and tighten later.
Multi project aggregation, CI integrations and the version caveats
By default scoverage generates reports for each project separately, and the aggregate command merges them:
$ sbt coverageAggregateYou do not need to run coverageReport before coverageAggregate, because it aggregates over the sub-projects coverage data directly rather than over the generated report XML. That ordering detail saves a step in CI, and it also means the aggregate can be produced without generating every intermediate HTML file first.
The CI integrations are all separate tools that read the XML report. Codacy integration goes through the codacy-coverage-reporter. Coveralls integration uses the sbt-coveralls plugin. Codecov is done by sending the reports there from CI, with an example in the codecov-example-scala repository. For browsing reports visually rather than uploading them, the SonarQube plugin allows you to review overall project statement coverage and dig into sub-modules, directories and source files to see uncovered statements, and the documentation points at it as part of making statement coverage part of a continuous integration process and a quality standard.
The version caveats deserve one more mention because they are scattered through the document. ScalaJS and Scala Native support is limited to Scala 2. Scala 3 support starts with 3.2.x, and comment based exclusion is Scala 2 only, and package and file exclusion needs Scala 3.3.4 or 3.4.2 or newer.
There is also a troubleshooting note for tests that pass normally but fail once coverage is enabled. Scoverage does a lot of file writing behind the scenes to track which statements have been executed, and the usual culprits are timing issues on futures and other async operations, where the fix is to raise the timeouts by an order of magnitude, or tests running in a sandbox mode such as with java.security.PrivilegedAction<T>, where the fix is to run the tests outside the sandbox.
The project itself is Apache-2.0 licensed, with a LICENSE file at the root, and it uses .scala-steward.conf for dependency updates and .scalafmt.conf for formatting, alongside a .git-blame-ignore-ignore-revs file for the formatting reformat. The release history shows v2.4.4 on 2026-01-13, v2.4.3 on 2025-12-10 and v2.4.2 on 2025-11-17, and the last push was on 2026-10-02.
Editorial conclusion
sbt-scoverage suits a Scala team that wants coverage measured as part of an sbt build rather than bolted on afterwards, and the minimum coverage keys are the strongest feature here because they turn a report into a gate. It does not suit you if you are on an old sbt or a Scala version outside the supported matrix, and it does not suit a build that publishes instrumented artefacts, because the documentation is emphatic that you must clean with coverage disabled before publishing. Before you enforce thresholds, check the version matrix against your project, since comment based exclusion works only on Scala 2 and the package and file exclusion options need Scala 3.3.4 or newer, and turn the coverage command off in the same session where you enabled it.
Frequently asked questions
How do I add sbt-scoverage to an sbt build?
Add addSbtPlugin("org.scoverage" % "sbt-scoverage" % "x.x.x") to project/plugins.sbt, and make sure you are on sbt 1.2.8 or above. In an enterprise environment where that does not work, the fallback is a libraryDependencies entry for org.scoverage % sbt-scoverage_2.12_1.0.
How do I run Scala tests with coverage enabled?
Run sbt clean coverage test, or sbt clean coverage it:test if you also have integration tests. The clean matters, because the plugin instruments the build output and you need to clean again with coverage disabled before publishing.
Can sbt-scoverage fail the build on low coverage?
Yes. Set coverageFailOnMinimum := true along with the thresholds such as coverageMinimumStmtTotal, coverageMinimumBranchTotal, coverageMinimumStmtPerPackage and coverageMinimumStmtPerFile. The settings are enforced when the reports are generated, and they apply to an aggregate report too.
Where does the sbt-scoverage report end up?
In the target/scala-<scala-version>/scoverage-report directory, as both HTML and XML. The default location can be changed with coverageDataDir, either in the build or through the sbt set directive.
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/scoverage-sbt-scoverage)Was this useful?