Test Coverage with SimpleCov

· 4 min read · Updated March 17, 2026 · intermediate
ruby testing coverage simplecov tdd

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 filters
  • SimpleCov.start 'gem_name' — For gem development
  • SimpleCov.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:

  1. Is this code dead? If so, remove it.
  2. Is this an edge case I should test? Add a test.
  3. 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:

[![Coverage](https://codecov.io/gh/yourorg/yourrepo/branch/main/graph/badge.svg)](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