Write Faster Tests with a Factory Context

Posted January 13, 2014 by Taylor Fausak

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:

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?

[Originally posted to my blog.]


Taylor Fausak was born in California, but he got to Texas as soon as he could. He studied Computer Science at the University of Texas at Austin before entering the wild world of software development. After a brief stint at Cisco, he started his career at Famigo working on all aspects of web development. Then he swapped his Django experience for a chunky slice of Rails bacon and joined OrgSync in the fall of 2012. When he's not slinging code around, he likes riding bikes, playing Magic, and throwing frisbees.


comments powered by Disqus