SimpleCov is now version 1!
Here at FastRuby.io we work with test suites and test coverage reports every single day, given our specialty. In the Ruby world that means we interact with SimpleCov every day. Not only that, we explicitely rely on SimpleCov reports in two of the open source projects we maintain, RubyCritic and Skunk .
Which is why, after more than a decade at 0.x, we were super excited to see SimpleCov ship its first stable release on July 12, 2026. In this article we’d like to share the details on the breaking changes, the deprecations and how to address them, if you’re impacted by them.
Everything below refers only to the 1.0.0 entry in the changelog .
Also, it’s worth noting that the minimum required Ruby version for this release is 3.2.
Breaking changes
How filters match paths
Two related changes can quietly alter which files end up in your report.
The first is that SourceFile#project_filename now returns a relative path, lib/foo.rb instead of /lib/foo.rb, which affects any anchored RegexFilter that relied on the leading slash. Because this breaks without a warning, you need to grep your codebase for patterns relying on the leading slash:
grep -rn "add_filter\|remove_filter\|add_group" .simplecov spec/spec_helper.rb test/test_helper.rb
Any pattern that anchors on a slash needs rewriting:
# Before
add_filter %r{^/lib/}
# After
add_filter %r{\Alib/}
The second change is that StringFilter now matches at path-segment boundaries, so "lib" matches /lib/ but no longer matches /library/. If you were deliberately relying on substring behavior, like saying add_filter "_spec" to catch user_spec.rb, switch that filter to a Regexp:
# Before: matched anything containing the substring
add_filter "generated"
# After: explicit substring matching
add_filter(/generated/)
This change has one very important observation to note, however: An argument containing a dot is still treated as a filename pattern and still matches as a substring, so add_filter "test.rb" continues to catch faked_test.rb. A trailing slash likewise still means directory-only matching. Only dot-free arguments like "lib" or "_spec" became segment-anchored.
The status line moved to stderr
The “Coverage report generated for X to Y” line, and the per-criterion totals under it, now go to stderr instead of stdout. If a CI script of yours parsed that line from stdout, it will now see nothing. You can suppress the message entirely on the formatter:
SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter.new(silent: true)
Or you can restore the old behavior at the call site:
bundle exec rspec 2>&1
HTMLFormatter now outputs coverage.json
SimpleCov used to automatically activate JSONFormatter when the CC_TEST_REPORTER_ID environment variable was set, and that special case is gone. It is no longer needed, because the default HTMLFormatter now writes coverage.json alongside the HTML report, serializing the same payload JSONFormatter would. If you relied on the old auto-activation and you weren’t using the HTML formatter, you’ll need to add either the JSONFormatter or the HTMLFormatter explicitly. Note that only one of them is needed
and using HTMLFormatter gives you the JSONFormatter output plus the HTML report.
coverage.json schema change
coverage.json changed shape, from { "covered_percent": 80.0 } to the full { "covered": 8, "missed": 2, "total": 10, "percent": 80.0, "strength": 0.0 }, with covered_percent renamed to percent. On a related note, both simplecov_json_formatter and simplecov-html have been merged into the main gem. The old requires still work through a shim, so you can drop the separate gems from your Gemfile whenever it is convenient.
SimpleCov.start now loads the test_frameworks profile by default
Calling SimpleCov.start with no arguments now loads the test_frameworks profile by default, which filters out paths under test/, spec/, features/, and
autotest/. Rails applications usually call SimpleCov.start "rails" instead, and the rails profile has loaded test_frameworks for as long as the profile has
existed, so those projects need no change.
If you do call start with no arguments, your report will drop its overall coverage percentage, and how much depends on the ratio of test code to application
code. Should you also set the minimum_coverage attribute and the new number fall below that threshold, you’ll need to either establish a new acceptable
threshold or remove the filters:
# Option A: accept the more honest number and re-baseline the threshold
SimpleCov.minimum_coverage 80
# Option B: keep the old behavior by dropping the new filter
SimpleCov.start do
remove_filter %r{\A(test|features|spec|autotest)/}
end
Option B is genuinely useful if you want to surface dead test helpers that nothing calls anymore. For everything else, re-baselining is the better move. If the number you land on is lower than you are comfortable with, we put together 10 strategies for upgrading apps with low test coverage that should help.
Parallel waits and two removals
Under parallel_tests, SimpleCov now waits in the first process rather than the last, using ParallelTests.first_process?, which matches what the parallel_tests README recommends. For most people this fixes a deadlock, and the old PARALLEL_TEST_GROUPS=1 workaround is no longer needed. The rare project that wired up its own wait_for_other_processes_to_finish in an after(:suite) hook keyed on last_process? now hits the symmetric deadlock and has to switch to first_process?.
Two removals round out this section. SimpleCov.coverage_criterion is gone and primary_coverage (or coverage :branch, primary: true) replaces it and the docile dependency is gone too, with SimpleCov.configure blocks now evaluated via instance_exec and instance variable proxying, which needs no action on your side unless you were doing something exotic inside a configure block.
Deprecations
Below we give a small description of the deprecations that were created in this version. While these don’t break now, we recommend effecting these changes so that future upgrades go smoothly.
The configuration API was redesigned
The config API has been reorganized around a smaller, more consistent set of verbs:
| Legacy | Replacement | Notes |
|---|---|---|
add_filter |
skip |
Identical matcher grammar, no behavior change |
add_group |
group |
Identical matcher grammar, no behavior change |
track_files |
cover |
Behavior differs, see below |
use_merging |
merging |
No behavior change |
enable_for_subprocesses |
merge_subprocesses |
No behavior change |
enable_coverage_for_eval |
enable_coverage :eval |
Folds into the same call as :line / :branch / :method |
print_error_status (reader) |
print_errors |
The print_error_status= writer is unaffected for now |
Most of these are a straight rename:
# Before
SimpleCov.start do
add_filter "/vendor/"
add_group "Services", "app/services"
use_merging true
end
# After
SimpleCov.start do
skip "/vendor/"
group "Services", "app/services"
merging true
end
The one to pay attention to is cover. It includes unloaded files the way track_files did, but it also restricts the report to the matching set given to it, so a single cover "lib/**/*.rb" will drop app/ from your report entirely. To keep the old behavior, pass every directory you want reported:
# Before
track_files "lib/**/*.rb"
# After: list everything, not just the previously-tracked glob
cover "lib/**/*.rb", "app/**/*.rb"
SimpleCov.start cannot be called from .simplecov
Calling SimpleCov.start from .simplecov is deprecated. Tracking still begins for backward compatibility, but you get a one-time warning, and a future release will require the explicit call to live in spec_helper.rb or test_helper.rb. Treat .simplecov as configuration only:
# .simplecov
SimpleCov.configure do
skip "/spec/"
minimum_coverage 90
end
# spec/spec_helper.rb, at the very top, before anything else is required
require "simplecov"
SimpleCov.start "rails"
:nocov: comments and associated configurations are deprecated
This means specifically the SimpleCov.nocov_token and SimpleCov.skip_token configurations. Every file still using # :nocov: emits a one-time warning to stderr at load time. Instances of # :nocov: must be replaced with the new directive comments, # simplecov:enable and # simplecov:disable:
# Before
# :nocov:
def hard_to_test
legacy_thing
end
# :nocov:
# After
# simplecov:disable
def hard_to_test
legacy_thing
end
# simplecov:enable
It’s worth mentioning that the directive comments also allow you to specify if you want to disable only line, branch or method coverage, like so: # simplecov:disable line. By default, all 3 are enabled.
branches_coverage_percent and methods_coverage_percent are deprecated
This will likely affect you only if you have custom formatters. SimpleCov::SourceFile#branches_coverage_percent and #methods_coverage_percent are now replaced by covered_percent which take a criterion argument that defaults to :line:
# Before
file.branches_coverage_percent
file.methods_coverage_percent
# After
file.covered_percent(:branch)
file.covered_percent(:method)
minimum_coverage_by_* setters deprecated
Finally, the minimum_coverage_by_file and minimum_coverage_by_group setters give way to the new coverage method’s minimum_per_file and minimum_per_group verbs:
# Before
SimpleCov.minimum_coverage_by_file line: 70, "app/x.rb" => 100
# After
SimpleCov.coverage(:line) do
minimum_per_file 70
minimum_per_file 100, only: "app/x.rb"
end
The no-arg getters are unchanged, and only the setter forms warn.
New features worth your time
Once you are migrated, there is a real payoff here.
Configuring criteria and scoping the report
The new criterion-first coverage method configures each criterion (:line, :branch, :method) in one place, with identical syntax regardless of which one you are configuring:
SimpleCov.start do
coverage :line do
minimum 90
minimum_per_file 80
maximum_drop 5
end
coverage :branch, minimum: 80, primary: true
end
The options are minimum, maximum, exact, maximum_drop, minimum_per_file (with only: overrides), and minimum_per_group.
Alongside it, SimpleCov.cover finally provides an allowlist. Where add_filter could only ever subtract, cover is the positive counterpart:
SimpleCov.start do
cover "app/**/*.rb", "lib/**/*.rb"
end
It accepts string globs, Regexps, blocks, or arrays of those, and multiple calls union together.
Finally, if you want to start from a blank slate, SimpleCov.no_default_skips opts out of the filters SimpleCov.start installs.
Directive comments and synthetic branches
On the branch side, SimpleCov.ignore_branches lets you opt out of the synthetic :else branches that Ruby’s Coverage library reports for constructs with no literal else keyword, which includes exhaustive case/in matches, case/when without else, ||=, &&=, and if or unless without else:
SimpleCov.start do
enable_coverage :branch
ignore_branches :implicit_else
end
Rails applications get something else useful here: ignore_branches :eval_generated and the new ignore_methods :eval_generated drop the phantom branch and method entries that macros like delegate inject.
Parallel runners
SimpleCov’s coordination with parallel runners now goes through a pluggable SimpleCov::ParallelAdapters chain instead of hard-coding the parallel_tests gem’s API. Two adapters ship, one wrapping the parallel_tests gem and a generic one for any runner following the TEST_ENV_NUMBER and PARALLEL_TEST_GROUPS convention. Custom runners can register their own adapter by subclassing SimpleCov::ParallelAdapters::Base. There is also a new SimpleCov.parallel_wait_timeout (default 60 seconds) for the case where one worker runs much heavier files and routinely finishes well after the others, so raise it if you want that worker’s coverage in the merge rather than having the threshold checks run against a partial total.
Conclusion
There are other features we decided not to mention for brevity’s sake. They can all be found in the changelog, at any rate. Given what has changed, most migrations to SimpleCov 1.0 should be straightforward and we recommend to go through with it because the new features add many quality of life improvements for gathering good coverage reports especially in CI, which is essential for dealing with tech debt and guaranteeing your app is functioning as expected.
Here at FastRuby.io we use SimpleCov heavily and it’s one of the staple gems we always end up recommending to clients if they aren’t using anything.
If you want to also know what we can do for you and your team to make you move faster and not get dragged down by endless bugs, performance issues and legacy problems no one understands anymore, send us a message and let’s talk! .