Blazingly Fast Tests Using Spork, Rails 3, and RSpec 2

2011-02-10

Update:

This article on Ruby Inside is a much better version of the information provided here and most importantly it shows how to use the latest release candidate of Spork, 0.9.0rc, for better Rails 3.0 support. The author also uses Watchr for continuous integration testing. I’ve now added the previously missing info for Autotest but the Ruby Inside article is an all around better article; you should check it out.

The problem

Even a small Rails app can take several seconds to complete its testing but you should note that the actual specs take only fractions of a second to run. The main reason for this lackadaisical performance is the Rails environment needs to reload every time the tests are run.

So, what can we do about this? How can we make a usable, fast system for real people in a real world development environment?

One of the easiest ways to speed things up is to use a distributed Ruby solution like Spork and as it happens RSpec has built in support for DRb! Yea!

Spork will handle loading the Rails environment and will keep it loaded so you won’t be left twiddling your thumbs every time you run your tests. This can be a huge time saver when you start doing continuous integration testing where your tests are run automatically every time you change a file.

At this point you’re probably thinking, “Great so I’ll just install Spork and follow the instructions, it’s easy right?” Well, yes and no. Spork is super easy to use but it doesn’t immediately support Rails 3 out of the box; you’ll need to make a few tweaks but luckily those tweaks are fairly simple.

Enough yapping, let’s get started…

Assumes the following:

Rails 3.0.x – Our application framework.
RSpec 2.x – Our test framework.
And Spork 0.8.4 (0.9.0 will make the extra tweaks unnecessary). – Our DRb service.

Also I’m using OS X 10.6.6 so the instructions are written from that perspective. You might not have to change a thing if you’re using some other flavor of *nix but you might have to change everything, who knows.

How long does it take now?

Just to see how much time we’re saving lets find out how long it takes to run our specs now. Well use the ‘time’ command in front of the rspec command like so:

$ time rspec spec/
.....

Finished in 0.11343 seconds
5 examples, 0 failures

real  0m5.304s
user  0m4.384s
sys   0m0.910s

If you look you’ll see that our specs only took about 0.11 seconds to run but the whole process took a whopping 5.3 seconds to complete; that’s a huge difference.

Install Spork

We’ll start off by installing the Spork gem. We need to add it to our Gemfile anyway so lets do that and then use Bundler to install the gem.

[your rails app folder]/Gemfile
source 'http://rubygems.org' 

gem 'rails', '3.0.4' 
gem 'sqlite3' 

group :development do 
  gem 'rspec-rails' 
end 

group :test do 
  gem 'rspec' 
  gem 'capybara' 
  gem 'spork' # <= Add this line 
end

Now we use Bundler to install the Spork gem.

$ bundle install

Configure Rails

With the Spork gem installed we need to tell our Rails app how to use it so lets run Spork’s bootstrap option:

$ spork --bootstrap

The first fix we’re going to add makes sure if we delete a method that change is tracked by Spork. Open up your application.rb file and add this block inside the Application class:

if Rails.env.test? 
  initializer :after => :initialize_dependency_mechanism do 
  ActiveSupport::Dependencies.mechanism = :load 
  end 
end

With that block added your application.rb file should look something like this:

[your rails app folder]/config/application.rb
require File.expand_path('../boot', __FILE__) 
require 'rails/all' 

Bundler.require(:default, Rails.env) if defined?(Bundler) 

module SampleApp 
  class Application < Rails::Application 
    config.encoding = "utf-8" 
    config.filter_parameters += [:password] 
    
    # Add this for Spork 
    if Rails.env.test? 
      initializer :after => :initialize_dependency_mechanism do 
        ActiveSupport::Dependencies.mechanism = :load 
      end 
    end 
  end 
end

Configure RSpec

If you take a look at your spec/spec_helper.rb file you’ll see two blocks added by Spork. The Spork.prefork block is run once while the appropriately named Spork.each_run is run every time your tests are run. It’s important to note that the code in each block is only loaded once, when you start Spork, so any changes to these blocks will require a restart of Spork.

ActiveSupport::Dependencies.clear

You’ll want to grab everything that has something to do with Rails and drop it into the Spork.prefork block. Anything that takes a long time to load and doesn’t need to be reloaded for every test can be put in the prefork block which is pretty much everything. If you’re not sure you can start by putting it in the prefork block and if it doesn’t work move it to the each_run block later.

You’ll also need to add this line to the end of the RSpec.configuration block:

Next you want to tell Spork to reload all the application files on every run so it catches any changes. I also like to have it load the routes.rb file so I don’t have to reload Spork every time I add a new route. When you’re first designing your app and adding routes every few minutes you’ll get really tired of reloading Spork.

With the changes your Spork.each_run block will look about like this:

Spork.each_run do
  load "#{Rails.root}/config/routes.rb" 
  Dir["#{Rails.root}/app/**/*.rb"].each { |f| load f } 
end

When you’ve got all the changes made to your spec_helper.rb file it should look a bit like this:

[your rails app folder]/spec/spec_helper.rb
require 'rubygems' 
require 'spork' 

