Write faster tests with a factory context

by Taylor Fausak on

At OrgSync, we test our Ruby code with RSpec and factory_girl. A while back, I noticed our tests were slowing down for no apparent reason. It turns out our factories were creating a bunch of duplicate objects behind the scenes. For example, the event factory creates all these objects:

  • event
    • organization
      • school
      • umbrella
        • school
        • group_type
          • school
      • group_type
        • school
    • event_category
      • organization
        • school
        • umbrella
          • school
          • group_type
            • school
        • group_type
          • school

That’s 18 things: 1 event, 1 event category, 2 organizations, 2 umbrellas, 4 group types, and 8 schools. Of those, only 6 are needed. The other 12 are unnecessary duplicates.

It’s possible to avoid this by specifying the associations, which is what I did. I got fed up with manually doing that all the time and made a shared context. It ended up being much faster and a lot easier to use.

Here’s how, using Ruby 2.1.0p0, rspec 2.14.1, and factory_girl 4.3.0.

Let’s get started by writing some simple classes. We’re going to model a Reddit-style site with users, posts, and votes. Posts are submitted by users and users cast votes on posts.

class User < ActiveRecord::Base
  has_many :posts
  has_many :votes
end

class Post < ActiveRecord::Base
  belongs_to :user
  has_many :votes
end

class Vote < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
end

Next we’re going to create factories for these classes. Just like the classes, they’re pretty simple.

factory :user

factory :post do
  user
end

factory :vote do
  post
  user
end

Now we can use those factories in some tests. This particular test doesn’t do much, but it does show that two posts will be created.

let(:post) { create(:post) }
let(:vote) { create(:vote) }

it do
  expect(vote.post).to_not eq(post)
end

Sometimes this is what you want, but usually it isn’t. To avoid creating extra objects, you need to specify all of the associations. That’s tedious and error-prone, especially as the number of objects increases.

So to keep from repeating yourself, put all the definitions in a shared context.

shared_context 'factories' do
  let(:user) do
    create(:user)
  end

  let(:post) do
    create(:post, user: user)
  end

  let(:vote) do
    create(:vote, post: post, user: user)
  end
end

Then you can include the context and just start talking about the objects you want. You don’t have to build anything, and fewer objects will be created behind the scenes.

For example, this test creates half as many objects as the last one.

include_context 'factories'

it do
  expect(vote.post).to eq(post)
end

Let me repeat that: In this contrived example with three simple models, using the context created half as many objects. In a real test with real models, that would result in a significant speedup.

But what if we wanted to use let! to eagerly load some objects? It looks like the factory context won’t let us do that. But it does — just talk about the objects that need to be loaded.

before do
  user
  post
  vote
end

That’s not great, though. It’s not immediately obvious why those statements are there. We can do better by adding a helper method to the context.

def preload(*factories)
  factories.each do |factory|
    send(factory)
  end
end

Now you can use preload when you want to eagerly load an object.

before do
  preload(:user, :post, :vote)
end

After switching to a factory context, writing tests got easier and running tests got faster. Plus we didn’t lose any expressiveness compared to the old way. What’s not to like?