CrackedRuby CrackedRuby

Overview

Test coverage measures the proportion of source code executed during test runs. Coverage analysis identifies untested code paths, helping developers assess test suite completeness and find gaps in verification logic. The metric originated in the 1960s with structural testing research and became mainstream with automated testing frameworks.

Coverage tools instrument code to track execution during tests. When tests run, the instrumentation records which lines, branches, or methods execute. After the test suite completes, the tool generates reports showing covered and uncovered code. Coverage data appears as percentages, visual annotations, or detailed execution counts.

Ruby's standard library includes a Coverage module that tracks line and branch execution. Third-party gems like SimpleCov build on this foundation, providing HTML reports, threshold enforcement, and integration with continuous integration systems. Coverage analysis runs automatically in development and CI environments, catching untested code before deployment.

# Example: Basic coverage measurement
require 'coverage'
Coverage.start

def calculate_discount(price, customer_type)
  return price * 0.9 if customer_type == :premium
  return price * 0.95 if customer_type == :standard
  price
end

# Test execution
calculate_discount(100, :premium)  # => 90.0
calculate_discount(100, :standard) # => 95.0

result = Coverage.result
# => {"/path/to/file.rb" => [1, nil, 1, 1, 1, 1, nil]}

Coverage metrics guide test development but do not guarantee correctness. High coverage indicates executed code, not verified behavior. A test that calls a method without asserting outputs achieves coverage without validation. Effective testing combines coverage analysis with assertion quality, edge case verification, and integration testing.

Different coverage types measure distinct aspects of execution. Line coverage tracks executed source lines. Branch coverage measures conditional paths taken. Method coverage identifies called methods. Each metric reveals different gaps in test suites. Comprehensive testing strategies use multiple coverage types to identify blind spots that single metrics miss.

Key Principles

Execution Tracking forms the foundation of coverage measurement. Instrumentation code wraps application code, recording execution events. When a line executes, the instrumentation increments a counter. When tests complete, the coverage tool aggregates counters into metrics. Ruby's Coverage module hooks into the interpreter's trace functionality, collecting execution data with minimal overhead.

Line Coverage measures the percentage of source lines executed during tests. The metric counts executable lines, excluding comments and blank lines. A line counts as covered if it executes at least once, regardless of how many times. Line coverage identifies obviously untested code but misses untaken conditional branches within covered lines.

# Line coverage example
def process_order(order, inventory)
  return nil unless order.valid?           # Line 1: covered
  
  items = order.items.select do |item|     # Line 2: covered
    inventory.available?(item)             # Line 3: covered
  end                                      # Line 4: covered
  
  return nil if items.empty?               # Line 5: NOT covered if test data always has available items
  
  Order.new(items)                         # Line 6: covered
end

# Test covers lines 1, 2, 3, 4, 6 but not line 5
# Line coverage: 83% (5 of 6 lines)
# Branch coverage would be lower - the empty items path never executes

Branch Coverage tracks whether both true and false paths of conditionals execute. A single if statement creates two branches: the then-clause and the else-clause or continuation. Branch coverage exceeds line coverage in thoroughness by ensuring tests exercise all decision outcomes. Code with 100% line coverage may have 50% branch coverage if tests never trigger false conditions.

Method Coverage identifies which methods the test suite calls. This coarse-grained metric quickly spots completely untested modules. Method coverage complements line coverage by showing high-level test distribution across a codebase. A class with high line coverage but low method coverage suggests tests concentrate on a few methods while ignoring others.

Coverage Percentage Calculation divides covered units by total coverable units. For line coverage: (executed_lines / executable_lines) * 100. The denominator excludes comments, blank lines, and syntactic elements. Different tools count executable lines differently, making cross-tool comparisons unreliable. Coverage percentages provide relative progress indicators within a single tool, not absolute quality measures.

Coverage Aggregation combines metrics from multiple test runs. Unit tests, integration tests, and system tests execute different code paths. Running coverage across all test types reveals the complete tested surface. Aggregated coverage shows which code no test type exercises, identifying gaps in the overall testing strategy.

