Call trait from another trait with params in factory_bot
Asked Answered
S

2

25

I have a fairly chunky FactoryBot trait that accepts params and creates a has_many relation. I can call that trait as part of another trait to dry up traits or make it easier to bundles traits together when passing them to factories. What I don't know how to do is how to pass params to a trait when I'm calling it from another trait, or what to do instead.

e.g.

FactoryBot.define do
  factory :currency do
    name Forgery::Currency.description
    sequence(:short_name) { |sn| "#{Forgery::Currency.code}#{sn}" }
    symbol '$'
  end

  factory :price do
    full_price { 6000 }
    discount_price { 3000 }
    currency
    subscription
  end

  sequence(:base_name) { |sn| "subscription_#{sn}" }

  factory :product do
    name { generate(:base_name) }
    type { "reading" }
    duration { 14 }


    trait :reading do
      type { "reading subscription" }
    end

    trait :maths do
      type { "maths subscription" }
    end

    trait :six_month do
      name { "six_month_" + generate(:base_name) }
      duration { 183 }
    end

    trait :twelve_month do
      name { "twelve_month_" + generate(:base_name) }
      duration { 365 }
    end

    factory :six_month_reading, traits: [:six_month, :reading]
    factory :twelve_month_reading, traits: [:twelve_month, :reading]

    trait :with_price do
      transient do
        full_price 6000
        discount_price 3000
        short_name 'AUD'
      end

      after(:create) do |product, evaluator|
        currency = Currency.find_by(short_name: evaluator.short_name) ||
                     create(:currency, short_name: evaluator.short_name)
        create_list(
          :price,
          1,
          product: product,
          currency: currency,
          full_price: evaluator.full_price,
          discount_price: evaluator.discount_price
        )
      end
    end

    trait :with_aud_price do
      with_price
    end

    trait :with_usd_price do
      with_price short_name: 'USD'
    end
  end
end

create(:product, :with_aud_price) # works
create(:product, :with_usd_price) # fails "NoMethodError: undefined method `with_price=' for #<Subscription:0x007f9b4f3abf50>"

# What I really want to do
factory :postage, parent: :product do
  with_aud_price full_price: 795
  with_usd_price full_price 700
end
Selfassurance answered 1/12, 2015 at 0:30 Comment(0)
P
33

The :with_price trait needs to be on a separate line from the other attributes you're setting, i.e. use this:

trait :with_usd_price do
  with_price
  short_name: 'USD'
end

instead of this:

trait :with_usd_price do
  with_price short_name: 'USD'
end
Pulchritude answered 1/12, 2015 at 1:16 Comment(3)
At a guess it seems under the hood calling trait A within trait B makes trait B a subclass of trait A. Which would imply that it supports multiple inheritance and who knows what happens if both your traits define the same transient properties etc etc. I guess the first or last one called just takes precedence. Seems a bit surprising to define inheritance like a method call rather than with something like the parent: :foo syntax used for factories.Selfassurance
Ok. Have looked at this further, it's good as far as it goes, but I still can't do the example at the very bottom of the code sample in the OP. If I try in this form I get FactoryGirl::AttributeDefinitionError: Attribute already defined: full_price. Hmm... I might be able to make it work with arrays for the transient properties.Selfassurance
This doesn't seem to work in hooksVinitavinn
D
6

I'm on factory_bot 4.8.2 and the following is what works for me:

trait :with_usd_price do
  with_price
  short_name 'USD'
end
Different answered 28/2, 2018 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.