I'm trying to create some extra records from a Factorygirl callback. It has to use one of the traits in that factory and it has to pass it a dynamic variable. I thought I could do something like this:
create_list(:some_factory, 5, :some_trait, dynamic_variable: 1)
But I get an undefined method `dynamic_variable=' on the ActiveRecord object
You need to define the transient in factory. Here is an example, I copied from wiki
factory :user do
transient do
rockstar true
upcased false
end
name { "John Doe#{" - Rockstar" if rockstar}" }
email { "#{name.downcase}#example.com" }
after(:create) do |user, evaluator|
user.name.upcase! if evaluator.upcased
end
end
create_list(:user, 10, upcased: true).name
Related
I’m using Rails 4.2.3 with FactoryGirl. I have this factory for my users
FactoryGirl.define do
…
factory :user do
after(:build) do |user, vars|
print "in main user after build.\n"
def user.publish
# and here you can stub method response if you need
end
end
…
trait :with_callbacks do
after(:build) do |user, vars|
print "after build my user\n"
end
…
end
I wanted to override my base “after(:build)” method, so I created the trait “with_callbacks.” But when I call my factory with my traits
create(:user, :my_user)
It seems like both “after(:build)” methods are getting called based on the output …
after build my user
in main user after build.
Is there a way to rig things so that I can override the base factory’s “after(:build)” method?
Not sure what you're trying to do, but another option would be to use a transient variable (called ignore in FactoryBot versions < 5.0) to control the behavior, which can be overwritten in a trait.
Something like:
transient do # use `ignore` in factory bot < 5.0
with_callback_behavior { false }
end
after(:build) do |user, vars|
if vars.with_callback_behavior
puts "behavior for trait after build."
else
puts "behavior for main main after build."
end
end
trait :with_callbacks do
with_callback_behavior { true }
end
Of course, this means that
build :user, :with_callbacks
#=> "behavior for trait after build."
is the same as
build :user, with_callback_behavior: true
#=> "behavior for trait after build."
I currently have a FactoryBot trait set up as follows:
trait :with_role do
transient do
role { nil }
end
after(:create) do |staff_member, factory|
staff_member.staff_roles << StaffRole.fetch(factory.role) if factory.role
end
end
However, this trait only allows the factory to be passed a single role. I'm refactoring a series of tests wherein mutliple roles need to be assigned. This is a M-N relationship supported both by the DB and ORM via a junction table, and clearly functions as expected outside of FactoryBot because the original test implementation passes. My approach is as follows:
# Inside the factory
trait :with_roles do
transient do
roles { [] }
end
after(:create) do |staff_member, factory|
roles.each { |role| staff_member.staff_roles << StaffRole.fetch(factory.send(role)) }
end
end
# Invocation
staff_member = FactoryBot.create(:staff_member, :with_roles, roles: [
:role_1,
:role_2,
:role_3
], some_other_attribute: 1)
The problem is that when I try to invoke in this way, I get the following error:
NameError: undefined local variable or method `roles' for #<FactoryBot::SyntaxRunner:...>
I get the exact same error if I initialise roles as nil instead of an empty array. The previous implementation works fine, and I've followed it as closely as possible. Why is roles undefined despite being defined as a transient variable? Am I missing something about how FactoryBot works, and trying to do something impossible? Or am I just appproaching this wrong?
You should call a transient attribute on a factory:
after(:create) do |staff_member, factory|
factory.roles.each { |role| staff_member.staff_roles << StaffRole.fetch(factory.send(role)) }
end
So I have a factory:
factory :person do
password_string { Faker::Lorem.words(3).join }
after(:create) do |object|
object.password = object.password_hash(object.password_string)
object.save!
end
end
And I get an error:
NoMethodError:
undefined method `password_string=' for #<Person:0xe1cdf00>
Which is expected, but I want to define the password_string for my test environment (mainly so I can mock a signed in user). Is there a way to get around the NoMethodError by defining attributes that are unique to the factory?
Thanks,
Yes, you can! Usually you'd put these on a trait, not certain if you can put them on a plain factory or not.
trait :with_password_string do
ignore do
password_string nil
end
after(:build) do |content, evaluator|
if evaluator.password_string
content.password = content.password_hash(evaluator.password_string)
end
end
end
I am using traits to modify the behavior of my factory.
When the :with_answers trait is used, I want to create a quiz_answer with the created submission as parameter.
FactoryGirl.define do
factory :quiz_submission do
quiz_id 1
[...]
trait :with_answers do
after(:create) do |submission|
FactoryGirl.create(:quiz_answer, quiz_submission: submission.id)
[...]
end
end
end
end
The block that is passed to after(:create) is never entered, though.
Can anyone tell me why?
EDIT:
I call the factory with FactoryGirl.create :quiz_submission, :with_answers
The proper solution is to have a transient attribute and a global after(:create) handler that does the work based on that attribute.
Something like this:
FactoryGirl.define do
factory :quiz_submission do
transient do
submissions_count { 0 }
end
quiz_id 1
[...]
trait :with_answers do
submissions_count { 1 }
end
after(:create) do |submission, evaluator|
create_list(:quiz_answer, evaluator.submissions_count, quiz_submission: submission.id)
[...]
end
end
end
I am trying to create a model named global_list. It will take a list which is associated with a user and a global_id which is associated with an item. I need to create a user (a list is created in a callback for this object) and an item (where a global_identification is created as a callback to item creation). Neither of the following solutions work in trying to create these associated objects before. Should they? Is there a better way to handle this? I have tried creating accessors for liked_item_list_id but that also didn't work.
FactoryGirl.define do
factory :global_list do
before(:create) {
user=FactoryGirl.create(:user)
mi=FactoryGirl.create(:item)
}
list_id user.liked_list_id
global_id mi.global_id
end
end
Will a block help?
FactoryGirl.define do
factory :global_list do
before(:create) {
user=FactoryGirl.create(:user)
mi=FactoryGirl.create(:item)
}
list_id { List.first.id }
global_id { 3 } # 3 will be created at this point { mil.global_id } doesn't work
end
end
I'm starting to think this is not possible with FactoryGirl. Any help would be appreciated?
Try this.
FactoryGirl.define do
factory :global_list do
before(:create) { |list|
user=FactoryGirl.create(:user)
mi=FactoryGirl.create(:item)
list.list_id = user.liked_list_id
list.global_id = mi.global_id
}
end
end
Its completely untested
This is totally possible in FactoryGirl, and actually often used.
It's simple to define associated object in a factory like below. No fancy "before" needed.
FactoryGirl.define do
factory :global_list do
user
mi
foo_attribute
bar_attribute
end
factory :user do
# blah
end
factory :mi do
# blah
end
end
With above code, FactoryGirl will create required associated object at first and use their id to create this object automatically.
this did it:
FactoryGirl.define do
factory :global_list do
list_id { FactoryGirl.create(:user).liked_list_id }
global_id { FactoryGirl.create(:item).global_id }
end
end
Ruby's blocks in this context are confusing. So I asked: what's the practical difference between these two FactoryGirl declarations