Why Do I Have to Use the Factorybot Gem, Again?
- 4 minutes read - 686 wordsThe FactoryBot gem, previously known as FactoryGirl, is ubiquitous in Ruby and Ruby on Rails testing. If you aren’t familiar with it, you might be wondering, what’s the point? Wouldn’t it be simpler to just build objects myself?
In this post, I’ll explain what factories are and why you want them.
An Example
Let’s say we’re unit testing a full_name
method on a User
class.
full_name
takes two pieces of data, first_name
and last_name
, and
combines them with a space. Let’s also assume that we’ve tried to ensure data
integrity by requiring the presence of these two fields.
# app/models/users.rb
class User < ApplicationRecord
validates :first_name, :last_name, presence: true
def full_name
"#{first_name} #{last_name}"
end
end
Here’s our unit test of this model:
# spec/models/user_spec.rb
RSpec.describe User do
specify "#full_name returns a full name" do
user = User.create!(first_name: "Jim", last_name: "Weirich")
expect(user.full_name).to eq("Jim Weirich")
end
end
🤷♂️Seems like we’re doing fine without a factory.
How Factories Help
Soon our user class grows. Now we require three more pieces of data on the user: email, last IP address, and birth date.
# app/models/users.rb
class User < ApplicationRecord
validates :first_name,
:last_name,
:email,
:last_ip,
:birth_date, presence: true
end
When we run our test after adding these fields, it fails, because our User
is
no longer valid. Why? We didn’t give it all the data that it now requires.
Let’s do that.
# spec/models/user_spec.rb
RSpec.describe User do
specify "#full_name returns a full name" do
user = User.create!(
first_name: "Jim",
last_name: "Weirich",
email: "jim@example.com",
last_ip: "192.158.1.38",
birth_date: Date.today
)
expect(user.full_name).to eq("Jim Weirich")
end
end
Now the test passes– but is it a good test? I’ll respond to that question with
another question: why did we include email
, last_ip
, and birth_date
in
the setup? Because the model requires them. What does that have to do with the
test? If we’re asserting about a last name, nothing. Which pieces of data make
full_name
equal “Jim Weirich”? We can speculate, but we can’t really know.
Also, we must now add this useless data to every single existing test that has created a user. That’s a lot of rework! And it’s going to reappear each time the model changes.
Adding behavior to the model means rewriting unrelated tests, and tests that explicitly list every required field aren’t very readable or maintainable.
Wouldn’t it be nice to just say “If I have a user with the first name ‘Jim’ and last name ‘Weirich’, then and the full name is ‘Jim Weirich’”, with no other setup? That’s what factories do.
Adding Factories
Let’s create a user factory with FactoryBot.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
first_name: "Jim"
last_name: "Weirich"
email: "jim@example.com"
last_ip: "192.158.1.38"
birth_date: Date.today
end
end
Here’s our new test. Look familiar?
# spec/models/user_spec.rb
RSpec.describe User do
specify "#full_name returns a full name" do
user = FactoryBot.create(first_name: "Jim", last_name: "Weirich")
expect(user.full_name).to eq("Jim Weirich")
end
end
Now, we don’t have to change this test when a validation is added, we only have
to give default data to the factory. Because this test is so terse, it strongly
implies which pieces of data, first_name
and last_name
, lead to the
output of “Jim Weirich.” It’s a better test– more robust and communicative.
One last point: since our factory creates this specific first name and last name pairing, shouldn’t we omit them in the test, too? No, we shouldn’t. That data tell us why the test produces “Jim Weirich”. Without it, we’re just guessing about what the method does. And hard-coding them in the tests makes it harder for somebody else to inadvertently break the test by editing the factory.
When I remember, I like to intentionally use different strings from those found
in the factory, such as a first_name
“Ryan” and last_name
“Bates”. This is
another way to prove that the behavior I’m asserting about is the actual
behavior, rather than something that happened to work because of how a factory
was configured.
Wrapping Up
I hope this post explained what a factory is and why we need them. They don’t make sense until your app is bigger than hobby sized. Once you need them, you really need them.