I have a use case where I would like to manipulate a transient attribute in a factory based on the traits that are included. Is there a way to do this, or is there a better way to do what I'm trying to accomplish?
Let's say I'm building a House object. The house can have many windows. I want to create some sub-factories that will automatically create some windows, but I want to be able to add a trait for a specific type of window. The window_types transient attribute is actually a list of traits for the window factories.
factory :house do
floors 3
exterior 'Brick'
transient do
window_types { [:bay, :double_hung] }
end
trait :with_picture_window do
window_types.push(:picture)
end
factory :ranch_house do
floors 1
after(:create) do |house, evaluator|
evaluator.window_types.each do |window_type|
FactoryGirl.create :window, window_type
end
end
end
factory :mountain_house do
floors 2
exterior 'Log'
after(:create) do |house, evaluator|
evaluator.window_types.each do |window_type|
FactoryGirl.create :window, window_type
end
end
end
end
factory :window do
material 'Glass'
trait :bay do
# bay window attributes
end
trait :double_hung do
# double hung window attributes
end
trait :picture do
# picture window attributes
end
end
This throws a NoMethodError: undefined method 'push' for #<FactoryGirl::Declaration::Implicit> error from within the :with_picture_window trait.
For the sake of argument, assume that I do need different factories for these houses. Is there a way that I can modify the window_types transient attribute from within a trait, and then have that transient attribute be reflected in my sub-factories?
I would like to be able to do:
FactoryGirl.create :ranch_house
# creates a ranch house with only bay and double_hung windows
FactoryGirl.create :ranch_house, :with_picture_window
# creates a ranch house with bay, double_hung, AND picture windows
If not, is there a better way that I can accomplish this?
Many thanks in advance.
I was able to find a suitable solution based on the idea presented by Ben. I used a local array variable to store the types of windows that needed to be created. The traits for each window add a type of window to the array in the after(:build) block. Then, in an after(:create) block I actually create the window records.
The very first after(:build) block (the one not in a trait) is important because that resets the window_types array between object creation.
FactoryGirl.define do
window_types = []
factory :house do
floors 3
exterior 'Brick'
after(:build) do
window_types = []
end
after(:create) do |house|
window_types.each do |window_type|
FactoryGirl.create(:window, window_type, house: house)
end
end
trait :with_picture_window do
after(:build) do
window_types << :picture
end
end
trait :with_double_hung_window do
after(:build) do
window_types << :double_hung
end
end
trait :with_bay_window do
after(:build) do
window_types << :bay
end
end
factory :house_with_bay_and_picture_window, traits: [:bay, :picture]
end
end
I'm very curious to hear any thoughts on this approach. For now, this suits my needs and is very flexible.
I am not sure this will answer your question but commenting would be useless with the formatting limitations. Perhaps the followign pattern could be useful in solving this?
trait :with_window do
after(:build) do |house|
# Create a house window
house.windows << FactoryGirl.build(:window) #Presume you could also pass a window trait here.
end
after(:create) do |room|
# Clear the windows attached in after(:build) and create one without saving the house again
house.windows.each do |window|
window.house_id = house.id
window.save
end
house.reload
end
end
Hope this provides help in some way!
Related
Is there a FactoryBot method or some way to get available traits for a factory?
Ex:
FactoryBot.define do
factory :address, class: Address do
trait :in_california do
state 'CA'
end
trait :in_new_york do
state 'NY'
end
trait :in_florida do
state 'FL'
end
end
I want to be able to get the traits programatically, something like FactoryBot.get_traits (:address) and it would return an array of the traits defined for that factory, in this case that would be
["in_california", "in_new_york", "in_florida"]
Does that make it clearer?
I believe what you want is the following:
FactoryBot.factories[:address].defined_traits.map(&:name)
#=> ["in_california", "in_new_york", "in_florida"]
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
I have a bunch of factories that share a common trait:
trait :with_images do
after(:create) do |resource|
resource.images << FactoryGirl.create(:image, imageable: resource)
resource.enabled = true
resource.save
end
end
I would like to extract it in a separate file but not 100% sure how to arrange it.
Traits can be defined globally, so then you can use them in another factory you like. You could create a new file inside spec/factories, like, spec/factories/traits.rb or something:
And define your global traits:
FactoryGirl.define do
trait :complete do
complete false
end
end
And then you would have in another file, say spec/factories/user.rb, a different factory using this trait:
FactoryGirl.define do
factory :user do
complete
end
end
I'm just not sure if that's a good idea, I mean, I think it should be visible right away that a trait is kind of a complement of a given factory. It is very clean, but not so readable.
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