Test Coverage with SimpleCov
Test coverage tells you what percentage of your code actually runs during tests. High coverage doesn’t guarantee good tests, but low coverage is a warning sign. SimpleCov makes measuring Ruby coverage straightforward.
Why Measure Coverage?
Coverage helps you find code that never gets exercised:
- Unreachable code from dead conditionals
- Missing test cases for edge cases
- Feature branches that were never tested
Coverage is a tool for discovering gaps, not a score to maximize. Aiming for 100% coverage often leads to meaningless tests just to tick a box.
Setting Up SimpleCov
Add SimpleCov to your Gemfile:
group :test do
gem 'simplecov', require: false
end
Then configure it at the very top of your test helper, before any other code loads:
# test/test_helper.rb
require 'simplecov'
SimpleCov.start 'rails' do
add_filter '/test/'
add_filter '/config/'
add_filter '/vendor/'
end
require_relative '../config/environment'
require 'rails/test_helper'
The start method begins coverage measurement. Calling it first ensures all code gets tracked.
Coverage Modes
SimpleCov supports different coverage modes:
SimpleCov.start 'rails'— Rails app with default filtersSimpleCov.start 'gem_name'— For gem developmentSimpleCov.start— Vanilla Ruby project
Run your test suite as normal:
bundle exec rake test
# or
bundle exec rspec
After tests complete, SimpleCov generates an HTML report in coverage/.
Understanding Coverage Reports
Open coverage/index.html in your browser. You’ll see:
Line Coverage
The percentage of executable lines that ran. Each line of code shows green (covered), red (not covered), or gray (not executable).
Branch Coverage
Tracks whether both branches of conditionals were exercised:
def status
if approved?
"approved" # branch A
else
"pending" # branch B
end
end
If tests only call status when approved? returns true, branch B shows uncovered.
File List
Sorted by coverage percentage. Focus on important files with low coverage first—not just the files with the lowest numbers.
Interpreting Coverage Data
What’s a Good Coverage Number?
There’s no universal target. Many teams use 80% as a starting point:
- Below 50%: Likely significant untested functionality
- 50-80%: Room for improvement
- Above 80%: Decide if remaining gaps are intentional
Coverage Doesn’t Tell the Whole Story
High coverage with bad tests is worse than moderate coverage with good tests:
# Bad test that gives false confidence
def test_calculator
assert true # This passes but tests nothing!
end
This achieves 100% coverage but validates nothing. Review what your tests actually assert.
Using Coverage to Guide Testing
Find the Gaps
When you see red in coverage reports, ask:
- Is this code dead? If so, remove it.
- Is this an edge case I should test? Add a test.
- Is this error handling code? Make sure you test failure paths.
Example: Adding Missing Tests
Suppose you have a payment processor:
class PaymentProcessor
def charge(amount, card)
return :invalid_amount if amount <= 0
return :card_declined unless card.valid?
# Process payment...
:success
end
end
If coverage shows the card_declined branch untested, add:
def test_charge_declines_invalid_card
card = Card.new(valid: false)
result = processor.charge(100, card)
assert_equal :card_declined, result
end
Integration with CI
Run SimpleCov in your CI pipeline to track coverage over time:
# .github/workflows/ci.yml
- name: Run tests with coverage
run: bundle exec rake test
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage.xml
Create a coverage badge in your README:
[](https://codecov.io/gh/yourorg/yourrepo)
Filtering and Grouping
Exclude files that don’t need testing:
SimpleCov.start do
add_filter '/app/jobs/application_job.rb'
add_filter '/app/mailers/application_mailer.rb'
add_filter '/config/'
# Group related files
add_group 'Models', 'app/models/'
add_group 'Controllers', 'app/controllers/'
add_group 'Services', 'app/services/'
end
Coverage Thresholds
Fail builds when coverage drops:
SimpleCov.start do
minimum_coverage 80
# or per-group thresholds
minimum_coverage_by_group(
models: 90,
services: 80
)
end
This stops PRs with regressing coverage from merging.
Common Pitfalls
Testing Generated Code
Exclude scaffolds and migrations:
add_filter '/app/controllers/application_controller.rb'
add_filter '/db/migrate/'
Coverage Only at 100%
Don’t chase 100%. Focus on meaningful coverage of business logic, not boilerplate.
Forgetting to Start SimpleCov First
Always put SimpleCov.start at the very top of test helper. Code loaded before it won’t be tracked.
Summary
SimpleCov integrates seamlessly with Ruby’s test frameworks. Run it locally to find untested code, add it to CI to track coverage over time, and use coverage data strategically—not as a vanity metric.
Start with reasonable expectations (70-80%), focus on business-critical code, and review what your tests actually assert. Coverage is a guide, not a goal.
See Also
- Minitest Basics — Setting up your first Ruby tests
- Mocking and Stubbing with Minitest — Testing dependencies
- Integration Testing in Ruby — Beyond unit tests
- CI Setup with GitHub Actions — Automating your test suite