A little while ago I came across this interesting blog post about making tests run faster. In a nutshell the author managed to speed up their unit tests considerably by preloading the fixture data into the database (via a Rake task) and then not using any of the fixture stuff that Rails gives you. There’s a hefty price to pay though: you can’t use the fixture accessors, so users(:bob) becomes User.find(1) and comments(:bobs_comment_to_alice) becomes Comment.find(12).
We’re in a similar situation: we have several reasonably large Rails apps and tests that were taking more and more time to run. Other people have tried completely mocking out the database altogether. This speeds things up massively, but we really don’t want to go there: our interactions with the database are an important part of our application and often non trivial: it is vital they be covered by our tests. The speedup described above sounded good, but we weren’t ready to get rid of the fixtures accessors as they really help with the readability and maintainability of the tests.
A New Hope
How much can we claw back without having to give up fixture accessors? It turns out the answer is a lot. Most of our tests use several models (via the associations for example), for example a lot of the tests involve a question in someway (it is after all our business). Rails already caches fixtures on a per testcase basis, but that still means fixtures are reloaded once for each foo_test.rb file. Like any sane person we use transactional tests and so the tests themselves never clobber test data: we should never have to reload anything. Surely we can do better.
Plugin Baby!
We thought about this a few months ago and initially came up with a crude solution: put all the fixtures in test_helper.rb and only have them loaded once. Effective, although it would nice not to have this requirement. With a little more work a cleverer solution presents itself: override Fixture.create_fixtures. We stash away fixtures when they are loaded and if the same fixture is loaded again we just retrieve it from out cache. Using the plugin is easy, just grab it from here and add require 'faster_fixtures' to your test_helper.rb
A Wrinkle in Time
There’s one extra little wrinkle. Because previously fixtures were reloaded once per testcase, the data in the test database was never more then a few seconds old. Now that the data is only loaded once per test run, it might easily be 2-3 minutes old by the time the last tests run. Depending on how your tests are written this might be a problem. Consider for example the following:
Some fixtures:
recent_post:
created_at: <%= 4.minutes.ago.to_s(:db)%>
comment_on_recent_post:
created_at: <%= 3.minutes.ago.to_s(:db)%>
Some code:
def find_recent_posts
Post.find :all, :conditions => ['created_at > ?', 5.minutes.ago]
end
and a test
def test_find_recent_posts
assert_equal [posts(:recent_post)], Post.find_recent_posts
end
Nothing very exciting. However the change to the way fixtures are loaded can cause this test to fail when all the tests to run: if it runs towards then end, then the test data may be over 1 minute old and so posts(:recent_post) will be over 5 minutes old. There are also issues of consistency: if the comments fixture is loaded more than a minute before the posts fixture then you’d have a comment created before the post it is commenting on!
Time Travel
So, fix all tests to remove this stuff? Ugly. Marty, you’re not thinking fourth dimensionally! All we need is a little time travel, and for that we can use mocha. Mocha is a nice little mocking and stubbing library (although I expect you could use a different one).
The faster_fixtures plugin defines the following method
def freeze_now(frozen_now)
Time.stubs(:now).returns(frozen_now)
end
Calling this method stops time (at least if you’re inside the Ruby interpreter). We also ensure this is called as part of the test setup.
60% of the time, works every time
Well your mileage may vary, but we’ve used it on all of our apps. On the biggest app it cut test run time by 35%, and for the next biggest it cut test time by just a shade over 50%. Other people have reported similar gains. Typically unit tests show bigger gains, but functional tests usually show an appreciable gain.
Update
As of 7662, faster fixtures (minus the time correcting stuff) has been committed to the rails trunk. I will probably retool the plugin so that it can offer time freezing if required.