SimpleCov: Code Coverage for Ruby and Rails Projects

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
end
bundle install

Basic 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.html

The 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
end

For per-file thresholds:

SimpleCov.start 'rails' do
  minimum_coverage_by_file 70
end

Branch 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
end

Branch 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"
end

Groups 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
end

Merging 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'
end

SimpleCov 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
end

CI 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/"
end

Then in spec/spec_helper.rb:

require 'simplecov'
# SimpleCov loads .simplecov automatically when present

What 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.

Read more

Start now free