Speed Up Tests by Selectively Avoiding Factory Bot

Josh Clayton

I’ve talked about speeding up unit tests when using Factory Bot by relying on FactoryBot.build_stubbed, but there’s another surefire way to speed up your test suite with Factory Bot.

Don’t use it.

Most Unit Tests Don’t Need Persisted Data

There are plenty of times where data needs to exist in the database to accurately test an application; most acceptance tests will require some amount of data persisted (either via Factory Bot or by creating data driven through UI interactions). When unit-testing most methods, however, Factory Bot (and even persisting data to the database) is unnecessary.

Let’s start with a couple of tests around a method we’ll need to define, User#age:

describe User do
  describe "#age" do
    it "calculates age given birthdate" do
      user = generate_user_born_on 366.days.ago

      expect(user.age).to eq 1
    end

    it "calculates age correctly by rounding age down to the appropriate integer" do
      user = generate_user_born_on 360.days.ago

      expect(user.age).to eq 0
    end

    def generate_user_born_on(date)
      FactoryBot.create :user, birthdate: date
    end
  end
end

This seems like a harmless use of Factory Bot, and leads us to define User#age:

class User < ActiveRecord::Base
  def age
    ((Date.current - birthdate)/365.0).floor
  end
end

Running the specs:

rspec spec/models/user_spec.rb
..

Finished in 0.01199 seconds
2 examples, 0 failures

More than 100% Faster

Looking at User#age, though, we don’t actually care about the database. Let’s swap FactoryBot.create with User.new and re-run the spec.

rspec spec/models/user_spec.rb
..

Finished in 0.00489 seconds

Still a green suite, but more than 100% faster.

Associations Make a Test Suite Slower

Now, let’s imagine User grows and ends up having a Profile:

class User < ActiveRecord::Base
  has_one :profile

  def age
    ((Date.current - birthdate)/365.0).floor
  end
end

We update the factory, including the associated profile:

FactoryBot.define do
  factory :user do
    profile
  end

  factory :profile
end

Let’s re-run the spec using Factory Bot:

rspec spec/models/user_spec.rb
..

Finished in 0.02278 seconds
2 examples, 0 failures

Whoa, it’s now taking twice as long as it was before, but absolutely zero tests changed, only the factories.

Let’s run it again, but this time using User.new:

rspec spec/models/user_spec.rb
..

Finished in 0.00474 seconds
2 examples, 0 failures

Whew, back to a reasonable amount of time, and we’re still green. What’s going on here?

Persisting Data is Slow

FactoryBot.create creates two records in the database, a user and a profile. Persistence is slow, which we know, but because Factory Bot is arguably easy to write and use, it hides it well. Even changing from FactoryBot.create to FactoryBot.build doesn’t help much:

rspec spec/models/user_spec.rb
..

Finished in 0.01963 seconds
2 examples, 0 failures

That’s because FactoryBot.build creates associations; so, every time we use Factory Bot to build a User, we’re still persisting a Profile.

Writing to Disk Makes Things Worse

Sometimes, objects will write to disk during the object’s persistence lifecycle. A common example is processing a file attachment during an ActiveRecord callback through gems like Paperclip or Carrierwave, which may result in processing thousands of files unnecessarily. Imagine how much more slowly a test suite is because data is being created.

It’s incredibly difficult to identify these bottlenecks because of the differences between FactoryBot.build, FactoryBot.create, and how associations are handled. By remembering to use FactoryBot.build on an avatar factory, we may speed up some subset of tests, but if User has an avatar associated with it, even when calling FactoryBot.build(:user), avatars still get created - meaning valuable time spent processing images and persisting likely unnecessary data.

How to Fix Things

User#age is a great example because it’s quite clear that there’s no interaction with the database. Many methods on core domain objects will have methods like these, and I suggest avoiding Factory Bot entirely in these, if possible. Instead, instantiate the objects directly, with the correct data necessary to test the method. In the example above, User#age relies only on one point of data: birthdate. Since that’s the method being tested, there’s no need to instantiate a User with anything else. It provides clarity to yourself and other developers by explicitly defining the set of data it’s using for the test.

When testing an object and collaborators, consider doubles like fakes or stubs.

My general advice, though, is to avoid Factory Bot as much as is reasonably possible. Not because it’s bad or unreliable software (Factory Bot is very reliable; we’ve used it successfully since 2008), but because its inherent persistence mechanism is calling #save! on the object, which will always take longer than not persisting data.


Disclaimer:

Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.