Why Factories?

These days, most Rails projects use some form of factories in their test set up. What problem do they solve and why are they needed?

Too much info

Given a User model with first_name, last_name, and location fields, we could write a test like:

describe "#full_name" do
  it "combines first and last name" do
    user = User.new(
      first_name: "Joël",
      last_name: "Quenneville",
      location: "Boston"
    )

    expect(user.full_name).to eq "Joël Quenneville"
  end
end

This test has too much information in our test setup. The location is irrelevant to the output of the full_name method yet it’s hard to know that just reading the test. It looks like location has some impact on calculating the full name but we can’t know for sure without looking at the source.

Too little info

it "combines first and last name" do
  expect(the_test_user.full_name).to eq "Joël Quenneville"
end

This test has too little information in the setup. Why is the full name "Joël Quenneville"? Where does that come from? Is that one value or multiple combined together? There is no way of knowing here.

Just Right

it "combines first and last name" do
  user = User.new(first_name: "Joël", last_name: "Quenneville")

  expect(user.full_name).to eq "Joël Quenneville"
end

This has just the right amount of information. All of the data that influences the result is there in the setup. None of the irrelevant data is included.

Two rules of thumb when writing tests are:

  1. Include all data that impacts the expectation in the setup phase
  2. Include none of the data that does not impact the expectation

Required fields

It seems pretty easy to follow these two rules of thumb until you run into required fields. If User where an ActiveRecord model that looked like:

class User < ApplicationRecord
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :location, presence: true
end

Then this test

it "downcases the location on save" do
  user = User.new(location: "Boston")

  user.save

  expect(user.location).to eq "boston"
end

will fail with a validation error. Our validations require us to have irrelevant information.

We can try to avoid that with a helper function:

it "downcases the location on save" do
  user = build_user_with_defaults(location: "Boston")

  user.save

  expect(user.location).to eq "boston"
end

def build_user_with_defaults(overrides)
  defaults = {
    first_name: "Default",
    last_name: "Default",
    location: "Default"
  }

  User.new(defaults.merge(overrides))
end

Once more, our test contains all the relevant information in its setup but no more.

Factories

This idea of having a method to build an object while pre-filling defaults is inspired by the factory method pattern. Because of this, such methods are often referred to as “factories” in the testing world.

This approach to setting up test data is so helpful that many languages have libraries for writing test factories such as Ruby’s FactoryBot, Elixir’s Ex Machina, and Python’s factory_boy among others.

Using FactoryBot we might write:

factory :user do
  first_name "default"
  last_name "default"
  location "default"
end

and then write a test like:

it "downcases the location on save" do
  user = create(:user, location: "Boston")

  expect(user.location).to eq "boston"
end

Abusing factories

Just as with the regular constructor, it’s possible to abuse factories by cluttering up the test with irrelevant information.

it "downcases the location on save" do
  user = create(:user, location: "Boston", first_name: "Joël")

  expect(user.location).to eq "boston"
end

It’s also possible to rely on the default values, leading to mystery guests and possibly even tautological tests.

it "downcases the location on save" do
  user = create(:user)

  expect(user.location).to eq "default"
end

In many cases, you don’t even need to use a factory. If your object doesn’t have required attributes or you don’t need to satisfy Rail’s validations for a particular test, then you can get away with just setting the relevant properties directly.

Test factories exist to solve a specific problem. If your tests don’t suffer from that problem then don’t just blindly use a factory. Conversly, if you are using a factory, be wary of recreating the exact same problem that led to factories in the first place by adding to much data.

In all cases, keeping in mind why you use factories and what problems they solve will help you write tests that are easier to read and faster to run.