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.