FactoryBot Tips

Eric Anderson
3 min readApr 1, 2019

--

A few tips when creating factories with FactoryBot (formally FactoryGirl).

“Base” Factory

For most DB-backed models in your system, you will want to create a “base” factory. It is what you will generally use when you need an instance of a model but don’t generally care about the data (other than the data you supply).

The factory should be named after the object you are creating it for. If your object is named Order then your “base” factory should be named order. Your “base” factory should use the following rules:

  • Provide the minimum values necessary to be able to save a valid instance.
  • Optional fields or fields with default values are not generally provided.
  • belongs_to associations should be provided assuming they are required (the default in Rails)
  • If has_one, has_many or has_and_belongs_to_many records are required for your object to function you may need to populate those, but you should try to avoid your object having that sort of dependency.

Random Values

Anytime you need a value in a factory it is best to use a realistic but always changing value. For example, if I wanted to provide the age of a user I wouldn’t hard-code it to 18. I would use (1..110).to_a.sample. This would make it pick a random value between 1 and 110 each time I get an instance of that factory.

The reason for using changing values is to ensure your tests don’t happen to pass due to some chance value. Each time you run your test you are introducing a bit of randomness to ferret out issues that may depend on certain values.

If you need realistic values for common needs (company names, emails, etc) I suggest libraries like forgery or faker.

Locate Important Values in Tests

If the value in an object is important for making the test pass, it should be initialized in the test. This helps to prevent your factories from becoming brittle. You want to avoid the situation where changing a factory in trivial ways breaks tests.

For example, if you have a full_name method on your User object that you are testing which combines your first_name and last_name your tests should look like:

it 'returns first and last name combined' do
user = create :user, first_name: 'Jane', last_name: 'Doe'
expect( user.full_name ).to eq 'Jane Doe'
end

With the above, your tests will not fail regardless of how your factory is configured for first_name and last_name. While you could have fixed values of Jane and Doe in the factory and not have to provide them in your test, specifying the values in the test reduces the fragility of your test suite.

If creating an object with your desired attributes is too complicated to do inline, create a specialized factory but make sure the factory name indicates it has the state you want. For example your “base” factory for your Order object probably won’t have line items, but some tests need a complete order. A realistic complete order does have at least one line item as well as other info (payment, shipping, etc). In this case you probably want to make a factory called complete_order that gives you a realistic completed order.

By naming it completed_order the implied attributes and related records associated with that name do not make the factory fragile as the name becomes a contract between the test and the factory.

Avoid Factory Proliferation

It can be tempting to create a child factory for every possible type of object you might need. For example, you might initially think to have your “base” user factory choose a random valid permission level. But then create other factories for each role ( admin_user, normal_user, super_admin_user, etc.).

Instead, avoid creating a sub-factory for setting trivial attributes. If you can do create :user, role: 'admin' just as easy in your test do that instead. It might require a few extra characters but:

  • It keeps your factories more manageable
  • It makes it clear just by looking at the test what you are doing without having to go look up what admin_user actually means.

Even for a more complex factory type if you are only using it in the scope of one test file perhaps you can just place it in a RSpec let declaration.

--

--