Fabulous FactoryBot - A deep dive

Posted on · 1 minute read

When you write automated tests you sooner or later need to worry about test data. Regardless of whether you write unit, integration or end to end tests, it’s data that drives test execution down a specific path. Picking the right data is only one of the challenges associated with that - constructing it is another.

Today I want to talk about what you can do to simplify creating your test data. As long as you are using Ruby, that is. Let’s talk about FactoryBot and how to properly use it.

A widespread tool to simplify the creation of test data are factory methods. So long as your constructed objects are not too complex everything is peachy.

# Easy
def create_project(name: 'name')
  Project.create(name: name) 
end

 # I'm exaggerating here, but... yikes
def create_project(name: 'name', description: 'description', user: nil) 
  if user == nil 
    user = User.create(name: 'User') 
  end
  project = Project.create(name: name, description: description)
  Membership.create(user: user, project: project) 
  project
end

As your models and your tests become more complex so do your factory methods. Often you will end up with a bunch of similar factories which only differ in details - not great from a maintenance perspective. If things get complicated enough you might even end up writing your own test data generator using the builder pattern - which is a good idea, but quite a bit of effort.

There’s a better option. FactoryBot allows you to create factories that are both easy to use and highly configurable. It can save you a lot of time, and still keep your tests expressive and maintainable. It’s really quite powerful.

But you know what they say: With great power comes great responsibility. While FactoryBot allows you to write readable and performant tests, it also allows you to screw up your tests spectacularly.

In this two-part blog series we’ll look at some features of FactoryBot and how to utilize them for great good. The first part focuses on highlighting particularly useful features while the second part looks at how to use them in a small Ruby on Rails project to solve some tricky test problems.

FactoryBot’s Features

FactoryBot allows you to define factories for the models described above using its own domain specific language. If you haven’t used it before I suggest you have a look at the excellent FactoryBot documentation and this cheat sheet which is also excellent. In fact, if you haven’t: Go ahead and skim through those, like now. I’ll wait. We’ll refer to features listed in the referenced articles in the following sections.

Done? Great.

Let’s look at some of FactoryBot’s features in detail.

Attributes

When you create a factory, you may set attributes on the underlying model. No need to be explicit, if a user has a name you can just set it when using the factory:

factory :user # Yep, that's enough!

puts create(:user, name: 'Name').name
# => "Name"

Not very exciting, you could just as well use User.create(name: 'Name'). Things start to get interesting when you need to construct valid models within some constraints. An attribute needs to always be present? FactoryBot got your back.

factory :user do 
  name { 'Name' }
end

puts create(:user).name
# => "Name"

Be careful with default attributes though. It’s tempting to just set default values on everything - don’t! Like ‘normal’ factories your factories should only know about data that is absolutely required to construct valid instances of your models. Everything else just makes your tests harder to reason about.

When you have to deal with more complex constraints you can always construct them in the block passed to the attribute. Say, users need a unique email address. A naive approach would be something like this:

factory :user do 
  name { 'Name' }
  email { "#{SecureRandom.hex}@email.com" }
end

Please don’t do this. Using random data in your regression tests only makes them flaky. FactoryBot offers a different possibility, namely sequences:

factory :user do 
  name { 'Name' }
  sequence(:email) { |i| "user#{i}@email.com" }
end
puts create(:user).email
# => "[email protected]"
puts create(:user).email
# => "[email protected]"

Another thing that is quite useful are transient attributes. Basically these are attributes that don’t occur in your model but should be available within your factories. The official documentation explains them pretty well. We can use transients and associations to pass values to related models, which brings us to our next topic…

Associations

Associations can be used to create models that your current model depends on. They are one of the most awesome and most dangerous features of FactoryBot. They can make your tests super easy to read and an absolute nightmare to debug at the same time.

To see why I say that, assume that you have users and plans, and each user belongs to a plan. You can construct valid user instances using factories like so:

factory :user do
  name { 'User' }
  plan
end

factory :plan do 
  name { 'Business' }
end

puts create(:user).plan.name
# => "Business"

You can deal with associations in the same way you would deal with attributes, which means its possible to pass existing instances to the factory.

plan = create(:plan, name: 'Pro' }
puts create(:user, plan: plan).plan.name
# => "Pro"

As you can see associations can be chained. This is both the best and worst part of FactoryBot. You can create deeply nested structures using a single line of code, which is extremely comfortable. But it can also be a nightmare to debug. After all, its not transparent when you look at a test that create(:user) also creates a plan. Now imagine you have a lot more models…

If you want, you can specify the factory that should be used in an association explictely. You can even pass values to that factory - including transients. This does require that your association is wrapped in a block:

