Factory_bot: Is it possible to include a trait by default in a factory?

Created on 22 May 2013  路  3Comments  路  Source: thoughtbot/factory_bot

Consider this factory:

# Read about factories at http://github.com/thoughtbot/factory_girl

FactoryGirl.define do
  factory :follow_up do
    first_name      { Faker::Name.first_name }
    last_name       { Faker::Name.last_name }
    phone           { Faker::PhoneNumber.cell_phone.gsub(/[^\d]/, '').gsub(/^1/, '2')[0..9] }
    email           { Faker::Internet.email }
    email_preferred true
    consent         false

    trait :by_referral do
      association :hospital
      association :referral
      source { FollowUp::REFERRAL}
    end

    trait :by_provider do
      association :provider
      source { FollowUp::PROVIDER }
    end
  end
end

I want to either default to include the by_referral trait, or the by_provider trait. It shouldn't be valid if neither trait is included. Is this possible, or is there a better way to do this?

Most helpful comment

There's no direct way to shut off a factory or force a trait to be included, but what about something like this?

trait :follow_up_defaults do
  first_name {}
  last_name {}
  # ...
end

factory :follow_up_from_referral, class: FollowUp do
  follow_up_defaults
  hospital
  referral
  source { FollowUp::REFERRAL }
end

factory :follow_up_from_provider, class: FollowUp do
  follow_up_defaults
  provider
  source { FollowUp::PROVIDER }
end

The other way would be to include a trait in a factory by default:

factory :follow_up do
  first_name {}
  last_name {}
  # ...
  by_referral # <== defaults to by_referral

  trait :by_referral do
    # ...
  end

  trait :by_provider do
    # ...
  end

The potential issue here is that the association from the base trait will always be included, even if it technically shouldn't be. The first solution (while feeling a bit weird) ensures that a follow up always falls into one of the two categories.

Let me know if anything's not clear or if you come up with any more ideas for how to solve this!

All 3 comments

There's no direct way to shut off a factory or force a trait to be included, but what about something like this?

trait :follow_up_defaults do
  first_name {}
  last_name {}
  # ...
end

factory :follow_up_from_referral, class: FollowUp do
  follow_up_defaults
  hospital
  referral
  source { FollowUp::REFERRAL }
end

factory :follow_up_from_provider, class: FollowUp do
  follow_up_defaults
  provider
  source { FollowUp::PROVIDER }
end

The other way would be to include a trait in a factory by default:

factory :follow_up do
  first_name {}
  last_name {}
  # ...
  by_referral # <== defaults to by_referral

  trait :by_referral do
    # ...
  end

  trait :by_provider do
    # ...
  end

The potential issue here is that the association from the base trait will always be included, even if it technically shouldn't be. The first solution (while feeling a bit weird) ensures that a follow up always falls into one of the two categories.

Let me know if anything's not clear or if you come up with any more ideas for how to solve this!

Turns out I can chuck an if statement into a factory, so this works:

# Read about factories at http://github.com/thoughtbot/factory_girl

FactoryGirl.define do
  factory :follow_up do
    first_name      { Faker::Name.first_name }
    last_name       { Faker::Name.last_name }
    phone           { Faker::PhoneNumber.cell_phone.gsub(/[^\d]/, '').gsub(/^1/, '2')[0..9] }
    email           { Faker::Internet.email }
    email_preferred true
    consent         false

    if [1, 2].sample == 1
      by_referral
    else
      by_provider
    end

    trait :by_referral do
      association :hospital
      association :referral
      source { FollowUp::REFERRAL}
    end

    trait :by_provider do
      association :provider
      source { FollowUp::PROVIDER }
    end
  end
end

Thanks @joshuaclayton

In case anybody else comes across the same issue as me - you cannot have a default trait which has the same name as an existing factory. See https://github.com/alphagov/content-publisher/pull/1568#discussion_r362202799 for full context.

Instead of

image

trait :image do
  # etc
end

...I had to rename to:

for_image

trait :for_image do
  # etc
end

...as I already had an :image factory elsewhere in the repository.

This nuance is captured in the getting started guide: "Note that defining traits as implicit attributes will not work if you have a factory or sequence with the same name as the trait" - but is easy to miss if you are skimming.

Was this page helpful?
0 / 5 - 0 ratings