Published on
← Blog——

Run RSpec specs in parallel (with the parallel_rspec gem)

Authors

In this article, I explain how to run your RSpec test suite in parallel — and bank a 2x-4x speedup — using the parallel_rspec gem

parallel_rspec is a great gem that makes this process easy — it handles cloning your database and running tests across N parallel workers. Further down I also benchmark the optimal number of workers and... turns out the defaults are pretty perfect 😅

Table of Contents

Overview

RSpec runs specs serially in a single CPU process. That is, one-by-one, using a single lonesome CPU core from your beefy dev machine. This is in contrast to Minitest + Rails, which natively supports parallel testing.

Given modern development machines typically have 8, 16, even 32+ powerful CPU cores, and plenty of RAM to boot, it would be great if we could actually make use of that hardware to run our specs faster.

We'd expect a sizeable speedup, and that's exactly what we see, if we use a gem like parallel_rspec. This gem makes it easy to run specs across N workers in parallel (default N=4), leading to a 2x-4x speedup (approx).

I've tried a few different parallel-test-runner gems and by far prefer parallel_rspec. Another popular option is parallel_tests, but I've always found it fiddly and annoying to deal with. In contrast, parallel_rspec has been rock-solid.

Speed comparison

Here's a speed comparison between vanilla rspec and parallel_rspec (using the default N=4 workers) running the AttendList test suite on my base M2 MacBook Air.

It contains 279 examples across a variety of spec types:

Vanilla RSpec

Terminal
❯ rspec
........................................................................
.................................................

Finished in 32.39 seconds 🐌

Parallel RSpec

Terminal
❯ bin/parallel:rspec
........................................................................
.................................................

Finished in 10 seconds 🤯

Running specs in parallel leads to a ~3.2x speedup, saving 22.39 seconds.

Getting started with parallel_rspec

Setting up the parallel_rspec gem is very straightforward (that's a big part of why I like this gem!)

Just add it to your Gemfile:

Gemfile
group :development, :test do
 gem 'parallel_rspec'
end

then install it with bundle.

The parallel_rspec gem runs each thread against its own database instance (to avoid deadlocks), so prepare the test databases & run your specs with:

Terminal
# 1. prepare test databases
bundle exec rake db:parallel:create db:parallel:prepare

# 2. run specs in parallel
bundle exec prspec spec/

That's it! Your specs should run substantially faster than just running rspec.

Keep reading to explore my benchmarks and handy bin/parallel: scripts.

By default, prspec will use 4 workers to run your tests.

You can tweak this by adjusting the $WORKERS environment variable. However, I've found that 4 workers is pretty perfect, so be careful when tweaking this. If you do adjust WORKERS, ensure you re-run the :create and :prepare commands.

Each prspec thread runs against a separate instance of your database (created when running db:parallel:create and migrated with db:parallel:prepare). It automatically handles creating and naming your additional database instances.

Fine-tuning the number of workers

I've benchmarked prspec using 1-8 workers. The (rough and only vaguely scientific!) data I've collected is below:

tldr; the default WORKERS value of 4 seems optimal, at least for my machine.

N workerstime
1 (rspec, no parallel)21.7 seconds
212.4 seconds
39.4 seconds
4*7.5 seconds
58.9 seconds
612.2 seconds
716.6 seconds
817.1 seconds

Here's that data plotted:

In my testing, the default WORKERS value of 4 was optimal and led to specs running the quickest.
In my testing, the default WORKERS value of 4 was optimal and led to specs running the quickest.

I've run similar benchmarks on more powerful machines and got comparable results. With a beefy enough computer, though (and/or fewer other programs running), you may find N=5 or even N=6 to be optimal.

However, for most people, N=4 will deliver speedup enough without the need to re-run these benchmarks.

Note: Keen observers will note that rspec ran significantly faster here than (21.7 seconds) compared to the previous section (32.39 seconds). I ran these benchmarks against an earlier test suite of AttendList with fewer specs. The speedup in this section is similar, though — around 2.9x

Handy bin/parallel: scripts

I've put together a couple of scripts to encapsulate the 2 key things you'll do with parallel_rspec:

These scripts are modelled after the same bin/ pattern as bin/dev

bin/parallel:prepare

A short wrapper script to create & prepare test databases for each parallel thread:

bin/parallel:prepare
#!/usr/bin/env sh

# Prepare copies of DB for parallel_rspec
# https://github.com/willbryant/parallel_rspec
#
bundle exec rake db:parallel:create db:parallel:prepare

bin/parallel:rspec

Wraps the actual spec running aspect of parallel_rspec. You call it like so:

Terminal
bin/parallel:rspec                # all specs
bin/parallel:rspec spec/models    # subset of specs
bin/parallel:rspec
#!/usr/bin/env sh

# Runs specs in parallel with parallel_rspec.
# By default runs in 4 groups (determined by WORKERS env)
# If no args are provided, run the whole suite; otherwise, run the given path(s).
#
# https://github.com/willbryant/parallel_rspec
#
set -e

if [ "$#" -eq 0 ]; then
 exec bundle exec prspec spec/       # run all specs if no path provided
else
 echo "Running parallel specs in $*..."  # prints "running parallel specs in spec/path/to/specs"
 exec bundle exec prspec "$@"
fi

Conclusion

I've been using the parallel_rspec gem a ton lately in my side projects, and it's worked so well that I've recently introduced it at my workplace too.

The speed gains from running specs in parallel are no-joke — ~3x in this article — and parallel_rspec so far has been rock-solid, without any of the troubles I had dealing with parallel_tests.