Rails test objects up and running with FactoryBot
Over the next few days, I thought we could cover some essential gems that I like to use when testing Rails applications with either MiniTest or RSpec. I thought the best place to start this discussion would be talking about one can generate test data using a gem from thoughtbot that you'll likely find in most code bases.
Creating test data with FactoryBot (beep boop beep!)
factory_bot_rails might be the first gem I add to every Rails project. The factory pattern is an object-oriented approach for creating an interface for spinning up various objects, and that's exactly what factory_bot_rails
does by introducing a friendly DSL.
Simple example factory
Let's say we have an Article
model in our Rails CMS application. In such a codebase, we might build a factory like this to create data in our tests:
FactoryBot.define do
factory :article do
# default attribute values defined here
title { "Best article ever" }
description { "No really this is the best article I've ever read!" }
published { false }
end
end
Let's say we wanted to write a scope in our Article
model to return unpublished articles (or drafts).
class Article < ApplicationRecord
scope :drafts, -> { where(published: false) }
end
A test for this using factory_bot_rails
and MiniTest might look something like this:
test '.drafts should only return articles that are not published' do
draft = create(:article, published: false)
published_article = create(:article, published: true)
results = Article.drafts
assert_includes(results, draft)
assert_not_includes(results, published_article)
end
Extending our factory using traits
Right now our article
factory only creates one type of article by default. Let's add some trait
s to show how flexible FactoryBot
can be.
FactoryBot.define do
factory :article do
# default attribute values defined here
title { "Best article ever" }
description { "No really this is the best article I've ever read!" }
published { false }
trait :published do
published { true }
end
trait :draft do
published { false }
end
end
end
Traits overwrite default attribute values and are helpful when you find yourself overriding these attributes to the same values consistently. Let's try leveraging our new traits by updating our previous test.
test '.drafts should only return articles that are not published' do
draft = create(:article, :draft)
published_article = create(:article, :published)
results = Article.drafts
assert_includes(results, draft)
assert_not_includes(results, published_article)
end
We're making a trade-off here. We've introduced some convenience, but we've sacrificed some clarity. Reading this test in isolation, we can't really be sure what :published
or :draft
does; however, it can make writing tests easier while being slightly more difficult to grok what's going on.
You can even take it a step further by creating factories from traits. Let's say we've added the ability to mark an Article
as featured
so that it shows up in a prioritized location of our app.
FactoryBot.define do
factory :article do
# default attribute values defined here
title { "Best article ever" }
description { "No really this is the best article I've ever read!" }
published { false }
featured { false }
trait :published do
published { true }
end
trait :draft do
published { false }
end
trait :featured do
featured { false }
end
# Defining new factories here
factory :published_article, traits: %i[published]
factory :draft, traits: %i[draft]
factory :front_page_article, traits: %i[published featured]
end
end
With these traits and factories added to our original article factory, the following methods can be called:
create(:published_article)
create(:draft)
create(:front_page_article)
Use your best judgement and create the factories that read well to you, and remember the trade-off you're making.
Setting unique fields with sequences
Let's say we've received a new feature requirement that each article must have a new unique title. We'll be covering how to address that in a code base in a separate blog post in a couple of days, but let's talk about how we can ensure our factories create unique titles in our test.
One option is to specify unique titles for every test case.
test "do something" do
article = create(:article, title: "Test Article")
article2 = create(:article, title: "Test Article 2")
end
But a common scenario in testing is needing to create batches of objects (i.e. for index pages). To do this, FactoryBot
has a method called create_list
.
create_list(:article, 5)
If we create these five articles using the convenience of create_list
they would all end up with a title of our default value, "Best article ever"
. What a humble codebase we've found ourselves in.
FactoryBot's sequence
method can help us here by providing each record with a unique index value that can be used to guarantee unique attributes. Here's what it looks like in action:
FactoryBot.define do
factory :article do
sequence(:title) { |n| "Article #{n}" }
description { "No really this is the best article I've ever read!" }
published { false }
featured { false }
# traits and records abbeviated
end
end
Now create_list(:article, 5)
would create article with the titles "Article 0", "Article 1", "Article 2", and so on.
Tying records together
We forgot to tie articles to the user's creating them. Let's pretend we've added a user_id
column to our articles
table, and update our factory using an association.
Let's say the generated user factory that was created during our rails generate model
command looks something like this:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "test#{n}@example.com" }
password { "password" }
first_name { "John" }
last_name { "Dorian" }
end
end
We can automatically create user records when creating articles by using an association. If articles are tied to a user belongs_to :user
, then the following user
method will be available to us.
FactoryBot.define do
factory :article do
user
sequence(:title) { |n| "Article #{n}" }
description { "No really this is the best article I've ever read!" }
published { false }
featured { false }
# traits and records abbeviated
end
end
Now calling create(:article)
will create a User
record first and set the articles user_id
column. You can also create the user manually and pass it in.
user = create(:user)
article = create(:article, user: user)
Conclusion
This was a brief introduction into factory_bot_rails, but I hope you found it useful. There's a lot more to discuss here that I plan on covering in follow-up posts. For example:
- How can you build an object without storing it in the database?
- How does FactoryBot affect the time it takes to run your test suite?
- How much information should we set in our default attributes?
- How can we randomize these attributes with realistic data?
- How could we leverage our factories to create local data for working in development?
- How does FactoryBot handle polymorphic relationships?
- Why FactoryBot, why not fixtures?
We'll answer all of these in due time, but if you're interested in reading ahead the factory bot GitHub repo has some excellent documenation for getting started.