Temporal Coverage Analysis tracks coverage changes over time. New code with low coverage indicates incomplete test development. Decreasing coverage suggests test deletion or code additions without corresponding tests. Coverage trends inform testing priorities more than absolute percentages. A pull request that adds 100 lines with 20% coverage deserves more scrutiny than one adding 10 lines with 60% coverage.

Ruby Implementation

Ruby's Coverage module resides in the standard library, providing built-in coverage tracking without external dependencies. The module supports line coverage, branch coverage, method coverage, and combinations thereof. Coverage starts before code loads and generates reports after execution completes.

# Starting line coverage
require 'coverage'
Coverage.start(lines: true)

# Application code loads here
require_relative 'lib/calculator'

# Run tests
calculator = Calculator.new
calculator.add(5, 3)
calculator.multiply(2, 4)

# Retrieve coverage data
result = Coverage.result
# => {
#   "/path/to/lib/calculator.rb" => {
#     lines: [1, 1, nil, 1, 1, nil, 1, 0, nil]
#   }
# }

The Coverage.result hash maps file paths to execution counts. For line coverage, array indices represent line numbers (0-indexed offset), and values show execution counts. A nil value indicates a non-executable line. Zero means an executable line never ran. Positive integers count executions.

Branch Coverage in Ruby tracks conditional execution paths. Each branch receives a unique identifier, and the coverage report shows which branches executed. Branch coverage requires explicit activation through the branches option.

Coverage.start(lines: true, branches: true)

def calculate_shipping(weight, express)
  base = weight * 5
  if express
    base * 2
  else
    base
  end
end

calculate_shipping(10, true)

result = Coverage.result
# => {
#   "/path/to/file.rb" => {
#     lines: [1, 1, 1, 1, 0, 1],
#     branches: {
#       [:if, 0, 3, 2, 7, 4] => {
#         [:then, 1, 4, 4, 4, 9] => 1,
#         [:else, 2, 6, 4, 6, 8] => 0
#       }
#     }
#   }
# }

The branches hash uses arrays as keys representing branch locations: [:type, id, start_line, start_col, end_line, end_col]. The value hash maps branch paths to execution counts. This example shows the then-branch executed once while the else-branch never ran.

Method Coverage identifies called methods. The Coverage module tracks method definitions and invocations, reporting which methods tests exercise.

Coverage.start(methods: true)

class PaymentProcessor
  def process_credit_card(amount)
    amount * 1.03  # 3% fee
  end
  
  def process_paypal(amount)
    amount * 1.05  # 5% fee
  end
  
  def process_crypto(amount)
    amount * 1.01  # 1% fee
  end
end

processor = PaymentProcessor.new
processor.process_credit_card(100)
processor.process_paypal(50)

result = Coverage.result
# Shows process_credit_card and process_paypal called, process_crypto not called

SimpleCov Integration wraps the Coverage module with developer-friendly reporting. SimpleCov generates HTML reports, enforces coverage thresholds, and integrates with CI systems. The gem automatically configures coverage for test frameworks like RSpec, Minitest, and Cucumber.

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

SimpleCov.start do
  add_filter '/test/'
  add_filter '/spec/'
  
  add_group 'Models', 'app/models'
  add_group 'Controllers', 'app/controllers'
  add_group 'Services', 'app/services'
  
  minimum_coverage 90
  maximum_coverage_drop 5
end

# SimpleCov automatically tracks coverage when tests run
# Generates HTML report in coverage/ directory

SimpleCov filters exclude test files, vendor code, and generated files from coverage calculations. Groups organize coverage reports by application structure. Minimum coverage thresholds fail builds when coverage drops below targets. Maximum coverage drop prevents significant regressions.

Coverage Formatters control report output. SimpleCov supports multiple formatters simultaneously, generating HTML for humans and JSON for automated tools.

SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
  SimpleCov::Formatter::HTMLFormatter,
  SimpleCov::Formatter::JSONFormatter,
  SimpleCov::Formatter::LcovFormatter
])

Tools & Ecosystem

SimpleCov dominates Ruby coverage tooling. The gem handles instrumentation setup, result aggregation across test runs, and report generation. SimpleCov automatically detects test frameworks and configures coverage appropriately. HTML reports highlight uncovered lines in red, providing visual feedback for test development.

Installation and basic setup:

# Gemfile
group :test do
  gem 'simplecov', require: false
end

