← Back to Upcase

factory_bot Ruby Gem Tutorial | Online Video Tutorial by thoughtbot


(Upcase ) #1

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

(rderik) #2

The companion video is just linked to the preview, not the full video.