A Lite Spec Helper for Faster Rails Tests

speed dial for a car or machine showing maximum power

The Root of Slow Tests in Rails

Running tests in Rails is usually slow and the slowness often originates from the very first line of most spec files – require 'spec_helper.rb'.

Take a typical spec_helper.rb for example (from gitlab):

A simple examination of this file reveals its heavy load duties:
– The whole Rails environment at L14
– Test frameworks and their configuration such as rspec and capybara
– Test helpers and their configuration, such email_spec, factorygirl and webmock
– Test reporters and accelators – simplecov, spork

That’s a lot of code to load, and it’s loaded regardless of test contexts. Acceptance tests load it, integration tests load it, unit tests load it as well. This one-for-all spec_helper.rb contributes to unnecessary load time when we need to run any single test.

Do We Really Need That Much?

In the process of Test Driven Development, the most frequent tests we need to run during development are unit tests. We only need to run integration tests and acceptance tests towards the end of a development cycle.

Further more, with good design patterns, most objects would be built upon POROs (Plain Old Ruby Object) such as a Value Object, Policy Object or Form Object. These objects are built upon the framework APIs and could be tested in isolation from the framework.

To test these POROs, we don’t need to load the whole Rails framework. We just need to pick the required components to launch the tests. This cures the root cause of slow tests.

Here is a working demo to illustrate this idea.

git clone https://github.com/yangchenyun/lite_spec_helper_demo

It contains the source and specs of a Rails model validator that inherits from ActiveModel::Validators.

The folder keeps the conventional Rails structure, but omits all code except two relevant files: the validator to be tested and the specs for the validator.

Screenshot 2013-12-17 09.17.50

Running rspec spec/models, you will find it blazingly runs three tests. This is the minimal requirements to launch a test.

$ rspec spec/
...

Finished in 0.02124 seconds
3 examples, 0 failures

Learn to Run Tests Without Rails

A quick glimpse over the this spec file will reveal several lines which usually don’t appear in such files:

# ...
require 'active_model'
require 'active_support/core_ext/object'
require_relative '../../app/models/order_delivery_date_validator'

# ...

The two requires in the above snippet are dependencies required to run the validator within the spec. This extra work is necessary because we are not loading Rails and all its magic under the hood.

Firstly, no components of Rails are loaded by default – no ActiveModel, no ActiveSupport, no ActionController, nor is any of our project code. Secondly, ActiveSupport::Autoload is not utilized here, so we cannot rely on Rails to guess object names and load the correct source file. The responsibility to load required files is ours now.

require 'active_model' loads in this file and makes ActiveModel::Validator available.

require 'active_support/core_ext' extends Object with blank? and Fixnum with days.

This manual work also brings another implicit benefit. The external APIs are exposed and provide the infomation about the context in which our object lives.

Finally, require_relative '../../app/models/order_delivery_date_validator' loads the source code to be tested through a relative path.

Build the Minimal spec_helper_lite.rb

Now, we start subtracting what is common across multiple similar specs.

The require_relative line is a good point to start. The ../.. is too verbose and makes it hard to maintain if we change folder structure. What if we could require the source code back in a more concise way such as require 'order_delivery_date_validator'?

This can be achieved by modifying the $:($LOAD_PATH) in Ruby. When we require 'something', Ruby will look in all the paths in $: and look for something.rb to load according to the ruby-doc:

If the filename does not resolve to an absolute path, it will be searched for in the directories listed in $LOAD_PATH ($:). If the filename has the extension “.rb”, it is loaded as a source file …

To enable this concise syntax, we simply add the app/models directory to $:. We can also add any directories that hold other source using this configuration.

# spec/spec_helper_lite.rb
# ...
$:.unshift File.expand_path '../../app/models', __FILE__

Now we can utilize this light helper in our specs. Instead of using require_relative with a long path with lots of .., we could just require 'order_delivery_date_validator'. Furthermore, we can always put commonly required files for most specs into this “lite” helper in the future.

Here is the updated repo with this newly created spec_helper_lite.rb.

Less is More

Compared to the feature-rich typical spec_helper.rb, our spec_helper_lite.rb is minimal. Instead of loading all the framework overhead, it loads only the test framework rspec and tweaks $: to ease the load of our project source code.

This spec_helper.rb supports unit testing well and is much faster than the original one. Also, it reveals the dependencies in the specs and detects over-complex objects, as well.

The full-functioning spec_helper.rb could still be loaded for integration tests and acceptance tests, but the spec_helper_lite.rb will make you happier in your frequently running unit tests.

Conclusion

From a minimal requirement to run unit tests against an object in Rails, we’ve created lite version of spec_helper.rb. It provides minimal utilities for file loading and reveals object dependencies, all while speeding up unit tests.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • benjamintanweihao

    Thanks for writing this Steven! The first code snippet is messed up though.