This week, Chris is joined by thoughtbot's Development Director in Boston, Josh Clayton, to talk about factory_bot; a topic near and dear to Josh's heart, as he's been the maintainer of the project for over five years.
FactoryBot is one of thoughtbot's most popular open-source projects, and is one of the few gems that we consider a requirement for every project, new or old. Let's discover why.
What is It?
For any given test, we need some data about which to make an assertion after performing some operation. To help produce test data more easily, Rails comes with built-in support for fixtures, but time and time again we found that fixtures were tedious to write and led to testing antipatterns. factory_bot is intended to be a replacement for fixtures that makes it easier to produce test data while avoiding these antipatterns.
What exactly is it about fixtures that we're trying to move away from?
Fixtures are static data stored in a YAML file; you have to handwrite however many variants of objects that your tests will need, which can be tedious. Also, it moves some of the complexity of a test out of the test itself, and makes it harder to understand at a glance.
Also, fixtures are often all-or-nothing. If you have written several slightly different objects in your fixtures to support a few different tests, you have to load them all up every time; but not every test needs all of the objects.
In contrast, factory_bot allows you to generate valid one-off objects as needed, and you can dynamically send in data to override default attributes and create the variants that you need on a test-specific basis. It tries to strike the right balance between conciseness and explicitness.
How FactoryBot helps avoid mystery guests
Let's look at a specific example. Fixtures often lead to Mystery Guests, an antipattern where we are performing some assertions in our tests but it's not at all obvious at a glance what the data we're asserting against is:
# first_name_spec.rb test using fixture
require "rails_helper"
describe "User" do
describe "first_name" do
it "is the first part of the name" do
user = users(:johnq)
expect(user.first_name).to eq("John")
end
end
end
Looking at just the test alone, it's hard to know exactly what's going on. Are
we're testing capitalization? Is the attribute concerned called full_name
?
Who knows?
Once we look at the fixture, it becomes more clear:
# users.yml fixture
johnq:
name: John Q User
email: user@example.com
nancy:
name: Nancy User
email: other@example.com
It seems like we're expecting the first_name
method to split the name
attribute, but we could only know that after examining the fixture. In
contrast, using factory_bot, the test would look like this:
# first_name_spec.rb test using factory_bot
require "rails_helper"
describe "User" do
describe "first_name" do
it "is the first part of the name" do
user = build(:user, name: "John Q User")
expect(user.first_name).to eq("John")
end
end
end
This test is now a lot more clear at a glance, and that's a major goal of factory_bot. factory_bot provides the ideal optimization between hiding extraneous details from our test setup, and explicitly including the pertinent details within the spec setup.
Wiring it up
Let's play around with some of the basic factory_bot functionality in a console session.
Here is a simple factory definition:
FactoryBot.define do
factory :user do
name "John"
email { "#{name}@example.com" }
last_signed_in { 10.days.ago }
password "password"
github_username { "#{name}-github" }
end
end
We're specifying here that we want a user's default name to be "John", and how to use the name to construct the rest of the attributes. (We'll get into more depth on defining factories later in the episode.)
To play around with it, we'll spin up a console and do some setup:
(development) main:0> require "factory_bot"
(development) main:0> require "./factories"
(development) main:0> include FactoryBot::Syntax::Methods
The last line allows us to say things like build(:user)
and create(:user)
rather than FactoryBot.build(:user)
and FactoryBot.create(:user)
.
(This setup is not usually required while writing tests; it will be handled by the rails_helper file.)
Methods for building
build
Now we can quickly build a new filled out user with valid attributes with:
(development) main:0> build(:user, name: "James")
Notice that we can be explicit about the name, or any other attribute that is important for the test that we are writing at the moment, and the default will be overridden.
attributes_for
and attributes_for_pair
We can also easily generate a hash of attributes, which is often handy (for example, in controller spec when you need some data to post):
(development) main:0> attributes_for(:user, name: "James")
Similarly, for each of the methods in factory_bot, there is an _pair
and
_list
variant that will give you multiple objects:
(development) main:0> attributes_for_pair(:user, name: "James")
In this case, they are largely identical, since we don't have any dynamic or sequential data -- but we'll see soon how we can do that, and how it makes creating multiple variants of an object a breeze.
build_stubbed
One of our favorite methods is build_stubbed
:
(development) main:0> build_stubbed(:user)
This is a little different than build
, in that the object acts as though it
has been persisted to the database, without actually touching the database; and
it will even build_stubbed
objects for any associations that we define in the
factory as well. Again, there are also build_stubbed_pair
and
build_stubbed_list
methods.
Methods for defining
Now let's have a closer look at the methods we can use to define our factories.
Dependent attributes
We've already encountered the first nice feature: dependent attributes. This
allows us to, for example, have attributes like email
and github_username
which are dependent on name
:
factory :user do
name "John"
email { "#{name}@example.com" }
github_username { "#{name}-github" }
end
This behavior works even if we pass in a custom name, all of these attributes will be custom as well, accordingly.
Lazy evaluation
Another important thing to note is the lazy evaluation of these attributes; rather than using a value for the attribute, we pass a block which will be evaluated on demand. This is particularly important for time values:
# Good
last_signed_in { 10.days.ago } # lazily evaluated
password "password" # not lazily evaluated
If we had instead not used the block,
last_signed_in 10.days.ago # not fine
password "password" # fine
then that value would be initialized when the factories file is read, and static for all factories created.
Sequences
Another very commonly used feature is sequences, which makes it easy to create unique values for each attribute across multiple objects. Right now, our email attribute looks like this:
email { "#{name}@example.com" }
However, if we have two objects with the same name, their email addresses would also be the same. Often for fields that we validate to be unique (like email), this is not ideal.
Instead, we can use a sequence:
sequence(:email) { |n| "#{n}@example.com" }
The block variable n
will receive a value that the sequence
method
guarantees will be unique to each factory. Take a peek at Upcase's factories
file for lots more examples of sequences, including defining a standalone
sequence that can be used across all factories and have guaranteed uniqueness.
Associations
Many times, we are testing not just the behavior of objects in isolation, but objects along with associated objects. factory_bot can help us quickly populate these associations as well.
For example, in Upcase, an invitation must belong to a team; and factory_bot makes it trivial to build and associate a team when building an invitation.
One thing to note is that if you FactoryBot.create
the object, it will also
FactoryBot.create
the associated objects. If you FactoryBot.build
the
object, it will still FactoryBot.create
the associated objects. This
reasoning behind this has to do with satisfying validations.
If you really want to keep yourself isolated from the database, use
FactoryBot.build_stubbed
instead, which will also
FactoryBot.build_stubbed
associated objects.
Traits
One of the last features that we use a lot is traits, which are named additional arguments that you can pass when creating factory_bot objects. Again, to use Upcase as an example:
build_stubbed(:invitation, :accepted)
This will cause the :accepted
trait of invitations to be invoked, which
is a block of additional setup work:
trait :accepted do
recipient factory: :user
accepted_at { Time.now }
end
In the trait block, we are free to override or add additional behavior, so it's a nice way to dry up our actual specs and make them more explicit at the same time with a nicely named argument.
Vim plugin for navigating to factories
Despite factory_bot being better than fixtures at avoiding Mystery Guests, it can still be helpful from time to time to quickly navigate back and forth between specs and factories. Chris has (of course) created a Vim plugin to make this really easy. You can find all the details and install instructions on the Rfactory page on GitHub.
The big picture
That is a solid introduction to the most commonly used features of factory_bot. Now, let's talk about when we should and shouldn't be using it.
First of all, it's important to recognize that sometimes we simply don't need
all of the power that factory_bot gives us; you don't always need to be
persisting data to the database. While build_stubbed
can mitigate many of
these issues, unit tests, for example, can often be run as effectively and
much more quickly by simply instantiating the object directly. (Read more
about this in Josh's Giant Robots post.)
Multiple named factories for the same object
The whole point of factory_bot is to replace fixtures, and avoid hiding the complexity of test data in differently named fixtures. If you find yourself doing something similar, and having differently named factories for the same object, then you might want to reevaluate your approach or whether factories is a good choice.
You should have one factory per model, and the factory should set up a minimal viable object for you, allowing you to override whatever attributes are relevant to the test in question.
Use a single factories file
In the same vein, we recommend having a single factories file, although factory_bot doesn't require this. This is, again, to reinforce the idea that our factories should just be setting up simple and minimal baseline objects, and shouldn't contain a lot of complexity, and should thus fit into a single file. Upcase, for example, uses a single factories file.
Using FactoryBot to generate development data
Another place where we often want to quickly generate valid data is for priming
our development database. factory_bot can be used to great effect here, and in
fact thoughbot's Suspenders includes the boilerplate required to use it in the
dev:prime
rake task.
Note that there's a subtle difference between development data and seed data;
db/seeds.rb
should be reserved for static data that the application requires,
like the fifty US states, and be intended to run in production. This is not a
good use case for our factories, which are intended to evolve over time.
Conclusion
We hope that gives you a better sense of how to use factory_bot, and some situations when you should (and shouldn't) use it. For much more information, check out the very detailed FactoryBot Getting Started. We'll see you in the forums!
This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/factory-bot