# test/test_helper.rb (must be first require)
require 'simplecov'
SimpleCov.start

require 'minitest/autorun'
# ... rest of test configuration

Coverage Groups organize reports by logical application components. Groups appear as separate sections in HTML reports, showing coverage per module rather than per file. This organization helps identify undertested application areas.

SimpleCov.start 'rails' do
  add_group 'API', 'app/api'
  add_group 'Background Jobs', 'app/jobs'
  add_group 'Presenters', 'app/presenters'
  add_group 'Validators', 'app/validators'
  
  add_filter 'app/admin'  # Exclude admin interface
  add_filter 'lib/tasks'  # Exclude rake tasks
end

SimpleCov Profiles provide preset configurations for common frameworks. The rails profile automatically configures groups for models, controllers, helpers, and mailers. The test_frameworks profile filters test directories. Custom profiles extract shared configuration across projects.

SimpleCov.profiles.define 'myapp' do
  add_filter '/config/'
  add_filter '/db/'
  
  add_group 'Long Files' do |src_file|
    src_file.lines.count > 100
  end
  
  add_group 'Short Files' do |src_file|
    src_file.lines.count < 50
  end
end

SimpleCov.start 'myapp'

Coverage Merging combines results from parallel test runs. CI systems often split test suites across multiple processes for speed. SimpleCov merges these separate coverage results into unified reports.

# In test helper, before SimpleCov.start
SimpleCov.command_name "Job #{ENV['CI_NODE_INDEX']}" if ENV['CI']
SimpleCov.use_merging true

SimpleCov.start do
  merge_timeout 3600  # Merge results from last hour
end

Coveralls and CodeCov integrate coverage reporting with version control. These services track coverage across commits, showing trends and highlighting regressions in pull requests. Both support Ruby through SimpleCov formatters.

# For Coveralls
gem 'coveralls', require: false

# In test helper
require 'coveralls'
Coveralls.wear!

# For CodeCov
# SimpleCov generates JSON, CodeCov CLI uploads it
# No code changes required

Undercover Gem analyzes coverage for changed lines in git branches. Rather than measuring entire project coverage, Undercover reports coverage for modified code only. This focuses testing effort on new and changed functionality.

# Run in CI or pre-commit
undercover --compare origin/main
# Reports coverage only for lines changed since branching from main

Mutant Gem extends coverage analysis with mutation testing. Mutant modifies code and reruns tests, checking if tests catch the changes. Code with 100% line coverage but passing tests under mutation lacks assertion quality. Mutant reveals these gaps.

# Gemfile
gem 'mutant-rspec'

# Run mutation testing
mutant --include lib --require myapp --use rspec 'MyApp*'
# Generates mutants and verifies tests catch them

Ruby Coverage Module provides the low-level instrumentation for all coverage tools. Direct use of the Coverage module suits specialized scenarios where SimpleCov's automation interferes with test setup.

require 'coverage'

# Fine-grained control over coverage types
Coverage.start(
  lines: true,
  branches: true,
  methods: true,
  oneshot_lines: true  # Performance optimization
)

# Manual result retrieval and processing
coverage_data = Coverage.result
coverage_data.each do |file, data|
  line_coverage = data[:lines]
  total_lines = line_coverage.compact.size
  covered_lines = line_coverage.count { |count| count && count > 0 }
  percentage = (covered_lines.to_f / total_lines * 100).round(2)
  
  puts "#{file}: #{percentage}% (#{covered_lines}/#{total_lines})"
end

Practical Examples

Example 1: Improving Line Coverage

A service class with conditional logic shows low coverage. Tests verify happy paths but miss error conditions.

# app/services/order_fulfillment.rb
class OrderFulfillment
  def fulfill(order)
    return { success: false, error: 'Invalid order' } unless order.valid?
    
    inventory_check = check_inventory(order)
    return { success: false, error: 'Insufficient inventory' } unless inventory_check
    
    payment_result = process_payment(order)
    return { success: false, error: payment_result[:error] } unless payment_result[:success]
    
    shipment = create_shipment(order)
    return { success: false, error: 'Shipment failed' } if shipment.nil?
    
    { success: true, shipment_id: shipment.id }
  end
  
  private
  
  def check_inventory(order)
    order.items.all? { |item| item.quantity_available >= item.quantity }
  end
  
  def process_payment(order)
    # Payment processing logic
  end
  
  def create_shipment(order)
    # Shipment creation logic
  end
