Your tests are slow? Put them in solitary confinement

As Rails matures and starts appearing in more and more organisations, I’m starting to notice a trend amongst the companies I work with. Maybe this might sound familiar…

They have an app that’s a few years old, it was an MVP that became a product. Originally it was built by 1 or 2 people but now it’s maintained by a larger team. Through perseverance and determination, the code is, in general, in a decent state, no-one’s saying that dreaded R-word (refactor).

Test coverage is also looking pretty good, it provides the developers with confidence no matter what part of the app they are working on.

This is all sounding pretty good, so what’s the problem?

Firstly, by now the app is a giant ball of code. What was a simple MVP, grew in complexity along with it’s success to become a feature laden monolithic beast.

Secondly, have you tried running the full test suite? It’s slow and not just in an inconvenient, ‘I have to wait a minute’ for the suite to run. But in a ‘I’ll go for lunch while the tests run and I’ll still be waiting when I get back’ way.

So, what should you do about it?

Well, I’ll deal with the second problem first, because a faster test suite will improve your life as a developer so much.

Isolate

One of the keys to fast tests is to only test what you need to test. If you don’t need to test a database interaction, then don’t. I see so many tests where the setup involves creating lots of objects in the database, purely because it’s ‘easier’ than isolating the test properly.

Stubs and mocks are your friends on this journey to a faster test suite. The great thing about this isolation approach is that it quickly reveals areas that are coupled together, usually if a test requires a lot of setup this is a good indication of coupling, which gives you a good place to begin improving your code.

There will be cases where you have to interact with the database, testing scopes or custom finder methods is a common example. In this context, hitting the db in these tests is fine. The important thing to remember is to keep a clear focus on what you are testing and isolate it from everything else.

 Next Level Isolation

Ok, so now you’ve got all your tests in isolation, no stray objects interfering with your object under test. Can we go faster?

Yes… it’s time for the ultimate in isolation. If your framework, eg. Rails, is not the subject under test then you should be isolating your tests from it.

There’s been some good stuff written about this already. See the talks by Gary Bernhardt and Corey Haines.

Basically, most of our tests don’t depend on our framework (in this case Rails), so we should treat it like any other dependency.

So, imagine we’re building a Cart and part of it’s responsibility is to calculate the total value of every line item it contains. This has nothing to do with Rails, it’s purely a case of performing a calculation on a collection of objects. So let’s test it like that.

How about some code…

# spec/spec_faster_helper.rb
$: << File.expand_path('../../app/models', __FILE__)

# Stub out ActiveRecord
module ActiveRecord
  class Base
    def self.has_many(*args); end
  end
end

# spec/models/cart_spec.rb
require 'spec_faster_helper'
require 'cart'

describe Cart do
  let(:cart) { Cart.new }

  describe '#total_value' do
    context 'with 2 line items' do
      let(:line_item) { double 'line_item', price: 1 }

      before { cart.stub(line_items: [line_item, line_item] }

      it 'calculates the total price of all the line items' do
        expect(cart.total_value).to eq 2
      end
    end
  end
end

# app/models/cart.rb
class Cart < ActiveRecord::Base
  has_many :line_items

  def total_value
    line_items.map(&:price).inject(&:+)
  end
end

Regardless of the speed benefits, this is good because it’s isolating the class under test from another dependency, Rails. The bonus is that this spec will run faster than it’s counterpart written using the default spec_helper provided by rspec-rails.

Summary

Hopefully this will help provide an actionable intro into speeding up your tests and making your development life a little easier.

Are you interested in writing faster tests? Sign up to my newsletter for bi-weekly tips on Faster Tests With Rails

* indicates required