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.