Skip to content

Instantly share code, notes, and snippets.

@TiuTalk
Created September 3, 2026 18:01
Show Gist options
  • Select an option

  • Save TiuTalk/b63ebe19bb761733145da2532410aa3e to your computer and use it in GitHub Desktop.

Select an option

Save TiuTalk/b63ebe19bb761733145da2532410aa3e to your computer and use it in GitHub Desktop.
Rails self-coverage workflow: drive a spec directory to 100% line coverage of its source counterpart
# frozen_string_literal: true
# Loaded via `rspec --require ./bin/coverage_eager_load` during coverage runs so
# SimpleCov tracks every app/models file, even ones no spec exercises. Without
# this, unreferenced models never load and silently drop out of the report.
RSpec.configure do |config|
config.before(:suite) do
Dir[Rails.root.join('app/models/**/*.rb')].sort.each do |file|
require_dependency file
rescue StandardError, LoadError => e
warn "coverage_eager_load: skipped #{file}: #{e.class}: #{e.message}"
end
end
end
#!/usr/bin/env ruby
# frozen_string_literal: true
# Reports per-file line coverage for app/models from SimpleCov's resultset.
# Usage: parses coverage/.resultset.json (produced by a COVERAGE=1 spec run).
# ruby bin/model_coverage.rb # all app/models files, worst-first
# ruby bin/model_coverage.rb <path> # only files whose path includes <path>
require 'json'
require 'set'
root = File.expand_path('..', __dir__)
resultset = File.join(root, 'coverage', '.resultset.json')
filter = ARGV[0]
abort "No coverage/.resultset.json. Run a COVERAGE=1 spec/models run first." unless File.exist?(resultset)
data = JSON.parse(File.read(resultset))
merged = Hash.new { |h, k| h[k] = [] }
data.each_value do |suite|
suite.fetch('coverage', {}).each do |file, cov|
lines = cov.is_a?(Hash) ? cov['lines'] : cov
next unless lines
existing = merged[file]
lines.each_with_index do |hit, i|
next if hit.nil?
existing[i] = (existing[i] || 0) + hit
end
end
end
def skipped_lines(file)
return [] unless File.exist?(file)
skip = []
skipping = false
File.readlines(file).each_with_index do |line, i|
stripped = line.strip
if stripped =~ /\A#\s*(:nocov:|simplecov:disable)\b/
skipping = stripped.include?(':nocov:') ? !skipping : true
skip << (i + 1)
next
end
if stripped =~ /\A#\s*simplecov:enable\b/
skipping = false
skip << (i + 1)
next
end
skip << (i + 1) if skipping
end
skip
end
rows = []
total_relevant = 0
total_covered = 0
merged.each do |file, lines|
next unless file.include?('/app/models/')
rel = file.sub("#{root}/", '')
next if filter && !rel.include?(filter)
skip = skipped_lines(file).to_set
skip.each { |n| lines[n - 1] = nil }
relevant = lines.count { |h| !h.nil? }
covered = lines.count { |h| !h.nil? && h.positive? }
next if relevant.zero?
total_relevant += relevant
total_covered += covered
missed = lines.each_index.select { |i| !lines[i].nil? && lines[i].zero? }.map { |i| i + 1 }
rows << { file: rel, pct: covered * 100.0 / relevant, covered: covered, relevant: relevant, missed: missed }
end
rows.sort_by! { |r| [r[:pct], -r[:relevant]] }
rows.each do |r|
pct = format('%6.2f%%', r[:pct])
missed = r[:pct] < 100 ? " missed: #{r[:missed].first(25).join(',')}#{'...' if r[:missed].size > 25}" : ''
puts "#{pct} #{r[:covered]}/#{r[:relevant]} #{r[:file]}#{missed}"
end
overall = total_relevant.zero? ? 100.0 : total_covered * 100.0 / total_relevant
puts '-' * 60
puts format('OVERALL app/models: %.2f%% (%d/%d lines) files: %d', overall, total_covered, total_relevant, rows.size)
puts format('Files < 100%%: %d', rows.count { |r| r[:pct] < 100 })

Self-coverage workflow

A repeatable loop to drive a spec directory to 100% line coverage of its target source directory on its own — e.g. bundle exec rspec spec/models covering 100% of app/models without leaning on controller or service specs.

Specs only. Do not change production code to make a line reachable. If a line can only be covered by proving the code is broken, stop and report the bug.

The examples below use app/models / spec/models; the same steps apply to any pair (services, policies, workers, …) — point the tooling at the other directory.

Tooling

Two coverage-only helpers (not production code):

  • coverage_eager_load.rb — rspec --require hook. Force-loads every source file under the target dir in before(:suite) so SimpleCov tracks files no spec touched (otherwise unreferenced files silently drop out of the denominator).
  • model_coverage.rb — parses coverage/.resultset.json and prints per-file line coverage for the target dir, worst-first, plus overall and the missed line numbers. It honors # :nocov: and # simplecov:disable / # simplecov:enable so its numbers match SimpleCov's own report.

Both currently filter on app/models; change that path (and the eager-load glob) to target another directory.

The loop

  1. Measure the full baseline:
    COVERAGE=1 bundle exec rspec --require ./bin/coverage_eager_load spec/models
    ruby bin/model_coverage.rb            # worst-covered first
    
  2. Pick one candidate: the worst file, or a small one for a quick win.
  3. Read the source file, and its existing spec if present.
  4. Add or extend the spec. Structure: outer describe '#method', inner context per scenario. Target the missed lines (ruby bin/model_coverage.rb <file>).
  5. Re-measure that file to confirm 100%:
    COVERAGE=1 bundle exec rspec --require ./bin/coverage_eager_load spec/models/<name>_spec.rb
    ruby bin/model_coverage.rb <name>
    
  6. Commit (one file per commit). Repeat from step 2.

coverage/.resultset.json holds only the files from the last COVERAGE run (SimpleCov overwrites the same command name), so a per-file run makes other files look uncovered, and a file's create-path lines covered by a different file's spec look missed. Re-run the full baseline (step 1) before trusting a missed list or an overall number.

Rules

  • Line coverage only; branch coverage is out of scope.
  • Test real objects; stub only external dependencies (network, DB clients, cloud services). Stub a collaborator at its boundary rather than reimplementing it.
  • Genuinely test-unreachable lines (class-load code gated by env, methods that only run a live external call) get # simplecov:disable# simplecov:enable with a one-line reason, or a config-level add_filter for a whole file.
  • A runtime guard like return if Rails.env.test? is NOT unreachable: stub Rails.env.test? after creating records, then call the method.
  • If a spec can only pass by proving the code is broken, stop and report the bug with a failing example — do not change the source to hide it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment