SimpleCov: Code Coverage for Ruby and Rails Projects
SimpleCov is the standard code coverage library for Ruby. It hooks into Ruby's Coverage module, tracks which lines and branches your tests exercise, and generates HTML reports so you can see exactly what's untested.
Installation
# Gemfile
group :test do
gem 'simplecov', require: false
endbundle installBasic Setup
SimpleCov must start before any application code is loaded. Place this at the very top of spec/spec_helper.rb (or test/test_helper.rb for Minitest):
require 'simplecov'
SimpleCov.start
# All other requires come AFTER
require 'rails_helper'For Rails projects, use the built-in Rails profile:
SimpleCov.start 'rails'The Rails profile automatically excludes config/, db/, and other non-testable files.
Reading the Report
After your test suite runs, SimpleCov writes an HTML report to coverage/index.html. Open it in a browser:
open coverage/index.htmlThe report shows:
- Line coverage — which lines were executed
- Branch coverage — which conditional branches were taken
- File-level breakdowns — click any file to see uncovered lines highlighted in red
Enforcing a Minimum Threshold
Add a coverage gate so CI fails if coverage drops:
SimpleCov.start 'rails' do
minimum_coverage 90
endFor per-file thresholds:
SimpleCov.start 'rails' do
minimum_coverage_by_file 70
endBranch Coverage
Line coverage misses untested branches — a line with x > 0 ? a : b is "covered" if it runs, even if only one branch was ever taken. Enable branch coverage explicitly:
SimpleCov.start 'rails' do
enable_coverage :branch
primary_coverage :branch # use branch % as the primary metric
minimum_coverage branch: 80, line: 90
endBranch coverage requires Ruby 2.5+.
Grouping Files
Organise the report by application layer:
SimpleCov.start 'rails' do
add_group "Models", "app/models"
add_group "Controllers", "app/controllers"
add_group "Services", "app/services"
add_group "Jobs", "app/jobs"
add_group "Serializers", "app/serializers"
add_group "Helpers", "app/helpers"
endGroups appear as tabs in the HTML report, making it easy to spot which layers have coverage gaps.
Excluding Files
Some files don't need coverage — initializers, concerns that are tested indirectly, generated code:
SimpleCov.start 'rails' do
add_filter "/config/"
add_filter "/bin/"
add_filter "app/channels" # if not testing Action Cable
add_filter do |source_file|
source_file.lines.count < 5 # skip tiny files
end
endMerging Multiple Test Suites
If you run RSpec and Minitest separately (or split into unit/integration suites), SimpleCov can merge the results:
# In each test helper:
SimpleCov.start 'rails' do
command_name 'RSpec' # or 'Minitest'
endSimpleCov writes .resultset.json files and merges them when all suites complete. The final report reflects combined coverage.
For parallel test execution (e.g., parallel_tests gem), enable the parallel formatter:
require 'simplecov'
SimpleCov.start 'rails' do
# Each process writes its own .resultset.json with a unique key
# SimpleCov merges automatically
endCI Integration
GitHub Actions
- name: Run tests
run: bundle exec rspec
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/Fail the build automatically with minimum_coverage — no extra CI step required.
Codecov / Coveralls
Both services accept SimpleCov's .resultset.json:
# Gemfile
gem 'codecov', require: false
# spec/spec_helper.rb
require 'simplecov'
require 'codecov'
SimpleCov.formatter = SimpleCov::Formatter::Codecov if ENV['CI']
SimpleCov.start 'rails'.simplecov File
For complex configurations, extract the setup into a .simplecov file in the project root:
# .simplecov
SimpleCov.start 'rails' do
enable_coverage :branch
minimum_coverage line: 90, branch: 80
minimum_coverage_by_file 60
add_group "Models", "app/models"
add_group "Services", "app/services"
add_group "Controllers", "app/controllers"
add_filter "/spec/"
add_filter "/config/"
endThen in spec/spec_helper.rb:
require 'simplecov'
# SimpleCov loads .simplecov automatically when presentWhat Coverage Doesn't Tell You
Coverage tells you which lines ran, not whether those lines were tested correctly. A test that calls user.save without asserting anything gives you coverage on save with zero confidence. The flip side: a critical code path with 100% coverage can still have bugs if the assertions are weak.
Use coverage as a floor — uncovered code is definitely untested, covered code is only possibly tested. Complement it with mutation testing (mutant gem) for deeper confidence.
24/7 Test Monitoring
SimpleCov measures how much of your code is covered; HelpMeTest monitors whether your covered code keeps passing. Connect your test suite and get alerted the moment a regression slips through. Start free at helpmetest.com.