Rails spring on steroid: take spring test time from 6s to 0.5s

We run a large app and test times even with spring where around 6s, so I fixed that 🙂

  • Preload environment
  • Preload test_helper via forking_test_runner
  • Disable slow at_exit handler
  • Enable by line running via testrbl
  • Preload fixtures
  • Turn logging on by default
# config/environment.rb
Spring::Commands::Test.after_environment if ENV['SPRING_PRELOADED']


# config/spring.rb
if defined?(Spring)

  raise "Spring is out of date: gem install spring" if Gem::Version.new(Spring::VERSION) < Gem::Version.new("1.3.6")

  ENV['SPRING_LOG'] = 'log/spring.log' # turn logging mode on
  ENV['SPRING_PRELOADED'] = 'true'

  class Spring::Commands::Test
    def env(*)
      "test"
    end

    def call
      # running by line number ?
      if ARGV.first =~ /^(\S+):(\d+)$/
        file, line = $1, $2
        pattern = Testrbl.pattern_from_file(File.readlines(file), line)
        ARGV[0..0] = [file, "-n", "/#{pattern}/"]
      end

      # running with --changed flag ?
      if ARGV.delete("--changed")
        ARGV[0...0] = Testrbl.send(:changed_files)
      end

      # load all the tests
      ForkingTestRunner.send(:enable_test_autorun, 'lib/slug_ids') # require an innocent files since we handle require ourselves
      ARGV.each do |arg|
        break if arg.start_with?("-")
        require_test(File.expand_path(arg))
      end
    end

    def description
      <<-USAGE
Run a test.
                    test/unit/xxx_test.rb:123            # test by line number
                    test/unit                            # everything _test.rb in a folder (on 1.8 this would be test/unit/*)
                    xxx_test.rb yyy_test.rb              # multiple files
                    --changed                            # run changed tests
                    test/unit/xxx_test.rb -n "/hello/"   # test by name
      USAGE
    end

    # we need to do some initialization after everything is loaded
    # otherwise we end up with for example extra tests running because urls are not cleaned out
    def self.after_environment
      return unless Rails.env.test?

      require 'testrbl'

      # preload as much of the test environment as possible for fast test startup
      require "forking_test_runner"
      ForkingTestRunner.send(:disable_test_autorun)

      require_relative '../test/helpers/test_helper'
      if User.count.zero?
        puts "No fixture data loaded. preload using rake db:fixtures:load"
        puts "Falling back to slower fixture loading strategy..."
      else
        ForkingTestRunner.send(:preload_fixtures)
      end

      # make test process not hang 2+s after it is done with tests
      # minitest calls at_exit twice and we need to hook in after the second
      # this might lead to some tempfiles pollution or logs missing ... time will tell
      require 'minitest/unit'
      class << MiniTest::Unit
        def at_exit(&block)
          @at_exit_called ||= 0
          @at_exit_called += 1
          super

          if @at_exit_called == 2
            super do
              status = (($!.respond_to?(:status) && $!.status) || 1)
              exit! status
            end
          end
        end
      end
    end

    private

    def require_test(path)
      if File.directory?(path)
        Dir[File.join path, "**", "*_test.rb"].each { |f| require f }
      else
        require path
      end
    end
  end

  Spring.register_command "test", Spring::Commands::Test.new
end

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s