Version note: This 2019 workaround targets the Spring and SimpleCov versions listed below. It overrides SimpleCov internals; check the current SimpleCov documentation before copying it into a newer application.

Here’s how you can get accurate SimpleCov test coverage while using Spring to preload your Rails application.

If you’d like to read more about how I figured this out, I’ve written a longer blog post that goes into a lot more detail: How I Figured Out A Test Coverage Issue With SimpleCov And Spring.


The following code has been tested with these library versions:

  • spring: 2.0.2, 2.1.0
  • simplecov: 0.16.1, 0.17.1
  • rspec: 3.8.0, 3.9.0
  • rails: 5.2.3

Gemfile

Prevent spring from being automatically required:

The code below is also available in the original gist.

group :development, :test do
  gem 'spring-commands-rspec', require: false
  gem 'spring-watcher-listen', require: false
  gem 'spring', require: false
  # ...
end

config/spring.rb

Configure Spring and monkey-patch SimpleCov:

if ENV['RAILS_ENV'] == 'test'
  require 'simplecov'

  SimpleCov.class_eval do
    class << self
      attr_accessor :spring_preload_result

      # Returns the result for the current coverage run, merging it across test suites
      # from cache using SimpleCov::ResultMerger if use_merging is activated (default).
      # If Spring is used, we merge in coverage from the preloaded Rails app.
      def result
        return @result if result?

        # Collect our coverage result
        if running
          raw_result = add_not_loaded_files(Coverage.result)
          if SimpleCov.spring_preload_result
            raw_result = SimpleCov::RawCoverage.merge_results(
              SimpleCov.spring_preload_result,
              raw_result
            )
          end
          @result = SimpleCov::Result.new(raw_result)
        end

        # If we're using merging of results, store the current result
        # first (if there is one), then merge the results and return those
        if use_merging
          SimpleCov::ResultMerger.store_result(@result) if result?
          @result = SimpleCov::ResultMerger.merged_result
        end

        @result
      ensure
        self.running = false
      end
    end
  end

  SimpleCov.command_name 'RSpec'
  SimpleCov.start 'rails'

  # Prevent SimpleCov from writing test coverage after the Spring process exits
  SimpleCov.pid = nil
end

Spring.after_fork do
  next unless Rails.env.test?

  # Ensure that SimpleCov writes test coverage for this process
  SimpleCov.pid = Process.pid
end

config/environment.rb

Keep the existing application boot code and add the coverage capture after Rails.application.initialize!:

# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

if defined?(Spring) && defined?(SimpleCov) && Coverage.running?
  SimpleCov.spring_preload_result = Coverage.peek_result
end

spec/spec_helper.rb

Add this code to the top of spec/spec_helper.rb:

# If we are using Spring, then SimpleCov is started in config/spring.rb
unless defined?(Spring)
  require 'simplecov'
  SimpleCov.command_name 'RSpec'
  SimpleCov.start 'rails'
end

spec/rails_helper.rb

Add these lines to the top of spec/rails_helper.rb:

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)

# Eager load all files so that SimpleCov reports accurate coverage.
Rails.application.eager_load!

That’s it! Now you should see accurate test coverage whenever you run your RSpec tests with Spring.


Before:

After:


I am planning to open a SimpleCov issue to get some feedback about this approach, and it would be great if SimpleCov and Spring just worked together by default. In the meantime, you can use this code to get things working.

If you’d like to learn more about how I investigated this issue, you can check out this longer write-up that goes into a lot more detail: How I Figured Out A Test Coverage Issue With SimpleCov And Spring.