FactoryBot - How can I pass a transient array? - ruby-on-rails

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

Related

Rspec / FactoryBot factories are not running after_initialize when createing a new instance in my rails app

I have some models that use an after_initialize hook like so to set a load of default attributes and other things:
after_initialize do |some_model|
initializer(some_model)
end
def initializer(some_model)
#... loads of pre-processing
end
This all runs great in development and production, however in my FactoryBot and rspec tests this hook isn't running. I have a factory that looks like this:
FactoryBot.define do
factory :some_model do
some_number { 1 }
is_it_something { false }
account { create(:user).accounts.first }
end
end
But my tests are looking like this:
some_model = create(:some_model)
some_model.initializer(some_model)
The issue is I'm having to call the initializer seperatly after creating the FactoryBot model. This is causing all sorts of issues because the create() triggers the create validations, and they fail because the initializer isn't ran.
How can I get FactoryBot to run after_initialize when creating a new instance?
Thanks.
(I might have got some terminology wrong here, so please correct me if I have.)
You could use the after(:build) callback of the factory to call the initializer since factorybot sets the attributes after initialization.
FactoryBot.define do
factory :some_model do
some_number { 1 }
is_it_something { false }
account { create(:user).accounts.first }
after(:build) do |some_model|
some_model.initializer(some_model)
end
end
end
that way you don't have to call it every time you create new object from the factory
Anyway, this initializer method looks like a code smell. I'm not sure what does it do, but maybe there's a better way to do that that also prevents this problem.
FactoryBot calls setters for attributes, not the constructor, so build :some_model, some_attribute: 123, other_attribute: :foo is in fact equivalent of:
SomeModel.new.tap{|m|
m.some_attribute = 123
m.other_attribute = :foo
}
This way initializer is called on a empty object and only after that all other attributes are getting set.
When you need to set some attributes that are dependent on others - i'd opt into custom setters like
def some_other_attribute=(val)
self.some_attribute = calculate(val)
super
end
or do it in before_validation

Factory Girl create_list with trait and dynamic variable

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

Rails FactoryGirl instance variable

I would like to create factory using local variable.
Currently I have the following factory:
FactoryGirl.define do
factory :offer_item, class: BackOffice::OfferItem do
service
variant
end
end
My expectation is to create something like below
FactoryGirl.define do
variant = FactroyGirl.create(:variant)
factory :offer_item, class: BackOffice::OfferItem do
service
variant { variant }
after(:create) do |offer_item|
offer_item.service.variants << variant
end
end
end
but then I get:
/.rvm/gems/ruby-2.2.3/gems/factory_girl-4.7.0/lib/factory_girl/registry.rb:24:in `find': Factory not registered: variant (ArgumentError)
All models are nested inside BackOffice module. Generally I want the same object has association with two other objects. I think there is a some problem with scope in my factory.
Variant factory is inside other separated file.
The issue is you are trying to create a factory before FactoryGirl has finished loading all of the factory definitions. This is because you defined the variable at the scope of a factory definition. Even if this did work, you would likely end up sharing the same variant record between multiple offer_items that you create in your test because this code would only get executed once during initialization.
Since you defined an association to Variant, you probably don't need to create it as an extra step. We can leverage FactoryGirl's ability to create our associations for us and then copy the object into your #service in your after(:create) callback. Maybe it would look something like this (untested):
FactoryGirl.define do
factory :offer_item, class: BackOffice::OfferItem do
service
variant
after(:create) do |offer_item|
offer_item.service.variants << offer_item.variant
end
end
end

Is it possible to define a method in a Factory Girl factory that is not in the associated Active::Record model?1

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

error in FactoryGirl when trying to create objects before another object

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

Resources