Speeding up Ruby on Rails Tests and RSpec

There comes a time in the life of every Ruby on Rails project where you and your team will end up with long running tests. Rails tests can contain factories, fixtures and lots of setup procedures before tests are executed, and all of these can contribute to major slowness in the test runs.

I’ve seen four ways of dealing with slow running tests in Rails:

  1. start deleting tests (or mark them as skip-able): the drawback is your test coverage drops and you may end up with more bugs
  2. use more integration tests rather than unit tests: this exercises a lot of code paths so test coverage won’t drop too much but in the end most developers will write integration tests that exercise the “happy path”
  3. Run your tests in parallel
  4. Run only the tests that were most likely to be affected by recent code changes

The latter two methods are much better than the first two.

My favourite so far is #3 because it still runs all of the integration and unit tests that you have but makes use of the fact that you can offload the work of running tests to multiple machines. Everyone now has access to multiple machines through AWS (Amazon Web Services) or Microsoft Azure or RedHat’s cloud. You can run parallel tests on your local development machine.

Run Your Ruby on Rails Tests In Parallel (Rails 3, 4 and 5)

The idea is that each test file will be run in separate processes.

If you have 5 tests that have a duration 2 minutes, in sequential runs it will take 10 minutes to run the whole test suite. If you have 5 processes available for parallel runs, it will only take 2 minutes to run it.

That’s a huge difference and means you can make 30 test runs in an hour rather than 6 test runs.

Rails 6: Parallel Testing is Built-In

The latest version of the Ruby on Rails framework, Rails 6, has parallel testing built into its core. It uses threads or processes for parallel test runs. The way it works is that you re-open the ActiveSupport::TestCase and add just one method call to it:

class ActiveSupport::TestCase
  parallelize(workers: 2)
end

Additionally, you can override the number of parallel works by providing the environment variable PARALLEL_WORKERS when you run “rails test” like this:

PARALLEL_WORKERS=5 rails test

Splitting RSpec test files into multiple files

However, some of your Rails tests may take longer; within the spec you could have multiple test cases and contexts that are taking too long. For instance, one test context with a few tests could be taking 1 minute while the rest of the test cases in that file only take 10 seconds. At that point, you can split the long-running test context into multiple files.

Here’s an example of how that might look like:

# spec/my_controller_spec.rb
describe MyController do
  context 'Slow tests' do
    it 'runs slowly #1' do
      # ...
    end
    it 'runs slowly #2' do
      # ...
    end
  end

  context 'Faster part of the test suite' do
    it 'runs in 10 seconds or less' do
      # ...
    end
  end
end

And now here’s how we could split that controller rspec test file into multiple files.

# spec/my_controller_spec.rb
describe MyController do
  context 'Faster part of the test suite' do
    it 'runs in 10 seconds or less' do
      # ...
    end
  end
end

# spec/my_controller_slow_1_spec.rb
describe :MyControllerSlow1 do
  def self.described_class
    MyController
  end

  include_context 'my controller helpers'

  it 'runs slowly #1' do
    # ...
  end
end

# spec/my_controller_slow_2_spec.rb
describe :MyControllerSlow2 do
  def self.described_class
    MyController
  end

  include_context 'my controller helpers'

  it 'runs slowly #2' do
    # ...
  end
end

Most importantly, this works because we can override the class method described_class. This method is used by the rspec-rails extensions that make it easier to test Rails classes with RSpec.

In conclusion, by using shared contexts and helpers and by splitting files, you can optimize your Rails tests even further.

One more way to speed up Ruby on Rails tests

Above are ways to speed up tests today without doing too much work. The ways above can give you quick wins which result in faster test runs and a better developer feedback cycle and slightly happier developers.

However, there is one more way to speed up tests: setting higher quality standards. Treat your tests as you would your code and dive deep into what the testing frameworks offer and what optimizations they can give you to improve performance.

For instance, imagine you are using Rspec and Factory_bot and are trying to speed up a test suite with four test cases. Before each test case is run, there is setup that occurs. In this setup function we create a few data models stored in the database. None of the test cases modifies the data. The first potential optimization comes from Factory_bot; you can construct the data models in memory rather than storing them in the database. The second potential optimization is that you can refactor all four test cases into one test case and use Rspec’s custom expectation messages.

Now imagine you have hundreds or thousands of test suites. Saving a few seconds here and there is good, but applying these optimizations across the entire test suite is even better. Unfortunately, that can be difficult and time-consuming.

So, if it takes a lot of effort to fix test suites written in the past, and we’ve already used the tactics for quick wins, what’s left?

The answer is fixing the future. You can do this by having higher standards for all newly written test suites and optimizing them as soon as they are written.

In code reviews, review the test code as thoroughly as you would review the rest of the code.