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

Check in your gems Gemfile.lock

Advantages of checking in the Gemfile.lock when developing gems:

  • A known good state to fall back on and do incremental updates
  • Being able to instantly work on an old project
  • No random failures for every new contributor
  • No “Bundler could not find compatible versions for gem” errors when Gemfile changes and you have an old lock

If you want to test your project against multiple/newest versions of dependent gems, make travis delete the Gemfile.lock before bundling or even run it against lockfiles with older versions (see wwtd), this is a more reliable way of finding bugs then counting on random contributors to find them.

ruby-rails-jquery-release

Transparant redirect for jquery ajax requests in rails with status code 278

There is no perfect backwards compatible solution out there afaik, but this is getting me pretty close to where I want it to be.

  • instead of redirecting set a empty 278 response (made up status code) for xhr requests
  • tell jquery to redirect when receiving a 278 response
  • the success handlers are still executed / there is no way to stop them, but they can either just insert the blank reply or do a != '' check
// application.js
// when we get a fake redirect response, go there
// this will still execute success handlers, but hopefully the fail or are not ugly ...
$.ajaxSetup({
  statusCode: {
    278: function(_,_,response){
      // using turbolinks to not lose back functionality
      Turbolinks.visit(response.getResponseHeader('X-278-redirect'));
    }
  }
})
# some controller
redirect_to_with_xhr signup_path

# application_controller.rb
# ajax requests follow all redirects, so we have to improvise with a
# special code and header to not get placeholders replaced with full pages.
def redirect_to_with_xhr(path)
  if request.xhr?
    response.headers["X-278-redirect"] = path
    head 278
  else
    redirect_to path
  end
end

Print capistrano execution time per server

Are all servers slow or just one ?

Output
Example logging all times > 3 seconds.

executing 'ruby -e "r = rand(10); puts %{sleep #{r}}; sleep r"'
 ** [out :: dbadmin1] sleep 6
 ** [out :: app1] sleep 4
 ** [out :: work1] sleep 2
 ** [out :: mirror1] sleep 5
 ** Server dbadmin1 finished command in 4 seconds
 ** Server app1 finished command in 7 seconds

Code

module PerServerTime
  # set via Capistrano::Command.per_server_time_threshold = 111
  def self.included(base)
    class << base
      attr_accessor :per_server_time_threshold
    end
  end

  # whenever a command finishes on 1 server print how long it ran on this server
  def process_iteration
    return super if @channels.size < 2 # do not need per-server logging for 1 server

    @start_time ||= Time.now.to_f
    @closed_channels ||= []

    @channels.each do |channel|
      if channel[:closed] && !@closed_channels.include?(channel)
        @closed_channels < (self.class.per_server_time_threshold || 30)
          host = channel.connection.instance_variable_get(:@xserver).host
          logger.info("Server #{host} finished command in #{time} seconds")
        end
      end
    end

    super
  end
end

Capistrano::Command.send(:include, PerServerTime)

Omniauth / OAuth integration test with webmock

Took me a while to figure out all the stubs … maybe this is useful later :)
I’m using this to test our facebook / twitter signup flows.

def external_redirect(url)
  yield
rescue ActionController::RoutingError # goes to twitter.com/oauth/authenticate
  current_url.must_equal url
else
  raise "Missing external redirect"
end

it "signs up" do
  twitter_id = 12345
  visit "/"
  
  stub_request(:post, "https://api.twitter.com/oauth/request_token").
    to_return(body: "oauth_token=TOKEN&oauth_token_secret=SECRET&oauth_callback_confirmed=true")

  external_redirect "https://api.twitter.com/oauth/authenticate?x_auth_access_type=read&oauth_token=TOKEN" do
    click_link "Continue with Twitter"
  end

  # https://dev.twitter.com/oauth/reference/post/oauth/access_token
  stub_request(:post, "https://api.twitter.com/oauth/access_token").
    to_return(body: "oauth_token=TOKEN&oauth_token_secret=SECRET&user_id=#{twitter_id}&screen_name=twitterapi")

  # https://dev.twitter.com/rest/reference/get/account/verify_credentials
  stub_request(:get, "https://api.twitter.com/1.1/account/verify_credentials.json?include_entities=false&skip_status=true").
    to_return(body: %{{"profile_image_url_https": "image_normal.png"}})

  visit "/auth/twitter/callback"
end