Jake Worth

Jake Worth

Why Do I Have to Use the Factorybot Gem, Again?

Published: October 17, 2022 • Updated: December 30, 2022 3 min read

  • ruby
  • testing

The 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.

What are your thoughts on this? Let me know!


Join 100+ engineers who subscribe for advice, commentary, and technical deep-dives into the world of software.