How to deal with complex Factory Bot associations in RSpec tests

by Jason Swett,

What’s the best way to handle tests that involve complex setup data? You want to test object A but it needs database records for objects B, C and D first.

I’ll describe how I address this issue but first let me point out why complex setup data is a problem.

Why complex setup data is a problem

Complex setup data can be a problem for two reasons. First, a large amount of setup data can make for an Obscure Test, meaning the noise of the setup data drowns out the meaning of the test.

Second, complex setup data can be a problem if it results in duplication. Then, if you ever need to change the setup steps, you’ll have to make the same change in multiple places.

How to cut down on duplication and noise

Unfortunately, I find that it’s often not very possible to take a complex data setup and somehow make it simple. Often, the reality is that the world is complicated and so the code must be complicated too.

What we can do, though, is push the complexity down to an appropriate level of abstraction. The way I tend to do this in Factory Bot is to use traits and before/after hooks.

Below is an example of some “complex” test setup. It’s not actually all that complex, but unfortunately examples often have to be overly simplified. The concept still applies though.

describe 'account with appointment soon' do
  it 'behaves a certain way' do
    account_with_appointment_soon = create(:account)

    create(
      :appointment,
      customer: account.customer,
      starts_at: Time.zone.now + 1.day
    )

    # assertions go here
  end
end

What if instead of this relatively noisy setup, the setup could be as simple as the following?

describe 'account with appointment soon' do
  it 'behaves a certain way' do
    account_with_appointment_soon = create(:account, :with_appointment_soon)

    # assertions go here
  end
end

This can be achieved by adding a trait called with_appointment_soon to the :account factory definition. Here it is:

FactoryBot.define do
  factory :account do
    customer

    trait :with_appointment_soon do
      after(:create) do |account|
        create(
          :appointment,
          customer: account.customer,
          starts_at: Time.zone.now + 1.day
        )
      end
    end
  end
end

That’s all that’s needed. We haven’t made the complexity go away, but we have moved the complexity out of our test so that our test is more easily understandable.

Leave a Reply

Your email address will not be published. Required fields are marked *