CrackedRuby logo

CrackedRuby

SimpleCov

A comprehensive guide to SimpleCov for code coverage analysis in Ruby applications and test suites.

Testing and Quality Code Quality Tools
8.3.3

Overview

SimpleCov analyzes Ruby code execution during test runs to generate coverage reports. The gem instruments Ruby code at runtime, tracking which lines execute during test suite runs and producing detailed coverage statistics. SimpleCov operates by hooking into Ruby's coverage API and wrapping the standard library's Coverage module with additional reporting capabilities.

The gem centers around the SimpleCov module, which provides the primary interface for configuration and report generation. The SimpleCov::Result class represents coverage data for a complete test run, while SimpleCov::SourceFile objects contain line-by-line coverage information for individual files. Coverage collection happens automatically once SimpleCov starts, requiring no modification to application code.

SimpleCov supports multiple output formats including HTML, JSON, and terminal reports. The HTML formatter generates interactive reports showing covered and uncovered lines with color-coded highlighting. JSON output enables integration with external tools and continuous integration systems.

# Basic setup in test helper
require 'simplecov'
SimpleCov.start

# Configure coverage thresholds
SimpleCov.minimum_coverage 90
SimpleCov.minimum_coverage_by_file 80

The gem tracks line coverage by default but supports branch coverage analysis on Ruby 2.5+. Branch coverage provides deeper insight into conditional logic execution, tracking whether both sides of if/else statements execute during testing. SimpleCov integrates with major testing frameworks including RSpec, Test::Unit, and Minitest without requiring framework-specific configuration.

Coverage data persists between test runs in the .simplecov_resultset.json file, enabling merging of results from multiple test processes or CI matrix builds. This persistence allows SimpleCov to generate accurate coverage reports even when tests run in parallel or across different environments.

Basic Usage

SimpleCov activation requires calling SimpleCov.start before loading application code, typically in a test helper file. The gem begins tracking coverage immediately after start, so placement matters for accurate results.

# spec/spec_helper.rb or test/test_helper.rb
require 'simplecov'
SimpleCov.start

# Load application after starting SimpleCov
require_relative '../lib/my_app'

The start method accepts configuration blocks for immediate setup. Common configurations include specifying coverage directories, setting minimum thresholds, and selecting formatters:

SimpleCov.start do
  add_filter '/spec/'
  add_filter '/test/'
  
  minimum_coverage 85
  minimum_coverage_by_file 75
  
  formatter SimpleCov::Formatter::MultiFormatter.new([
    SimpleCov::Formatter::HTMLFormatter,
    SimpleCov::Formatter::SimpleFormatter
  ])
end

Coverage collection happens transparently during test execution. SimpleCov tracks every line that executes, building internal coverage maps without impacting application functionality. After test completion, SimpleCov generates reports based on collected data.

Filters exclude specific files or directories from coverage analysis. The add_filter method accepts strings, regular expressions, or blocks for flexible filtering:

# Exclude directories
SimpleCov.add_filter '/vendor/'
SimpleCov.add_filter '/gems/'

# Pattern-based filtering  
SimpleCov.add_filter /\.proto$/

# Block-based filtering
SimpleCov.add_filter do |src_file|
  src_file.lines.count < 10
end

Groups organize coverage results into logical sections for reporting. Groups help separate different application components in generated reports:

SimpleCov.add_group 'Models', 'app/models'
SimpleCov.add_group 'Controllers', 'app/controllers'
SimpleCov.add_group 'Libraries', 'lib'
SimpleCov.add_group 'Long files' do |src_file|
  src_file.lines.count > 100
end

Coverage thresholds enforce minimum coverage requirements. SimpleCov exits with non-zero status codes when coverage falls below configured thresholds, enabling CI integration:

SimpleCov.minimum_coverage 90
SimpleCov.minimum_coverage_by_file 80

# Different thresholds for different groups
SimpleCov.minimum_coverage({
  'Models' => 95,
  'Controllers' => 85,
  'Libraries' => 75
})

Advanced Usage

SimpleCov provides sophisticated configuration options for complex project requirements. The configure block enables detailed customization beyond basic start parameters:

