Fast tests are desirable, but oftentimes as the project evolves and the necessity of going fast kicks in and writing tests feels like an obstacle leading to less attention to quality, let alone efficiency, and speed.

Here is a curated list of strategies I’ve used in the past to drastically improve the speed of my RSpec test suite:

Do not include Rails in every test

If you are familiar with RSpec you are probably aware of these two files: spec_helper.rb and rails_helper.rb.

I’d like to walk you through what their purpose is and why are there two files instead of just one.

spec_helper

This should be RSpec’s lightweight configuration, allowing it to be incredibly fast while only including the least amount of dependencies and config to do the job.

My strategy is to always default to the spec_helper, and if need for additional dependencies (or Rails), then and only then switch to the rails_helper. This might not sound like a lot, but trust me it is. Imagine having 1000 test examples and before each of them including and executing a helper only required by one test. If you are into TDD, the individual test will load files and run in under a second (or even a tenth of a second), boosting your development productivity, especially if you use the Guard gem.

You want to have the spec_helper as lightweight as possible and be aggressive in leaving it that way.

rails_helper

Here go additional helpers, that usually are required only for a hand of your test suite, for instance stubbing a mailer to stop from doing HTTP requests, or Stripe additional configuration. Notice that only a small group of your tests should include the rails_helper, not your entire suite. This saved us a lot.

I want this chapter to allow you to add these new questions into your thought process when writing tests:

  • Do I need the entire Rails folder to test this?
  • What other dependencies do I need to be able to test this?

Analyze your factories

FactoryBot is a great tool which an easy DSL to work with, but sometimes it can be easy to overwhelm a factory definition with instructions that add up unnecessary seconds to the test. There is a great gem to profile your factories called test-prof.

This will show you a breakdown of the number of factory invocations per type, also the amount of time each factory takes to persist its data.

Using it you can analyze and tweak your factories to be more efficient and eliminate insignificant configurations.

$ FPROF=1 rspec spec/controllers

[TEST PROF INFO] Factories usage

 Total: 574
 Total top-level: 376
 Total time: 00:01.714 (out of 00:02.388)
 Total uniq factories: 18

   total   top-level     total time      time per call      top-level time               name

     130         130        0.5644s            0.0043s             0.5644s               user
     128         104        0.6530s            0.0051s             0.5746s              actor
      81          61        0.1450s            0.0018s             0.1121s          spectator
      76           2        0.0644s            0.0008s             0.0013s       premium_plan
      74           0        0.5573s            0.0075s             0.0000s            profile
      25          23        0.1174s            0.0047s             0.0952s       subscription
      20          16        0.2212s            0.0111s             0.1864s              bonus
     ...

One of the most expensive configurations for a factory is its associations.

FactoryBot.define do
  factory :user do
    sequence :email do |n|
      "user#{n}@example.net"
    end
    first_name { 'test' }

    # Careful with this one
    association :city

    # Same with this
    after(:create) do |u|
      u.create_profile!
    end
  end
end

Notice in this example once a user is created it automatically creates an associated city, which might not be needed for every test example. I know it’s much more convenient to only call create(:user) instead of create(:user, city: city), but this alone will save seconds.

There is also the after(:create) hook which creates a profile after each user is created, again something that might not be needed everywhere, so factories need to be carefully written mindful of performance.

Persistence is expensive

For convenience, we might rely on always persisting models, although many times we don’t need to save the record to the database. Instead of using FactoryBot.create, try and go the other way and use FactoryBot.build_stubbed as your default and only switch when needing persistence.

require 'spec_helper'

RSpec.describe UserSerializer do
  subject(:serializer) { described_class.new(user).serializable_hash }

  let(:user) { build_stubbed(:user) }

  it 'returns the correct attributes' do
    expect(serializer[:data]).to include(
      name: user.name,
      email: user.email
    )
  end
end

In this example, we want to test that a Serializer object works, but although we could create a user to test this, there is no persistence needed, so build_stubbed works just fine.

Profile your slowest tests

There is a config you can add to spec_helper:

  config.profile_examples = 3

This allows you to see your 3 slowest examples so you can identify their bottlenecks.

$ rspec spec/requests

Top 3 slowest examples (0.50012 seconds, 2.8% of total time):
  GET /api/actors when authenticated with valid params has the correct count
    0.20219 seconds ./spec/requests/actors_spec.rb:60
  GET /api/actors when authenticated with valid params returns a 200
    0.15047 seconds ./spec/requests/actors_spec.rb:48
  GET /api/actors when authenticated with valid params has the correct meta information
    0.14747 seconds ./spec/requests/actors_spec.rb:66

Stub external HTTP requests

Use a library like webmock and block external HTTP requests. For example, the next snippet would block any request that goes to Mandrill in order to send a transactional email. This alone can save seconds for each example because you won’t be waiting for any HTTP request.

# config/rails_helper.rb

config.before do
  stub_request(:any, /mandrillapp.com/)
    .to_return(status: 200, body: [{ '_id' => 'test' }].to_json)
end

Conclusion

Make sure you visualize your test suite from a high-level view and understand the heavy dependencies. Have a habit of profiling your test performance to minimize the bottlenecks.

Key takeaways:

  • Make the spec_helper as light as possible and start the test by including it.
  • Keep factories as minimal as possible.
  • Use build_stubbed when possible instead of create.
  • Profile your slowest tests.
  • Stub external HTTP requests.