Spork.prefork do 
  ENV["RAILS_ENV"] ||= 'test' 
  require File.expand_path("../../config/environment", __FILE__) 
  require 'rspec/rails' 
  
  Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 
  
  RSpec.configure do |config| 
    config.mock_with :rspec 
    config.fixture_path = "#{::Rails.root}/spec/fixtures" 
    config.use_transactional_fixtures = true 
    
    # Needed for Spork 
    ActiveSupport::Dependencies.clear 
  end 
end 

Spork.each_run do 
  load "#{Rails.root}/config/routes.rb" 
  Dir["#{Rails.root}/app/**/*.rb"].each { |f| load f } 
end

Our final configuration tells RSpec to use Spork by adding the —drb command line switch to the .rspec file.

[your rails app folder]/.rspec
--color
--drb

If you’re sure you want to use Spork for all the apps you use RSpec with you can add it to ~/.rspec instead but I’d hold off on that till you’re sure.

Start Using Spork!

We’re all done with the configuration the only thing left to do is start up Spork. You’ll want to run it in its own shell so open up a new terminal window and run the spork command from the root of your Rails app:

$ spork
Using RSpec
Loading Spork.prefork block...
Spork is ready and listening on 8989!

Run RSpec

Now every time you run RSpec for that app it will use Spork and your tests will be much faster:

$ time rspec spec/
.....

Finished in 0.12398 seconds
5 examples, 0 failures

real    0m0.981s
user    0m0.425s
sys 0m0.124s

See how much faster it is?! In this case it’s over 500% faster! Of course that kind of savings won’t always scale directly with the number of your tests since most of our time in this instance is taken up just by loading the Rails environment.

That’s pretty much it; you should only have to restart Spork if you make some major changes to your Rails app like adding new gems. Basically anything that would require you to restart your test server will also require you to restart Spork; like if you change any of the configuration files other than the routes.rb file.

Continuous Integration Testing

The whole point of this exercise is to make testing faster so what’s better than having immediate feedback from your tests? Part of continuous integration is running your tests every time you compile your code but since Ruby isn’t a compiled language we can just run our tests when we save a change to our source code files.

For this immediate gratification fix I’m partial to the solution RSpec originally had built-in, Autotest. It’s incredibly easy to use and to install. From a terminal window simply type:

$ gem install autotest
Successfully installed autotest-4.4.6
1 gem installed

I use the whole ZenTest suite but if you don’t need all of that you can just install autotest by itself:

$ gem install autotest-standalone

You can leave it at that and Autotest will poll for changes in any files but that’s not very efficient. Let’s install a system file hook for Autotest so it only has to work if there are changes made to our files.

If you’re using OS X you’ll want to install the autotest-fsevent gem:

$ gem install autotest-fsevent
Building native extensions.  This could take a while...

-------------------------------------------------------------------------------

In order to use autotest-fsevent, install either the comprehensive 
ZenTest gem or the lightweight autotest-standalone gem and then add the 
following line to your ~/.autotest file:

require 'autotest/fsevent'

For more information, feedback and bug submissions, please visit:

http://www.bitcetera.com/products/autotest-fsevent

If you like this gem, please consider to recommend me on Working with
Rails, thank you!

http://workingwithrails.com/recommendation/new/person/11706-sven-schwyn

-------------------------------------------------------------------------------

Successfully installed autotest-fsevent-0.2.4
1 gem installed

Do as the instructions say and add the following line to your ~/.autotest file:

require 'autotest/fsevent'

If you’re using a Linux based system you can install the libinotify library and then the autotest-inotify gem instead. I haven’t tested either of these so can’t give you any help with them.

Growl Notices

As an added bonus you can also install autotest-growl and get Growl notices when your tests run; assuming you have Growl installed of course. This is incredibly useful since you don’t have to bother with your terminal window. You can just look at the growl notices and only need to mess with your terminal window if you need to know where your test is failing.

$ gem install autotest-growl
-------------------------------------------------------------------------------

In order to use autotest-growl, install either the comprehensive 
ZenTest gem or the lightweight autotest-standalone gem and then add the 
following line to your ~/.autotest file:

require 'autotest/growl'

Make sure the notification service installed on your computer:

http://growl.info (Growl for Mac OS X)
http://growlforwindows.com (Growl for Windows)
http://www.galago-project.org (libnotify for Linux)

If Growl notifications are not always displayed, take a look at the README
for assistance.

For more information, feedback and bug submissions, please visit:

http://www.bitcetera.com/products/autotest-growl

If you like this gem, please consider to recommend me on Working with
Rails, thank you!

http://workingwithrails.com/recommendation/new/person/11706-sven-schwyn

-------------------------------------------------------------------------------

Successfully installed autotest-growl-0.2.9
1 gem installed

Again you’ll need to follow the instructions and add a line to your ~/.autotest file:

require 'autotest/growl'

Drink Beer

That’s it. You now have super-fast tests that are automatically run every time you update a code file.

If you have any problems please drop me a line; this info was cobbled together from numerous sources with my own additions to make it a little more useful.