end

Initial tests achieve 60% coverage, testing only successful fulfillment:

# test/services/order_fulfillment_test.rb
require 'test_helper'

class OrderFulfillmentTest < Minitest::Test
  def test_successful_fulfillment
    order = create_valid_order_with_inventory
    result = OrderFulfillment.new.fulfill(order)
    
    assert result[:success]
    assert result[:shipment_id]
  end
end

# Coverage report shows untested lines:
# - Invalid order path (line 3)
# - Insufficient inventory path (line 6)
# - Payment failure path (line 9)
# - Shipment failure path (line 12)

Adding tests for error conditions raises coverage to 95%:

class OrderFulfillmentTest < Minitest::Test
  def test_successful_fulfillment
    order = create_valid_order_with_inventory
    result = OrderFulfillment.new.fulfill(order)
    
    assert result[:success]
    assert result[:shipment_id]
  end
  
  def test_invalid_order
    order = create_invalid_order
    result = OrderFulfillment.new.fulfill(order)
    
    refute result[:success]
    assert_equal 'Invalid order', result[:error]
  end
  
  def test_insufficient_inventory
    order = create_order_exceeding_inventory
    result = OrderFulfillment.new.fulfill(order)
    
    refute result[:success]
    assert_equal 'Insufficient inventory', result[:error]
  end
  
  def test_payment_failure
    order = create_order_with_failing_payment
    result = OrderFulfillment.new.fulfill(order)
    
    refute result[:success]
    assert_match /payment/i, result[:error]
  end
  
  def test_shipment_creation_failure
    order = create_order_with_shipment_failure
    result = OrderFulfillment.new.fulfill(order)
    
    refute result[:success]
    assert_equal 'Shipment failed', result[:error]
  end
end

Example 2: Branch Coverage Analysis

A pricing calculator contains nested conditionals. Line coverage reports 100%, but branch coverage reveals untested paths.

class PricingCalculator
  def calculate_price(product, customer, quantity)
    base_price = product.base_price * quantity
    
    discount = if customer.premium?
      if quantity > 100
        0.25
      elsif quantity > 50
        0.20
      else
        0.15
      end
    elsif customer.standard?
      if quantity > 100
        0.15
      else
        0.10
      end
    else
      0
    end
    
    base_price * (1 - discount)
  end
end

Tests covering premium customers with high quantities:

def test_premium_customer_large_order
  product = Product.new(base_price: 10)
  customer = create_premium_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 150)
  assert_equal 1125.0, price  # 1500 * 0.75
end

# Line coverage: 100%
# Branch coverage: 33%
# Untested branches:
# - premium with 50-100 quantity
# - premium with <50 quantity
# - standard with >100 quantity
# - non-premium, non-standard customers

Full branch coverage requires testing all conditional combinations:

def test_premium_customer_medium_order
  product = Product.new(base_price: 10)
  customer = create_premium_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 75)
  assert_equal 600.0, price  # 750 * 0.80
end

def test_premium_customer_small_order
  product = Product.new(base_price: 10)
  customer = create_premium_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 25)
  assert_equal 212.5, price  # 250 * 0.85
end

def test_standard_customer_large_order
  product = Product.new(base_price: 10)
  customer = create_standard_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 150)
  assert_equal 1275.0, price  # 1500 * 0.85
end

def test_standard_customer_small_order
  product = Product.new(base_price: 10)
  customer = create_standard_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 25)
  assert_equal 225.0, price  # 250 * 0.90
end

def test_guest_customer
  product = Product.new(base_price: 10)
  customer = create_guest_customer
  
  price = PricingCalculator.new.calculate_price(product, customer, 50)
  assert_equal 500.0, price  # No discount
end

Example 3: Coverage-Driven Refactoring

Coverage analysis identifies complex methods with low testability. The coverage report shows high cyclomatic complexity correlates with low coverage.