factory :user do
  transient do 
    plan_name
  end
  name { 'User' }
  plan { association :plan, name: plan_name }
end

puts create(:user, plan_name: 'Custom Plan').plan.name
# => "Custom Plan"

Even this simple example is getting a bit contrived. Which is why I recommend - as with attributes - that you stick to just specifying the minimal associations required by your model in your factory. Don’t worry, we’ll go over strategies to create big object trees in the second part of this series ;)

One last thing: When browsing tutorials on FactoryBot you will often see people advocating something like this:

factory :user do
  plan { create(:plan) }
end

You should never ever do this! To understand why we’ll have to look at build strategies.

Build Strategies

So far we’ve used create(...) to construct model instances. This does what it says on the tin: It creates a record in the database. There are times when you may not want this, and for these times FactoryBot offers the build strategies build and build_stubbed.

Build will not persist the model that it is being called for and build_stubbed will return an instance with its attributes stubbed out. Using build_stubbed results in models that can not be persisted to the database.

This is probably not very surprising, but build and build_stubbed offer superior performance.

Benchmark.measure { 10.times { create(:user) } }.total
#=> 0.08614699999999997>
Benchmark.measure { 10.times { build(:user) } }.total
#=> 0.006616000000000011>
Benchmark.measure { 10.times { build_stubbed(:user) } }.total
#=> 0.014236999999999944>

Creating models with their attributes stubbed out is slower, but more robust as your models still behaves as if they were persisted.

puts build(:user).plan_id
#=> nil
puts build_stubbed(:user).plan_id
#=> 1003

Until quite recently build would use create to construct associated models - which meant that even if you used build in the above example it would still call the database to create plan. This is due to a change to FactoryBot.use_parent_strategy which now defaults to true. For more information have a look at this discussion.

Considering the results above: If you are on a recent version of FactoryBot you should probably use build or build_stubbed in the vast majority of your test cases. Looking at our example in the previous section it now becomes obvious why explictely using create(...) in your associations is a bad idea. If you riddle your tests with constructs like that your performance will go down the drain regardless of which build strategy you decide to use: Stuff will always be persisted to the database, and you don’t want that.

A test running in 50ms vs one running in 5ms doesn’t seem to make much of a difference when you look at one test case. When you have thousands of those it certainly will.

Subfactories & Traits

As your model become more complex so will your factories. FactoryBot allows you to structure and compose functionality using two mechanisms: Inheritance and Traits.

Inheritance does what you would expect. It allows you to derive factories from other factories in order to add specialized behaviour or add specific attributes.

factory :user do 
  name { 'Name' }

  factory :user_with_email do 
    email { '[email protected]' }
  end
end
puts create(:user_with_email).name
# => "Name"
puts create(:user_with_email).email
# => "[email protected]"
end

Traits work similarly. They allow you to bundle functionality together and add to a factory. If you use traits like advertised in the documentation it’s not immediately obvious what benefits they offer. After all, they do look pretty similar to sub factories.

factory :user do 
  name { 'Name' }

  trait :with_email do 
    email { '[email protected]' }
  end
end

puts create(:user, :with_email).email
# => "[email protected]"
end

You can however use traits outside of specific factories, and combine them freely - and that’s what makes them powerful.

trait :with_email do 
  email { '[email protected]' }
end

factory :user, traits: [:with_email] do 
  name { 'Name' }
end
puts create(:user).email
# => "[email protected]"

In the example above you see an independent trait added to a factory. Nothing keeps you from adding this trait to other factories as well. We were explicit in adding the trait to our factory here, and you may also use traits as implicit attributes, but I would advertise against that as it makes your factories harder to understand.

trait :with_email do 
  email { '[email protected]' }
end

factory :user do 
  with_email 
  name { 'Name' }
end
puts create(:user).email
# => "[email protected]"

The fact that you can add traits to any factory, together with the fact that you may define transient attributes on them makes them a very useful tool when it comes to creating more complex factories.

Hooks & Lists

Hooks allow you to execute logic after a model was stubbed, built or created. This means that they can be used to modify models after they were constructed. You could, for example, use hooks together with lists to fill has_many associations:

  factory :user do 
    name { 'Name' }

    after(:create) do |user, _|
      user.memberships = create_list(:membership, 5, user: user)
    end
  end
puts create(:user).memberships.count
# => 5

Hooks are very powerful but should be used with care. Unintended side effects can and will happen if you use them excessively ;)

Summary

In this post I described some of the more useful features of FactoryBot. Associations, Traits and Hooks are super helpful when it comes to building your test data.

In the next part of this series we’ll have a look at a small Rails application and learn how the features highlighted here can help us to solve some interesting challenges associated with testing it.