Improving Unit Test Iteration Speed with Rails, Spork, and Resque

{13 of 365} TechSmith Titanium SporksI’ve been working on a project using Rails lately, and one of the most challenging things about it has been trying to make the tests run efficiently. Unfortunately, there’s so much setup to get the Rails environment going that simple little unit tests take about 5 seconds to run. Of that 5 seconds, it takes about 4.9 seconds for the Rails environment, and 0.1 second for the test itself. So naturally we want to use tools like Spork that will let us load up much of the common setup, and then just fork() each test to make sure it runs in a “pristine” environment. However, this gets really tricky when you are trying to both maximize how much code is loaded pre-fork() and maximize your ability to iterate on the code while keeping Spork alive. We found it all too easy to get into a state where just starting up the Rails environment loaded all of our models and controllers, and many of their attendant support classes. So of course any time you changed one of those pre-loaded classes, the change would not be reflected the next time you ran a test – unless you killed and restarted Spork, which squandered any time benefit you might get from using it in the first place.

Our first attempted solution to this problem was to force a reload of all the files we cared about on each fork, in Spork.each_run:

Spork.each_run do
  # ...
  Dir[Rails.root.join("ALL/THE/CODE/*.rb")].each {|f| load f}
  # ...
end

This got us most of the way there, as changes to code in a function would be reloaded, and new functions would get picked up. However, it opened us up to a subtle trap. Because we were reloading the updated class on top of the existing class, it would not remove any functions that were already declared. The same goes for renaming a function. The new function was present, but any code using the old function name would still find the old code. Consequently, it was easy to forget to update callers, and we would not notice the issue until we stopped Spork and ran the tests again.

Eventually I decided to dig deeper and try to force the behavior we really wanted: loading the Rails environment – but not our code – in Spork.prefork. This was harder than I expected it to be. For starters, Rails was “eager loading” our models and controllers when it started up. That is, it was loading them up as part of initializing the environment. To fix this, I had to use the following code in our subclass of Rails::Application:

config.autoload_paths += config.eager_load_paths.dup
config.eager_load_paths = []

Ultimately, the only things I left in our Spork.prefork were loading the Rails environment and some of our external testing dependencies. We depend on Rails to automatically load everything else on use, with the exception of a few classes we load automatically on the first request to a controller. And of course that first request happens after the fork, so there is no need to reload those classes in Spork.each_run.

So far, so good. However, our application also uses a Resque worker for data processing in an independent process. The Resque worker also needs to load several classes to do its work, and it would be smart to do so once, and not each time a job request is sent. Luckily, much like Spork, Resque also supports a block of code to run before it forks off its jobs. So I simply made Resque load all the code it uses before the fork.

Resque.before_first_fork do
  # ...
  Dir[Rails.root.join("ALL/THE/CODE/*.rb")].each {|f| require f}
  create_persistent_objects
  # ...

Technically this means that you would have to restart Spork to change the code used by the Resque worker, but this is an exceedingly rare case for us. We tend to use the Resque worker only in our capybara acceptance tests, where the speed gains of Spork are barely noticeable. Sure, we could wait and not load those classes until they are used, but it would mean reloading them on every Resque job; that is, not just test but production and development as well. (Or it would mean more convoluted, environment-specific setup.)

If you’re able to wrangle it properly, Spork can really help you speed up your unit test iteration. I hope some of these techniques prove useful to you as well.

Conversation
  • Jonathan Lin says:

    Have been trying out all sorts of tricks to get Spork to eager load my models and controllers on each_run, and this hits the spot!

  • Comments are closed.