3 Practices to Apply to Speed Up Our Rails Test Suite
Tags: ruby, rails, testing
Published: 2020-01-29 13:52:05

When more features are added to our application, the time it takes to run our tests increases. The pain of this is more evident when we work in a team of fellow developers, pushing to the repository and triggering builds on the CI platform multiple times per day! 😭
But don't worry, there are 3 practices we can apply to speed up our test suite!

### 1. Replace `let!` variables with `let` variables
Why should we replace `let!` with `let`? How does using `let` help speed up our test suite? Let's first take a look at what `let!` does behind the scenes. 🕵️‍♂️  

This is how the `let!` method looks like [in rspec-core's codebase](https://github.com/rspec/rspec-core/blob/e4e10d977320743931b9f4644c37b765d69a5fd6/lib/rspec/core/memoized_helpers.rb#L374).
```ruby
def let!(name, &block)
  let(name, &block)
  before { __send__(name) }
end
```
As we can see, it basically calls `let` and `before`. But notice the block passed to `before`.
```ruby
{ __send__(name) }
```
This will invoke the method which is identified by the `name` parameter **before** each example is run. Let's move onto to an example!  

This service was written as dumb as possible for the sake of this example. 😁 The service returns books that were fetched from a fictional platform called _Bibliothek_.

```ruby
module Bibliothek
  class GetBooks
    def self.run
      new.run
    end

    def run
      [
        {
          title: 'And Then There Were None',
          author: 'Agatha Christie'
        },
        {
          title: 'Far from the Madding Crowd',
          author: 'Tom Hardy'
        },
        {
          title: 'This Was a Man',
          author: 'Jeffrey Archer'
        }
      ]
    end
  end
end
```

And we have this spec for the service above.
```ruby
describe Bibliothek::GetBooks, type: :service do
  describe '.run' do
    let!(:titles) do
      # An expensive process
      sleep(2)

      ['And Then There Were None', 'Far from the Madding Crowd', 'This Was a Man']
    end
    let(:authors) { ['Agatha Christie', 'Tom Hardy', 'Jeffrey Archer'] }

    subject { described_class.run }

    it 'has 3 book records' do
      expect(subject.length).to eq(3)
      expect(subject.pluck(:author)).to eq(authors)
      expect(subject.pluck(:title)).to eq(titles)
    end

    it { should be_a Array }
  end
end
```
Even though we're not explicitly invoking `titles`, the `titles` variable will be evaluated for **each** example thanks to the way `let!` was written.
The time it takes for the entire `.run` example group to run is 4 seconds on average!  

How can we enhance this?  
We convert the `titles` variable from a `let!` into a `let`.

```ruby
describe Bibliothek::GetBooks, type: :service do
  describe '.run' do
    let(:titles) do
      # An expensive process
      sleep(2)

      ['And Then There Were None', 'Far from the Madding Crowd', 'This Was a Man']
    end
    # other code
  end
end
```
Now, the time it takes is slashed by half! 🎉🎉🎉  
The key here is to remember that a `let` variable is only evaluated **when we explicitly invoke it**.

### 2. Write multiple expectations in one single example
Sometimes, it's unnecessary to write an example (`it`) for each expectation (`expect`, `should` etc.).  
Although it's cleaner (who doesn't love short one-liners? 😍),
it can affect the performance of our tests.
How?  

Say, we have a service that updates the availability of a book.
```ruby
module Books
  class UpdateAvailability
    attr_reader :book

    def initialize(book)
      @book = book
    end

    def self.run(book)
      new(book).run
    end

    def run
      # An expensive process
      sleep(5)

      book.update(available: !book.available)
      OpenStruct.new(success?: true, book: book)
    end
  end
end
```
And we have this spec.
```ruby
describe Books::UpdateAvailability, type: :service do
  describe '.run' do
    let(:book) do
      create(
        :book,
        title: 'And Then There Were None',
        available: true,
        author: create(:author, name: 'Agatha Christie')
      )
    end

    subject { described_class.run(book) }

    it { expect(subject.success?).to be true }
    it { expect(subject.book.available).to be false }
  end
end
```
The time it takes on average is a staggering 10 seconds! Why is this the case?  
The subject is executed for each example, in our case being 2 examples. This is unnecessary, and it makes no sense for us to separate the expectations when they're clearly related to the data returned by `described_class.run(book)`.  

What can we do? It's simple. We group all expectations in one example.
```ruby
describe Books::UpdateAvailability, type: :service do
  describe '.run' do
    let(:book) do
      create(
        :book,
        title: 'And Then There Were None',
        available: true,
        author: create(:author, name: 'Agatha Christie')
      )
    end

    subject { described_class.run(book) }

    it do
      expect(subject.success?).to be true
      expect(subject.book.available).to be false
    end
  end
end
```
This will reduce the time taken by half! 🎉🎉🎉

### 3. Use `build_stubbed` when persisted objects aren't required for the test
There are times when the tests we write do not require us to use persisted objects. And that's when we realise we should use `build_stubbed` instead of `build` or `create`.
`build_stubbed` will stub out methods relevant to persistence. In other words, you won't be able to:
1. Test the persistence of an object.
2. Test the callbacks that should execute after updating an object.

These are the methods excluded from the stubbed object returned by `build_stubbed`. ([retrieved via factory_bot's code base](https://github.com/thoughtbot/factory_bot/blob/5a3247dd8c77e8ae98c4d1586726e382b143406c/lib/factory_bot/strategy/stub.rb#L6))
```ruby
DISABLED_PERSISTENCE_METHODS = [
  :connection,
  :decrement!,
  :delete,
  :destroy!,
  :destroy,
  :increment!,
  :reload,
  :save!,
  :save,
  :toggle!,
  :touch,
  :update!,
  :update,
  :update_attribute,
  :update_attributes!,
  :update_attributes,
  :update_column,
  :update_columns,
].freeze
```

### Conclusion
These three simple practices allow our test suite to run faster, but the gains varies. If we've not applied the first two practices, then the more complex and larger the codebase is, the more the amount of gains we can extract. But don't expect any massive gains from using `build_stubbed`! 😝

If you want to take a look at the code, feel free to visit [the repository](https://github.com/thealiilman/bessere-leistung/).  
Cheers for reading and I hope you find this article helpful! 🤓
Please leave your comments below if you have any questions / thoughts or if you know of other practices we can apply to speed up our Rails test suite. ✌️