Faster Tests With factory_grabber

I use factories instead of fixtures when testing. If you haven’t already discovered factories, check out this Railscast: Factories not Fixtures.

Now that you’re up to speed: I often find when writing tests that I simply want any record. Here’s a quick example scenario from a controller spec:

describe CommentsController do

  describe "POST /posts/1/comments" do

    before do
      @post = Factory :post
      @comment_attributes = Factory.attributes_for(:comment, :post => @post)
    end

    def do_post
      post :create, :comment => @comment_attributes, :post_id => @post.id
    end

    it "should create a new comment for @post " do
      lambda { do_post }.should change {@post.comments.count}.by(1)
    end

    it "should redirect to @post" do
      do_post
      response.should redirect_to(post_path(@post))
    end

  end

end

In this example, we’re creating a new post record even although there may already be several posts in the database. Since this test is only asserting that a new comment has been created for @post it doesn’t really matter what the post’s specific attributes are. All we care about is that it’s a valid post we can create comments for.

Inserting new records to the database is usually slower than simply retrieving an existing record. As a result, constantly creating factories when we already have appropriate records means our tests are slower and more inefficient than they should be.

Factory Grabber

I recently published a gem which addresses this issue. To check it out, simply run:

sudo gem install git://github.com/bodacious/factory_grabber.git

Factory Grabber is intended to be used with Factory Girl by ThoughtBot.

To use factory grabber in your tests/specs simply call the number of records and the model name as a method on Grab like so:

# return 47 individual comment records
@comments = Grab.forty_seven_comments
# will return 9 user records each with last_name "Smith"
@smiths = Grab.nine_users(:last_name => "Smith")
# return one record
@user = Grab.a_user
@article = Grab.an_article
@post = Grab.one_post

If there are appropriate records already in the database, Grab finds them. If there are not appropriate records, Grab will create them using the factories you’ve already defined.

For a practical example:

describe "GET /posts/1" do
  integrate_views

  before do
    @post = Grab.one_post :title => "This is the post title", :body => "This is the post's body"
  end

  def do_get
    get :show, :id => @post
  end

  it "should show the post title" do
    do_get
    response.should include_text(/This is the post title/)
  end

  it "should show the post body" do
    do_get
    response.should include_text(/This the post's body/)
  end

end

# test pagination
describe "GET /posts?page=1" do
  integrate_views

  before do
    # ensures there are at least eleven Post records
    # if there are less than eleven, new posts are created
    # if there are eleven or more no posts are created
    Grab.eleven_posts
  end

  def do_get
    get :index, :page => 1
  end

  it should find the latest 10 posts do
    do_get
    assigns[:posts].should == Post.find(:all, :order => created_at DESC, :limit => 10)
  end

end

Here’s example of the performance boots you can achieve:

                                 user     system      total        real(secs)
Create 50 new factories      0.100000   0.200000   0.300000 (  6.354282)
Grab 50 separate factories   0.300000   0.000000   0.300000 (  0.310373)
Grab 50 factories at once    0.020000   0.000000   0.020000 (  0.011400)

In this case, grabbing 50 existing records is almost 20 times faster than creating 50 new factories!

Just make sure you turn off transactional fixtures in your test_helper.rb or spec_helper.rb files:

Spec::Runner.configure do |config|
  config.use_transactional_fixtures = false
  config.use_instantiated_fixtures  = false
  config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
end

This gem is still in it’s infancy, I’d welcome any feedback/suggestions.

Written by

Photo of Gavin Morrice
Gavin Morrice

Software engineer based in Scotland

Work with me

Need help with some code?

In my free time, I like to help people improve their code and skills on Codementor.
Contact me on Codementor