SimpleCov.configure do
  load_profile 'rails'
  
  coverage_dir 'coverage_reports'
  command_name 'RSpec'
  
  merge_timeout 600
  
  add_filter do |src_file|
    !src_file.filename.match(/\/app\//)
  end
  
  track_files '{app,lib}/**/*.rb'
  
  formatter SimpleCov::Formatter::MultiFormatter.new([
    SimpleCov::Formatter::HTMLFormatter,
    SimpleCov::Formatter::JSONFormatter,
    SimpleCov::Formatter::LcovFormatter
  ])
end

Profiles provide predefined configurations for common project types. The Rails profile automatically excludes typical Rails directories that shouldn't count toward coverage:

SimpleCov.start 'rails' do
  minimum_coverage 90
  
  add_group 'Policies', 'app/policies'
  add_group 'Serializers', 'app/serializers'
  add_group 'Workers', 'app/workers'
end

Branch coverage analysis provides deeper insight into conditional logic execution. Enable branch coverage to track whether all branches of if/else statements, case/when blocks, and boolean expressions execute:

SimpleCov.start do
  enable_coverage :branch
  primary_coverage :branch
  
  minimum_coverage({ line: 90, branch: 80 })
end

Result merging combines coverage data from multiple test runs or parallel processes. Configure merge timeout and result expiration to handle various testing scenarios:

SimpleCov.configure do
  merge_timeout 3600 # 1 hour
  
  # Custom merge logic
  SimpleCov.merge_result_sets do |result_sets|
    result_sets.select { |rs| rs['timestamp'] > (Time.now - 3600) }
  end
end

Custom formatters enable specialized output formats. Create formatter classes implementing the format method:

class TeamCityFormatter
  def format(result)
    result.groups.each do |name, files|
      coverage = (files.covered_percent * 100).round(2)
      puts "##teamcity[buildStatisticValue key='CodeCoverageAbs#{name}' value='#{coverage}']"
    end
  end
end

SimpleCov.formatter = TeamCityFormatter

The at_exit hook controls when SimpleCov generates reports. Customize exit behavior for non-standard test execution patterns:

SimpleCov.at_exit do |result|
  if ENV['COVERAGE_REPORT'] == 'true'
    result.format!
  end
  
  puts "Coverage: #{result.covered_percent.round(2)}%"
  
  exit 1 if result.covered_percent < 85
end

File tracking configuration determines which files appear in coverage reports even when never loaded. Track files enables coverage reporting for completely unused files:

SimpleCov.track_files 'lib/**/*.rb'
SimpleCov.track_files '{app,lib}/**/*.{rb,rake}'

# Conditional tracking
SimpleCov.track_files do
  Dir.glob('lib/**/*.rb').reject { |f| f.match(/deprecated/) }
end

Testing Strategies

SimpleCov integration varies across testing frameworks but follows consistent patterns. Configure SimpleCov before loading application code and after requiring the testing framework:

# RSpec setup in spec/spec_helper.rb
require 'rspec'
require 'simplecov'

SimpleCov.start 'rails' do
  add_filter '/spec/'
  minimum_coverage 85
end

RSpec.configure do |config|
  config.filter_run_when_matching :focus
end

Parallel testing requires special consideration for coverage merging. SimpleCov automatically handles parallel test execution when using tools like parallel_tests:

# Setup for parallel_tests gem
SimpleCov.start do
  if ENV['TEST_ENV_NUMBER']
    SimpleCov.command_name "RSpec-#{ENV['TEST_ENV_NUMBER']}"
  end
  
  merge_timeout 600
end

Test coverage analysis benefits from strategic test organization. Structure tests to exercise different code paths and edge cases:

# Example of comprehensive test coverage
describe UserRegistration do
  let(:valid_params) { { email: 'user@example.com', password: 'secret123' } }
  
  context 'with valid parameters' do
    it 'creates user successfully' do
      registration = UserRegistration.new(valid_params)
      expect(registration.call).to be_success
    end
  end
  
  context 'with invalid email' do
    it 'fails with validation error' do
      params = valid_params.merge(email: 'invalid')
      registration = UserRegistration.new(params)
      expect(registration.call).to be_failure
    end
  end
  
  context 'with duplicate email' do
    before { create(:user, email: valid_params[:email]) }
    
    it 'fails with uniqueness error' do
      registration = UserRegistration.new(valid_params)
      expect(registration.call).to be_failure
    end
  end
end

Branch coverage testing requires exercising all conditional paths. Structure tests to cover both positive and negative branches:

describe PaymentProcessor do
  describe '#process' do
    context 'when payment succeeds' do
      before { stub_payment_gateway_success }
      
      it 'returns success result' do
        result = processor.process(payment_data)
        expect(result).to be_success
      end
    end
    
    context 'when payment fails due to insufficient funds' do
      before { stub_payment_gateway_failure(:insufficient_funds) }
      
      it 'returns failure with specific error' do
        result = processor.process(payment_data)
        expect(result).to be_failure
        expect(result.error_code).to eq(:insufficient_funds)
      end
    end
    
    context 'when gateway times out' do
      before { stub_payment_gateway_timeout }
      
      it 'raises timeout exception' do
        expect { processor.process(payment_data) }.to raise_error(Timeout::Error)
      end
    end
  end
end

Coverage-driven test writing identifies gaps in test coverage. Use SimpleCov reports to find untested code paths and write targeted tests:

# After running tests, check coverage report for missed lines
# Then write tests for uncovered branches

describe CacheManager do
  describe '#fetch' do
    context 'when cache hit' do
      before { cache.write('key', 'cached_value') }
      
      it 'returns cached value without calling block' do
        result = manager.fetch('key') { expensive_operation }
        expect(result).to eq('cached_value')
        expect(expensive_operation).not_to have_been_called
      end
    end
    
    context 'when cache miss' do
      it 'calls block and caches result' do
        result = manager.fetch('key') { 'fresh_value' }
        expect(result).to eq('fresh_value')
        expect(cache.read('key')).to eq('fresh_value')
      end
    end
    
    context 'when cache raises exception' do
      before { allow(cache).to receive(:read).and_raise(Redis::TimeoutError) }
      
      it 'falls back to block execution' do
        result = manager.fetch('key') { 'fallback_value' }
        expect(result).to eq('fallback_value')
      end
    end
  end
end

Production Patterns

SimpleCov deployment in production environments requires careful configuration to minimize performance impact. Production coverage analysis helps identify dead code and actual usage patterns in live applications:

# Production coverage setup
SimpleCov.start do
  if Rails.env.production? && ENV['ENABLE_COVERAGE']
    coverage_dir 'tmp/coverage'
    
    add_filter '/vendor/'
    add_filter '/config/'
    add_filter do |src_file|
      src_file.filename.match?(/\/db\/migrate\//)
    end
    
    formatter SimpleCov::Formatter::JSONFormatter
    
    # Minimize memory usage
    merge_timeout 60
    maximum_coverage_drop 5
  end
end

Continuous Integration integration enables automated coverage reporting and threshold enforcement. Configure CI pipelines to generate and publish coverage reports:

# GitHub Actions example
- name: Run tests with coverage
  run: |
    bundle exec rspec
    
- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v1
  with:
    file: ./coverage/.resultset.json
    
- name: Check coverage threshold
  run: |
    if [ $(cat coverage/.last_run.json | jq '.result.covered_percent') -lt 85 ]; then
      echo "Coverage below threshold"
      exit 1
    fi

Docker environments require special handling for coverage data persistence. Mount coverage directories as volumes to preserve reports across container restarts:

# Dockerfile additions for coverage
RUN mkdir -p /app/coverage
VOLUME ["/app/coverage"]

# In docker-compose.yml
services:
  app:
    volumes:
      - ./coverage:/app/coverage
    environment:
      - COVERAGE=true

Performance monitoring becomes critical in production coverage scenarios. SimpleCov adds overhead to application execution, requiring monitoring to prevent performance degradation:

SimpleCov.configure do
  if Rails.env.production?
    # Sample coverage to reduce overhead
    if rand < 0.1 # Cover 10% of requests
      start
    end
    
    # Custom at_exit to handle web server shutdown
    at_exit do |result|
      # Save results asynchronously
      Thread.new do
        result.format!
        upload_to_storage(result)
      end
    end
  end
end

Multi-server deployments require coverage result aggregation. Implement strategies to combine coverage data from multiple application instances:

class CoverageAggregator
  def self.merge_server_results
    results = []
    
    Dir.glob('/shared/coverage/server_*/resultset.json').each do |file|
      results << JSON.parse(File.read(file))
    end
    
    merged_result = SimpleCov::ResultMerger.merge_results(*results)
    
    File.write('/shared/coverage/merged_resultset.json', merged_result.to_json)
    
    SimpleCov::Formatter::HTMLFormatter.new.format(merged_result)
  end
end

Coverage reporting automation helps maintain code quality standards. Implement automated reports that integrate with project management and notification systems:

class CoverageReporter
  def self.generate_daily_report
    result = SimpleCov::Result.from_hash(
      JSON.parse(File.read('.simplecov_resultset.json'))
    )
    
    report = {
      date: Date.current,
      overall_coverage: result.covered_percent.round(2),
      file_count: result.files.count,
      groups: result.groups.transform_values { |files| files.covered_percent.round(2) },
      files_below_threshold: result.files.select { |f| f.covered_percent < 80 }
    }
    
    SlackNotifier.ping(format_coverage_message(report))
    ProjectTracker.create_issue(report) if report[:overall_coverage] < 85
  end
  
  private
  
  def self.format_coverage_message(report)
    "Daily Coverage Report: #{report[:overall_coverage]}% overall, " \
    "#{report[:files_below_threshold].count} files below threshold"
  end
end

Common Pitfalls

SimpleCov activation timing creates the most frequent configuration errors. Starting SimpleCov after loading application code results in zero coverage because the gem cannot instrument already-loaded files:

# INCORRECT: SimpleCov starts after loading app
require_relative '../lib/my_app'
require 'simplecov'
SimpleCov.start

# CORRECT: SimpleCov starts before loading app  
require 'simplecov'
SimpleCov.start
require_relative '../lib/my_app'

Exit hook conflicts occur when multiple gems override Ruby's at_exit behavior. SimpleCov relies on at_exit hooks to generate final reports, but other gems can interfere:

# Problematic interaction with other gems
require 'simplecov'
require 'some_gem_that_uses_at_exit'

SimpleCov.start

# Solution: Force SimpleCov report generation
SimpleCov.at_exit do |result|
  result.format! unless ENV['NO_COVERAGE']
end

Filter misconfiguration leads to unexpected coverage results. Overly broad filters can exclude important application code, while insufficient filtering includes test files in coverage calculations:

# Problematic: Too broad, excludes important files
SimpleCov.add_filter '/app/' # Excludes entire app directory

# Problematic: Doesn't exclude test files
SimpleCov.add_filter '/spec/models/' # Only excludes model specs

# Better: Specific exclusions
SimpleCov.add_filter '/spec/'
SimpleCov.add_filter '/test/'  
SimpleCov.add_filter '/features/'
SimpleCov.add_filter do |src_file|
  src_file.filename.match?(/\/config\//)
end

Branch coverage interpretation requires understanding Ruby's branch tracking behavior. Ruby counts ternary operators, logical operators, and rescue clauses as branches:

# This code has more branches than obvious
def process_user(user)
  return unless user&.active? # Branch 1: &&, Branch 2: unless
  
  status = user.premium? ? 'premium' : 'standard' # Branch 3: ternary
  
  begin
    send_notification(user, status)
  rescue NotificationError
    log_error("Failed to notify user #{user.id}") # Branch 4: rescue
  end
  
  user.admin? || user.moderator? # Branch 5: ||
end

Parallel test execution creates result merging challenges. SimpleCov may generate incomplete coverage reports when parallel processes don't properly coordinate result sharing:

# Configure proper parallel testing support
SimpleCov.start do
  # Use unique command names for each process
  if ENV['TEST_ENV_NUMBER'].present?
    command_name "RSpec-Process-#{ENV['TEST_ENV_NUMBER']}"
  end
  
  # Extend merge timeout for slow test suites
  merge_timeout 600
  
  # Ensure all processes wait for result merging
  SimpleCov.use_merging true
end

Memory consumption grows significantly in large codebases with comprehensive coverage tracking. SimpleCov maintains detailed line-by-line data for every tracked file:

# Reduce memory usage for large applications
SimpleCov.start do
  # Exclude large generated files
  add_filter do |src_file|
    src_file.lines.count > 1000 && src_file.filename.match?(/generated/)
  end
  
  # Use JSON formatter instead of HTML for lower memory usage
  formatter SimpleCov::Formatter::JSONFormatter
  
  # Limit tracked files
  track_files '{app,lib}/**/*.rb'
end

Coverage threshold failures in CI environments often result from environmental differences between local and CI test execution. Different Ruby versions, gem versions, or test data can cause coverage variations:

# Make thresholds environment-aware
SimpleCov.configure do
  if ENV['CI']
    minimum_coverage 88 # Slightly lower for CI variability
    maximum_coverage_drop 3
  else
    minimum_coverage 90 # Stricter for local development  
  end
  
  # Allow coverage drops during refactoring
  if ENV['REFACTORING_BRANCH']
    refuse_coverage_drop false
  end
end

Report generation timing issues occur when applications exit before SimpleCov completes report writing. This happens frequently in containerized environments or applications with custom exit handling:

# Ensure report generation completes
SimpleCov.at_exit do |result|
  begin
    result.format!
  rescue => e
    warn "SimpleCov report generation failed: #{e.message}"
    warn e.backtrace.join("\n")
  end
end

# For applications with custom exit handling
class Application
  def shutdown
    SimpleCov.result.format! if defined?(SimpleCov)
    super
  end
end

Reference

Core Methods

Method Parameters Returns Description
SimpleCov.start block (optional) nil Begins coverage tracking and accepts configuration block
SimpleCov.configure block nil Sets configuration options without starting coverage
SimpleCov.result - SimpleCov::Result Returns current coverage result object
SimpleCov.running - Boolean Indicates whether coverage tracking is active
SimpleCov.clear - nil Resets all coverage data and configuration
SimpleCov.merge_timeout timeout (Integer) Integer Sets/gets result merge timeout in seconds

Configuration Methods

Method Parameters Returns Description
add_filter pattern (String/Regex/Proc) nil Excludes files from coverage analysis
add_group name (String), pattern nil Creates named group for organizing results
minimum_coverage threshold (Numeric/Hash) Numeric/Hash Sets minimum coverage requirements
minimum_coverage_by_file threshold (Numeric) Numeric Sets per-file minimum coverage
maximum_coverage_drop drop (Numeric) Numeric Sets maximum allowed coverage decrease
coverage_dir path (String) String Sets output directory for reports
command_name name (String) String Sets identifier for result merging

Coverage Control

Method Parameters Returns Description
enable_coverage type (Symbol) nil Enables specific coverage types (:line, :branch)
disable_coverage type (Symbol) nil Disables specific coverage types
primary_coverage type (Symbol) Symbol Sets primary coverage type for thresholds
track_files pattern (String/Proc) nil Includes files in coverage even when not loaded
refuse_coverage_drop enabled (Boolean) Boolean Controls whether coverage drops cause failures

Result Methods

Method Parameters Returns Description
result.covered_percent - Float Overall coverage percentage
result.covered_lines - Integer Total number of covered lines
result.total_lines - Integer Total number of trackable lines
result.files - Array<SimpleCov::SourceFile> All tracked source files
result.groups - Hash<String, Array> Grouped files by configured groups
result.command_name - String Command identifier for this result
result.format! - nil Generates reports using configured formatters

Built-in Formatters

Formatter Output Description
SimpleCov::Formatter::HTMLFormatter HTML files Interactive web-based coverage reports
SimpleCov::Formatter::SimpleFormatter Terminal text Basic text output with coverage percentages
SimpleCov::Formatter::JSONFormatter JSON file Machine-readable coverage data
SimpleCov::Formatter::LcovFormatter LCOV format Compatible with LCOV tools and services
SimpleCov::Formatter::MultiFormatter Multiple Combines multiple formatters

Configuration Profiles

Profile Filters Groups Description
rails /spec/, /test/, /features/, /autotest/, /cucumber/, /db/, /config/, /vendor/ Controllers, Models, Helpers, Libraries, Plugins Rails application defaults
test_frameworks /spec/, /test/, /features/, /autotest/, /cucumber/ - Basic test file exclusions

Environment Variables

Variable Type Description
COVERAGE Boolean Enables/disables SimpleCov when set to 'true'
SIMPLECOV_COMMAND_NAME String Sets command name for result identification
TEST_ENV_NUMBER String Process identifier for parallel testing
CI Boolean Indicates continuous integration environment

Exit Codes

Code Condition
0 Coverage meets all configured thresholds
1 Coverage below minimum threshold
2 Coverage drop exceeds maximum allowed drop
3 SimpleCov configuration error

Coverage Types

Type Ruby Version Description
:line 1.9+ Tracks line execution coverage
:branch 2.5+ Tracks conditional branch coverage
:method 2.6+ Tracks method call coverage

Common Filter Patterns

# Standard exclusions
add_filter '/spec/'
add_filter '/test/'
add_filter '/vendor/'
add_filter '/config/'

# Pattern-based exclusions
add_filter /\.proto$/
add_filter /_pb\.rb$/
add_filter /\/db\/migrate\//

# Size-based exclusions
add_filter { |src| src.lines.count < 5 }
add_filter { |src| src.filename.match?(/generated/) }