Reputation: 129
I would like to use FactoryBot to return trait randomly like that:
FactoryBot.define do
factory :user do
[:active, inactive].sample
trait :active do
active { true }
...
end
trait :inactive do
active { false }
...
end
end
end
To do that:
(1..5).map{ |e| FactoryBot.build(:user) }.map(&:active?)
=> [true, false, false, true, false]
Actually is like that:
FactoryBot.define do
factory :user do
active { [true, false].Sample }
name { "name-#{SecureRandom.uuid}" }
birthday { active == true ? rand(18..99).years.ago - rand(0..365).days.ago : nil }
preferred_contact_method { active == true ? %w(phone email).sample : nil }
activated_at { active == true ? rand(1..200).days.ago : nil }
contact_phone_number { preferred_contact_method == "phone" ? "+33XXXXXXXXX" : nil }
contact_email { preferred_contact_method == "email" ? "[email protected]" : nil }
end
end
Is it possible to do that?
Upvotes: 3
Views: 1492
Reputation: 798
I agree with the comments on the question that in some cases, randomizing the traits at the test definition would be sufficient and appropriate. However this is not a one size fits all solution.
Would it be sufficient to write: FactoryBot.build(:user, [:active, :inactive].sample)? – Tom Lord Mar 21, 2019 at 9:25
It works but you have to think about it every time you use the factory. – Hugo Barthelemy Mar 21, 2019 at 9:42
In my case, I am wanting to randomize the factory because I need the results to be consistent regardless of the inconsistencies that come from 3rd party data. I prefer the chaos in the factory to simulate this expected randomness in the test input. Think of it as a much lighter weight adoption of the Netflix Simian Army approach sometimes referred to as adversarial debugging
.
I ran into issues with both of the provided answers and found the following to work for my needs, which include maintaining the Factory's ability to accept traits in the test and still behave as expected.
FactoryBot.define do
factory :user do
# I am intentionally not defining a default 'active' attribute for this example, to encourage the use of the traits that have been defined.
trait :active do
active { true }
end
trait :inactive do
active { false }
end
end
factory :randomized_user, parent: :user do
after(:build) do |user, _evaluator|
[].then do |traits|
traits << [:active, :inactive].sample
# more dynamic traits, if desired
# these keys will be strings, so we will symbolize them for compatibility with the merge method used later.
user.attributes.symbolize_keys.then do |user_attributes|
# since we can not render just the trait's attributes, we are going to render all of them for the parent factory with the selected traits passed in.
# NOTE: attributes_for will generate symbolized_keys.
random_attributes = attributes_for(:user, *traits)
# set the records attributes to the merge of present random_attributes with the present user_attributes taking precedence
user.attributes = random_attributes.compact.merge(user_attributes.compact)
end
end
end
end
end
# static factory results
FactoryBot.create_list(:user, 5).pluck(:active) => [nil, nil, nil, nil, nil]
FactoryBot.create_list(:user, 5, :active).pluck(:active) => [true, true, true, true, true]
FactoryBot.create_list(:user, 5, :inactive).pluck(:active) => [false, false, false, false, false]
# dynamic factory results
FactoryBot.create_list(:randomized_user, 5).pluck(:active) => [true, true, false, true, false]
FactoryBot.create_list(:randomized_user, 5).pluck(:active) => [false, true, false, false, true]
# dynamic factory results, overridden by the passed in trait taking precedence in this solution
FactoryBot.create_list(:randomized_user, 5, :active).pluck(:active) => [true, true, true, true, true]
FactoryBot.create_list(:randomized_user, 5, :inactive).pluck(:active) => [false, false, false, false, false]
after(:build)
fires regardless of build_strategy (create or build). This block also applies all of the standard Factory logic before, instead of after, any randomization is applied. Therefor, updating the resulting attributes is all that needs to happen.lambda
approachwhile this randomizes the factory it only randomizes it at the factory's definition, which means that it becomes a static definition once the factory is initialized and all factory products will be consistent until the next test run. I want randomization from one factory product to the next.
initialize_with
approachMy implementation of this answer resulted in a stack level too deep error or when tweaking the solution, I found that the factory definitions would stomp over the attributes that were being assigned in the initialize_with
block, or replicate the issue from the lambda
approach. Additionally, the check on an attribute being nil
may work often, but can cause problems as this would become an implied rule and worse if the value can / should be nil
. I prefer to be explicit.
Upvotes: 0
Reputation: 44725
That is quite an old question, but just had the same need as OP and I think I found the solution, alas it is not pretty:
FactoryBot.define do
factory :user do
active { nil }
initialize_with do
if active.nil?
build :user, %i[active inactive].sample, attributes
else
User.new(attributes)
end
end
trait :active do
active { true }
...
end
trait :inactive do
active { false }
...
end
end
end
FactoryBot.create_list(:user, 5).map(&:active) #=> random array of booleans
FactoryBot.create_list(:user, 5, :active).all?(&:active?) #=> true
Upvotes: 1
Reputation: 11423
Working on an eventual answer, I came across the fact that a trait
execute only once the block it is given. I'll share the sample of code I was on for the time being, although if you test it, you will only have active users or inactive users, but not both.
You could export the logic of your traits in two lambdas and then choose one by random:
trait_active = lambda do |context|
context.active { true }
#...
end
trait_inactive = lambda do |context|
context.active { false }
# ...
end
FactoryBot.define do
factory :user do
trait :active do
trait_active.call(self)
end
trait :inactive do
trait_inactive.call(self)
end
trait :schrodinger do
[trait_active, trait_inactive].sample.call(self)
end
end
end
The context
attribute in the lambda is quite important here, you can see more
about that in this answer.
Upvotes: 0