Fast RSpec/Rails: tiered spec_helper.rb
— Paul Annesley, March 2012
Slow Rails startup time is the TDD killer.
paul@paulbookpro ~/project ⸩ time rspec spec/lib/method_hunting_delegator_spec.rb
..
Finished in 0.00078 seconds
2 examples, 0 failures
rspec spec/lib/method_hunting_delegator_spec.rb -f d 6.76s user 1.64s system 91% cpu 9.225 total
Holy crap, that’s 9 seconds of Rails startup, for 0.00078 seconds worth of
RSpec. And this class/test doesn’t even use Rails! We can do better.
The culprit? That require "spec_helper"
at the top of every spec file which
loads the entire of Rails:
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'
# etc ...
There’s a few ways to deal with this, each with their own pitfalls. After
trying many approaches, I’ve settled on a tiered RSpec initializer
(spec_helper.rb and friends) which I can choose when invoking RSpec.
All the spec files still require "spec_helper"
, but it looks more like this:
Which means we can select different initializers using the SPEC environment
variable. The following spec_helper_unit.rb
is perfect for the
method_hunting_delegator_spec which took 9 seconds earlier, because there’s no
dependencies on Rails.
The result?
paul@paulbookpro ~/project ⸩ time SPEC=unit rspec spec/lib/method_hunting_delegator_spec.rb
..
Finished in 0.00079 seconds
2 examples, 0 failures
SPEC=unit rspec spec/lib/method_hunting_delegator_spec.rb 0.81s user 0.08s system 99% cpu 0.890 total
Under a second (0.890) is much more like it, and we still get class autoloading
provided by ActiveSupport. I use this mode for just about everything except
subclasses of Rails components, and those I keep a slim as possible. Moving
logic into SOLID classes is something you’ll benefit from anyway, and
these faster tests provide extra incentive. This example was a spec for a
standalone class living in RAILS_ROOT/lib/
but I use it for all sorts of
classes under app/models/
, app/presenters/
, app/forms/
etc.
But this zero-Rails initializer doesn’t help with testing your ORM-subclasses
(we’ll begrudgingly call them “models”) which depend on ActiveRecord:
paul@paulbookpro ~/project ⸩ SPEC=unit rspec spec/models/book_spec.rb
/Users/paul/project/app/models/book.rb:4:in `<top (required)>': uninitialized constant ActiveRecord (NameError)
And having tasted sub-second tests, 12 seconds is clearly unacceptable:
paul@paulbookpro ~/project ⸩ time rspec spec/models/book_spec.rb
.................
Finished in 0.67016 seconds
17 examples, 0 failures
rspec spec/models/book_spec.rb 8.08s user 1.85s system 78% cpu 12.698 total
But if you write your classes carefully, they don’t need to depend on much
from Rails except ActiveRecord. So let’s write a spec_helper which loads &
configures ActiveRecord, plus a few other bits and pieces useful for testing
database-persisted models.
You’ll have to excuse the Devise hackery; it was the one component tightly
coupled into a model (User
), and like most Rails app, that particular model
is at the center of the whole relationship graph. Perhaps there’s a better
solution, but this got me fast model tests for all but the user_spec itself.
Lets run that model spec again, this time boosted by SPEC=model
:
paul@paulbookpro ~/project ⸩ time SPEC=model rspec spec/models/book_spec.rb
.................
Finished in 0.58512 seconds
17 examples, 0 failures
SPEC=model rspec spec/models/book_spec.rb 6.50s user 0.21s system 98% cpu 6.844 total
Note that your model classes can still depend on external gems, but they’ll
need to e.g. require "money"
at the top. I suspect this explicit declaration
of dependency isn’t a bad idea anyway.
Of course, there’s always going to be specs which depend on the whole stack,
such as acceptance tests. For those, here’s the default spec_helper_full.rb;
basically like the original spec_helper.rb:
God speed.