# Original implementation - difficult to achieve full coverage
class ReportGenerator
  def generate(data, format, options = {})
    if format == 'pdf'
      if options[:landscape]
        generate_landscape_pdf(data, options)
      else
        if options[:include_charts]
          generate_pdf_with_charts(data, options)
        else
          generate_basic_pdf(data, options)
        end
      end
    elsif format == 'excel'
      if options[:multiple_sheets]
        generate_multi_sheet_excel(data, options)
      else
        generate_single_sheet_excel(data, options)
      end
    elsif format == 'csv'
      generate_csv(data, options)
    else
      raise ArgumentError, "Unsupported format: #{format}"
    end
  end
end

# Requires 8 tests for full branch coverage due to nested conditionals

Refactored implementation separates format handling, simplifying testing:

class ReportGenerator
  GENERATORS = {
    'pdf' => PDFGenerator,
    'excel' => ExcelGenerator,
    'csv' => CSVGenerator
  }.freeze
  
  def generate(data, format, options = {})
    generator_class = GENERATORS[format]
    raise ArgumentError, "Unsupported format: #{format}" unless generator_class
    
    generator_class.new(options).generate(data)
  end
end

class PDFGenerator
  def initialize(options)
    @options = options
  end
  
  def generate(data)
    return landscape_pdf(data) if @options[:landscape]
    return pdf_with_charts(data) if @options[:include_charts]
    basic_pdf(data)
  end
end

# Each generator tests independently
# Main class requires only 4 tests (3 formats + unsupported)
# PDFGenerator requires 3 tests (3 branches)
# Total: 7 tests vs 8, but with better isolation and clarity

Common Patterns

Test-First Coverage Pattern writes tests before implementation, achieving natural coverage. Each test exercises new code paths, preventing untested code from reaching the repository. Coverage tracking verifies completeness rather than driving test creation.

# Step 1: Write failing test
def test_applies_bulk_discount
  cart = ShoppingCart.new
  cart.add_item(product, quantity: 100)
  
  assert_equal 850.0, cart.total  # Expecting 15% discount on $1000
end

# Step 2: Implement minimal code to pass
class ShoppingCart
  def total
    subtotal = @items.sum { |item| item.price * item.quantity }
    subtotal >= 1000 ? subtotal * 0.85 : subtotal
  end
end

# Step 3: Coverage naturally follows implementation
# No uncovered lines because tests drove the code

Coverage Ratcheting prevents coverage regression. CI builds fail if coverage drops below the current level, allowing gradual improvement without backsliding. This pattern works for legacy codebases where reaching high coverage immediately proves impractical.

SimpleCov.start do
  # Store baseline coverage on first run
  # Fail builds if coverage decreases
  minimum_coverage line: 75, branch: 60
  
  # Allow coverage to fluctuate within 2% to handle statistical variance
  maximum_coverage_drop 2
  
  # Track coverage per file to identify regressions
  minimum_coverage_by_file 60
end

Differential Coverage Pattern measures coverage for changed code only. Pull requests must maintain or improve coverage for modified files, focusing testing effort where code changes. This pattern balances test development cost with risk reduction.

# In CI configuration
if ENV['GITHUB_EVENT_NAME'] == 'pull_request'
  changed_files = `git diff --name-only origin/main`.split("\n")
  
  SimpleCov.start do
    track_files changed_files.join(',')
    minimum_coverage 95  # Stricter requirement for new code
  end
else
  SimpleCov.start do
    minimum_coverage 80  # Relaxed for entire codebase
  end
end

Coverage Segmentation separates coverage requirements by code type. Critical business logic requires higher coverage than configuration code or simple accessors. This pattern acknowledges different risk levels across the codebase.

SimpleCov.start do
  add_group 'Critical', 'app/services' do
    minimum_coverage 95
  end
  
  add_group 'Important', 'app/models' do
    minimum_coverage 85
  end
  
  add_group 'Standard', 'app/controllers' do
    minimum_coverage 75
  end
  
  # Views and helpers have lower requirements
  add_group 'Views', 'app/views' do
    minimum_coverage 60
  end
end

Exclusion Marker Pattern excludes specific lines from coverage calculations. Generated code, defensive programming checks, and rarely-executed edge cases may warrant exclusion. SimpleCov recognizes special comments marking excluded code.

