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 –
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
– Test helpers and their configuration, such
– Test reporters and accelators –
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
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.
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' # ...
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
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
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.
require_relative '../../app/models/order_delivery_date_validator' loads the source code to be tested through a relative path.
Build the Minimal
Now, we start subtracting what is common across multiple similar specs.
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
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
Less is More
Compared to the feature-rich typical
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.
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.
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.
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.