Remove Duplication with FactoryBot's Traits

Josh Clayton

FactoryBot’s traits are an outstanding way to DRY up your tests and factories by naming groups of attributes, callbacks, and associations in one concise area. Imagine defining factories but without the attributes backed by a specific object. Here’s a basic example of a factory with two traits:

FactoryBot.define do
  factory :todo_item do
    name { 'Pick up a gallon of milk' }

    trait :completed do
      complete { true }
    end

    trait :not_completed do
      complete { false }
    end
  end
end

This would allow you to declare a complete or incomplete todo item very easily:

create(:todo_item, :completed)
create(:todo_item, :not_completed)

Pretty handy, eh? The other way to go about this would be to have different factories altogether for complete and incomplete:

FactoryBot.define do
  factory :todo_item, aliases: [:incomplete_todo_item] do
    name { 'Pick up a gallon of milk' }
    complete { false }

    factory :complete_todo_item do
      complete { true }
    end
  end
end

This may work just fine, but the problem I have with this is that any other permutations now have to be duplicated across both paths (complete and incomplete). What happens when todos get comments?

FactoryBot.define do
  factory :todo_item, aliases: [:incomplete_todo_item] do
    name { 'Pick up a gallon of milk' }
    complete { false }

    factory :incomplete_todo_item_with_comments do
      after(:create) do |instance|
        create_list :comment, 2, todo_item: instance
      end
    end

    factory :complete_todo_item do
      complete { true }

      factory :complete_todo_item_with_comments do
        after(:create) do |instance|
          create_list :comment, 2, todo_item: instance
        end
      end
    end
  end
end

This introduces duplication of creating comments because both the incomplete and complete todo items support comment creation. The alternative keeps the components of being complete/incomplete and having comments separate.

FactoryBot.define do
  factory :todo_item do
    name { 'Pick up a gallon of milk' }

    trait :completed do
      complete { true }
    end

    trait :not_completed do
      complete { false }
    end

    trait :with_comments do
      after(:create) do |instance|
        create_list :comment, 2, todo_item: instance
      end
    end
  end
end

You can mix and match traits as you please:

create(:todo_item, :completed, :with_comments)
create(:todo_item)
create(:todo_item, :not_completed, name: 'Pick up a bag of sugar')

Traits make your factories much more flexible, allowing you to add groups of attributes where needed.

Additionally, you’re dealing with traits as concepts. This means that the mechanics of achieving some sort of desired state are encapsulated. If the logic of your application changed so that an item being complete was not a boolean but rather a timestamp (for when the item was done), it’d be as simple as changing the trait:

FactoryBot.define do
  factory :todo_item do
    name { 'Pick up a gallon of milk' }

    trait :completed do
      # complete { true }
      completed_at { Time.now }
    end

    trait :not_completed do
      # complete { false }
      completed_at { nil }
    end
  end
end

Traits are a great way to add individual pieces of state to a factory without having to implement a massive hierarchy. Go forth and use traits!


Disclaimer:

Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.