Don’t use it.
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,
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
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
User#age, though, we don’t actually care about the database.
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.
Now, let’s imagine
User grows and ends up having a
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
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?
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.build doesn’t help much:
rspec spec/models/user_spec.rb .. Finished in 0.01963 seconds 2 examples, 0 failures
FactoryBot.build creates associations; so, every time we use
Factory Bot to build a
User, we’re still persisting a
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
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
avatars still get created - meaning valuable time spent processing images and
persisting likely unnecessary data.
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,
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.
Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.