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:
- Include all data that impacts the expectation in the setup phase
- 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
were 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 too 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.