SimpleCov terminal summary of test coverage

  • ruby
  • simplecov
  • formatter
04 Jan 2024

I recently had an issue with SimpleCov test coverage of my RSpec tests in CI. The test coverage on my local machine was like expected but in CI, it was failing due to not meeting the coverage threshold. For that specific project I had no test coverage service like CodeCov set up yet, so I only saw a number that was lower than expected.

From the Javascript world, I’m used to the test coverage terminal report with Jest/Istanbul that looks like this:

Screenshot of Jest

If I had this for SimpleCov, it would be easy to track down what’s going on. SimpleCov provides a simple formatter for that: SimpleCov::Formatter::SimpleFormatter. With that, you could get a really simple output of the files that are taken into account and their coverage. The SimpleCov README gives an example of the usage:

# spec/spec_helper.rb
require 'simplecov'

SimpleCov.start 'rails' do
  formatter SimpleCov::Formatter::MultiFormatter.new([
    SimpleCov::Formatter::SimpleFormatter,
    SimpleCov::Formatter::HTMLFormatter
  ])
end

# ...

The problem here is, that the SimpleFormatter does not produce any output. This means you would need an adapted formatter, that uses the SimpleFormatter and takes its return value to puts it to the console:

# spec/spec_helper.rb
require 'simplecov'

class SimpleCov::Formatter::SimpleFormatterTerminal
  def format(result)
    puts SimpleCov::Formatter::SimpleFormatter.new.format(result)
  end
end

SimpleCov.start 'rails' do
  formatter SimpleCov::Formatter::MultiFormatter.new([
    SimpleCov::Formatter::SimpleFormatterTerminal,
    SimpleCov::Formatter::HTMLFormatter
  ])
end

# ...

Screenshot of SummaryFormatter

This already helped me in finding the problem: my SimpleCov setup was wrong. I was missing the rails part.

# I had this
SimpleCov.start do
# instead of this
SimpleCov.start 'rails' do

However, the output of the SimpleFormatter is not really easy to grasp. It’s not aligned, not colored and the full paths are not really needed. Hence, I looked for better options.

I only found a gem that hasn’t been updated in 5 years that might do the job: simplecov-summary. In the end, it’s just a single file, so I copied it over.

It only prints the SimpleCov groups though. As I already had copied it over to my project, I adapted it to also print the files and make some more changes. This is what came out of it:

Screenshot of improved SummaryFormatter

It’s not as nice as the output from Jest/Istanbul, but maybe I will adapt it even more in the future. I might also make a new gem out of it. But for now, this is the code (MIT licensed):

require "colorize"

##
# Terminal summary formatter for SimpleCov.
#
# Adapted from https://github.com/inossidabile/simplecov-summary/blob/master/lib/simplecov-summary.rb
class SimpleCov::Formatter::SummaryFormatter
  def initialize(output = nil)
    @output = output || $stdout
  end

  def format(result) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
    @output.puts "SimpleCov stats:"
    @output.puts "----------------"

    name_length = (result.groups.keys + ["Total"]).map { |x| x.length }.max

    result.groups.each do |name, files|
      percentage = files.covered_percent.round(2)

      @output.puts "  #{name.rjust(name_length)}: #{percentage.to_s.rjust(5)}%"
        .colorize(color: color(percentage), mode: :bold)

      files
        .sort_by { |f| f.covered_percent }
        .each do |file|
          percentage = file.covered_percent.round(2)
          filename = file.filename.gsub(%r{^#{Dir.pwd}/}, "")

          @output.puts "  #{
                         name.rjust(name_length).gsub(/./, " ")
          }  #{percentage.to_s.rjust(5)}% #{filename}".colorize(color(percentage))
        end

      @output.puts ""
    end

    percentage = result.covered_percent.round(2)
    @output.puts "  #{"Total".rjust(name_length)}: #{percentage.to_s.rjust(5)}%"
      .colorize(color: color(percentage), mode: :bold)
    @output.puts ""
  end

  def color(percent)
    case percent
    when 90..100
      :green
    when 80..90
      :yellow
    else
      :red
    end
  end
end