class DataProcessor
  def process(data)
    validate_input(data)
    
    # :nocov:
    # This defensive check should never execute in production
    # Included for safety during major refactoring
    raise 'Unexpected nil data' if data.nil?
    # :nocov:
    
    transform_data(data)
  end
end

Mutation Testing Integration validates test quality beyond coverage. Tests with high coverage but low mutation kill rates indicate assertion gaps. This pattern uses coverage as a necessary but insufficient quality measure.

# First ensure line coverage >= 90%
SimpleCov.start do
  minimum_coverage 90
end

# Then verify test quality with mutation testing
# mutant --include lib --require myapp --use rspec 'MyApp::Calculator'
# Mutation score should be >= 80% for critical components

Common Pitfalls

Coverage Target Obsession prioritizes percentage metrics over test quality. Developers write tests that execute code without verifying behavior, achieving high coverage with low value. A test calling a method and ignoring results contributes to coverage without catching bugs.

# Useless test - high coverage, zero value
def test_process_payment
  processor = PaymentProcessor.new
  processor.process(payment_data)  # No assertion
  # Test passes regardless of method behavior
end

# Valuable test - same coverage, actual verification
def test_process_payment_success
  processor = PaymentProcessor.new
  result = processor.process(valid_payment_data)
  
  assert result.success?
  assert_equal 'APPROVED', result.status
  assert_equal payment_data[:amount], result.amount_charged
end

Ignoring Branch Coverage achieves 100% line coverage while missing conditional paths. Tests execute lines containing if statements without testing both branches. Line coverage alone provides false confidence.

def calculate_fee(amount, expedited)
  base = amount * 0.03
  base * 2 if expedited  # Returns nil when expedited is false
end

# Test achieves 100% line coverage
def test_calculate_fee
  result = calculate_fee(100, true)
  assert_equal 6.0, result
end

# But never tests expedited = false
# Bug: returns nil instead of base fee for standard processing

Testing Implementation Details achieves coverage by testing private methods directly. These tests couple to implementation, breaking during refactoring despite unchanged behavior. Coverage should measure tested behavior, not tested code.

class InventoryManager
  def restock_needed?(product)
    current_stock = fetch_current_stock(product)
    threshold = calculate_restock_threshold(product)
    current_stock < threshold
  end
  
  private
  
  def fetch_current_stock(product)
    # Implementation detail
  end
  
  def calculate_restock_threshold(product)
    # Implementation detail
  end
end

# Bad: Testing private methods
def test_fetch_current_stock
  manager = InventoryManager.new
  stock = manager.send(:fetch_current_stock, product)
  assert_equal 50, stock
end

# Good: Testing public interface
def test_restock_needed_when_below_threshold
  product = create_product_with_low_stock
  manager = InventoryManager.new
  
  assert manager.restock_needed?(product)
end

Coverage Inflation Through Noise includes irrelevant code in coverage calculations. Tests for configuration files, database migrations, or framework boilerplate inflate metrics without testing application logic. Filters should exclude non-business code.

# Including everything inflates coverage artificially
SimpleCov.start  # Default includes everything

# Proper filtering focuses coverage on application code
SimpleCov.start do
  add_filter '/test/'
  add_filter '/config/'
  add_filter '/db/migrate/'
  add_filter '/vendor/'
  add_filter 'schema.rb'
  add_filter 'application.rb'
end

Ignoring Integration Gaps achieves high unit test coverage while missing integration failures. Individual classes work correctly in isolation but fail when combined. Unit test coverage complements, not replaces, integration testing.

# Both classes have 100% unit test coverage
class OrderService
  def create_order(params)
    Order.new(params).save
  end
end

class InventoryService  
  def reserve_items(order_id)
    order = Order.find(order_id)  # Assumes order exists
    order.items.each { |item| reserve(item) }
  end
end

# Integration gap: OrderService and InventoryService never tested together
# Race condition if inventory reservation happens before order save completes
# High unit coverage masks integration bug

Chasing 100% Coverage invests disproportionate effort in edge cases with minimal risk. The last 5% to reach 100% coverage often requires testing error conditions, framework internals, or unreachable code. Cost-benefit analysis should guide coverage targets.

class FileProcessor
  def process(filename)
    file = File.open(filename)
    content = file.read
    file.close
    
    parse(content)
  rescue Errno::ENOENT
    # Achieving 100% coverage requires testing file system failures
    # Mocking File.open to raise exceptions tests error handling
    # But provides minimal value compared to testing parsing logic
    log_error("File not found: #{filename}")
    nil
  rescue Errno::EACCES
    log_error("Permission denied: #{filename}")
    nil
  rescue IOError
    log_error("IO error reading: #{filename}")
    nil
  end
end

# Tests for happy path: high value, easy to write
# Tests for each rescue clause: low value, complex mocking
# 80% coverage captures 95% of risk

Misinterpreting Coverage Drops treats coverage decreases as failures without understanding causes. Adding new code naturally lowers coverage percentage until tests follow. Absolute coverage changes matter more than percentages during active development.

# Before: 1000 lines, 900 covered = 90% coverage
# After adding feature: 1200 lines, 900 covered = 75% coverage
# Coverage "dropped" but same amount of code remains tested
# Need to add tests for 300 new lines to restore 90%

SimpleCov.start do
  # Better: Track absolute covered lines, not just percentage
  minimum_coverage 85
  refuse_coverage_drop  # Fails if covered line count decreases
end

Reference

Coverage Metrics

Metric Type Calculation Strength Limitation
Line Coverage executed_lines / total_lines Simple to understand and implement Misses untaken branches within lines
Branch Coverage executed_branches / total_branches Catches untested conditional paths Complex to calculate for nested logic
Method Coverage called_methods / total_methods Quick overview of tested surface Too coarse for detailed analysis
Statement Coverage executed_statements / total_statements Language-agnostic metric Similar to line coverage in practice
Path Coverage executed_paths / total_paths Comprehensive testing measure Exponential complexity in real code

SimpleCov Configuration Options

Option Purpose Example Value
minimum_coverage Set coverage threshold 90
maximum_coverage_drop Allow coverage variance 5
add_filter Exclude files from coverage '/test/'
add_group Organize coverage reports 'Models', 'app/models'
track_files Limit coverage to specific files '{app,lib}/**/*.rb'
merge_timeout Time window for merging results 3600
command_name Label for merged results 'RSpec'
formatter Report output format HTMLFormatter

Coverage Module Methods

Method Parameters Returns Purpose
Coverage.start options hash nil Begin coverage tracking
Coverage.result none hash Retrieve coverage data
Coverage.running? none boolean Check if tracking active
Coverage.peek_result none hash Get results without stopping
Coverage.resume none nil Continue after peek

Coverage Start Options

Option Key Type Default Effect
lines boolean false Track line execution counts
branches boolean false Track branch execution
methods boolean false Track method calls
oneshot_lines boolean false Track execution without counts
all boolean false Enable all coverage types

Common Coverage Targets by Component

Component Type Line Coverage Target Branch Coverage Target Rationale
Business Logic Services 90-95% 85-90% High risk, complex logic
Models 85-90% 80-85% Core data operations
Controllers 75-85% 70-80% Integration tested elsewhere
Helpers 80-90% 75-85% Pure functions, easy to test
Background Jobs 85-95% 80-90% Asynchronous, harder to debug
API Endpoints 85-95% 80-90% External interface contract
Utilities 90-95% 85-90% Reused across application
Configuration 60-70% 50-60% Low risk, simple logic

Coverage Report Interpretation

Indicator Meaning Action
Red lines in report Unexecuted code Add tests or remove dead code
Yellow lines in report Partially executed branches Test all conditional paths
Green lines with low count Rarely executed code Verify test scenarios are realistic
Entire files at 0% Completely untested modules Prioritize test development
Coverage decreasing over time Test development lagging Review testing practices
High coverage, many bugs Poor assertion quality Review test effectiveness

SimpleCov HTML Report Files

File Purpose Usage
coverage/index.html Main report entry point Open in browser for overview
coverage/assets/ CSS and JavaScript Automatic, supports report UI
coverage/.resultset.json Raw coverage data Programmatic access, CI tools
coverage/.last_run.json Previous run metadata Coverage change tracking

Exclusion Markers

Marker Scope Example
:nocov: Next line or block Exclude error handling
:nocov: block :nocov: Between markers Exclude debugging code
add_filter File or directory Exclude test helpers
skip_token Custom exclusion Project-specific needs