Developer Blog

Speed Up Your Rails Specs by 10x

One of the primary reasons people end up being lax in letting specifications drive the development of their Rails
applications is the time it takes to get feedback from running the suite of specifications.
A number of tools have been built to help alleviate this pain like Spork, Zeus, and Spring. In fact Rails 4.1 will now come with Spring standard.
Unfortunately, these tools are just crutches that tackle the symptoms of the problem rather than the problem itself. The
actual problem is writing tightly coupled code that expects to have the full Rails framework always present, which is slow to start up.

Developing Decoupled Code

The solution is to write code that is isolated and decouple your components from as much
of the system as possible. In other words, write SOLID Rails code.
As a specific example, one might typically directly use a model class to create an instance. Instead we can use dependency injection to remove hard coded references to classes. We just
need to make sure we safely reference the defaults using either block notation or a lazy
evaluating ||=. Below we have a service that needs to create Widgets which happen to
be ActiveRecord models. Instead of directly referencing the
Widget class, we use lazy evalutation in our chosen injection method. This allows us to
decouple our code and not need ActiveRecord loaded.

# A tightly coupled class. We don't want this.classMyServicedefcreate_widget(params)Widget.create(params)endend# We can inject in the initializerclassMyServiceattr_reader:widget_factorydefinitialize(dependencies={})@widget_factory=dependencies.fetch(:widget_factory){Widget}enddefcreate_widget(params)widget_factory.create(params)endend# Or we can explictly inject via a setter with a lazy readerclassMyServiceattr_writer:widget_factorydefwidget_factory@widget_factory||=Widgetenddefcreate_widget(params)widget_factory.create(params)endend# A specification injecting the dependency using the second methoddescribeMyServicedosubject(:service){MyService.new}let(:widget_factory){double'widget_factory',create:nil}before{service.widget_factory=widget_factory}it'creates a widget with the factory'doservice.create_widget({name:'sprocket'})expect(widget_factory).tohave_received(:create).with({name:'sprocket'})endend

A Base Rails-free Configuration

When writing your applications in this way you can then start to restructure how you setup your specifications and minimize the required environment to run both your specification and your code fulfilling the specification. The
typical spec_helper.rb will have a line like this:

1

requireFile.expand_path("../../config/environment",__FILE__)

This is what loads your entire Rails application and slows down the running of your tests.
To make your specifications faster, you need to use a configuration file that does not contain
this line. So let’s start by creating a very light weight base_spec_helper.rb:

We are requiring active_support and active_support/dependencies so we can have access to the autoloader Rails uses without actually loading up all of Rails. It is fairly light weight and the convienence outweighs the cost. In each spec helper which requires this base we will add the relevant portions of our app into the ActiveSupport::Dependencies.autoload_paths.

Plain Ruby Object Specifications

Depending on the part of application that you are specifying, you can create spec helpers
specific to what you need in any one context. For example, the simplest would be one for
specifying any type of pure Ruby class such as a service class.
A sample services_spec_helper.rb might be:

Model Specifications

Testing models needs a little bit more. Assuming you are using ActiveRecord you’ll need to include that as well as
establish a connection to your database. We won’t include factory_girl or database_cleaner as most of your tests
should not be actually creating database objects. In fact, the only place you really need to actually create an object in the database is when testing uniqueness validations. When you do need to create something you can just manually
clean it up or use a transaction. So a sample models_spec_helper.rb can look like this:

123456789101112

require'base_spec_helper'require'active_record'# RSpec has some nice matchers for models so we'll pull them inrequire'rspec/rails/extensions/active_record/base'Dir[File.join(RAILS_ROOT,"spec/support_models/**/*.rb")].each{|f|requiref}# Manually connect to the databaseActiveRecord::Base.establish_connection(YAML.load(File.read(RAILS_ROOT+'/config/database.yml'))['test'])ActiveSupport::Dependencies.autoload_paths<<"#{RAILS_ROOT}/app/models"

Feature Specifications

Finally, when creating feature specs, we do need our full Rails stack and our
feature_spec_helper.rb is going to look very similar to what your current
spec_helper.rb looks like.

Summary

I found myself using varitions on the above spec helpers in projects I work on
and decided I would write a set of generators to make it easier to bootstrap the
project. The gem can be found at https://github.com/Originate/rails_spec_harness

While introducing these changes into existing projects I have found speed increases of 8-12 times. The worst project experienced a 27x increase once these changes and the corresponding changes in coding habits where applied. As an example
I made a specification with 4 examples for a plain Ruby class. I then used the time command line utility to measure running rspec with the minimal spec helper as well as the full Rails spec helper and found the following: