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"]
Related
I'm making a big change in my system, so I changed one of my main tables into a STI, and create subclasses to implement the specific behavior.
class MainProcess < ApplicationRecord
end
class ProcessA < MainProcess
end
class ProcessB < MainProcess
end
In the application code, if I run MainProcess.new(type: 'ProcessA') it will return a ProcessA as I want.
But in the Rspec tests when I run FactoryBot::create(:main_process, type: 'ProcessA') it is returning a MainProcess and breaking my tests.
My factor is something like this
FactoryBot.define do
factory :main_process do
foo { 'bar' }
end
factory :process_a, parent: :main_process, class: 'ProcessA' do
end
factory :process_b, parent: :main_process, class: 'ProcessB' do
end
end
Is there some way to make FactoryBot have the same behavior of normal program?
I found the solution
FactoryBot.define do
factory :main_process do
initialize_with do
klass = type.constantize
klass.new(attributes)
end
end
...
end
The answer was founded here http://indigolain.hatenablog.com/entry/defining-factory-for-sti-defined-model (in japanese)
Edit #1:
⚠⚠⚠ Important ⚠⚠⚠
As mentioned here initialize_with is part of a private FactoryBot API.
According to the documentation:
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
So avoid to use if you can. (although I didn't find any other way to achieve this result without use it)
Edit #2
Besides the warning in the gem documentation (described above), the GETTING_STARTED.md actually suggest you use it
If you want to use factory_bot to construct an object where some attributes are passed to initialize or if you want to do something other than simply calling new on your build class, you can override the default behavior by defining initialize_with on your factory
If you just modify your original code to specify the class as the class type instead of a string, it works:
FactoryBot.define do
factory :main_process do
foo { 'bar' }
end
factory :process_a, parent: :main_process, class: ProcessA do
end
factory :process_b, parent: :main_process, class: ProcessB do
end
end
Here's the relevant section of the FactoryBot documentation.
initialize_with is marked as part of FactoryBot's Private API and not recommended for external use.
I think you can use nested factories to accomplish this.
factory :process do
factory :type_a_process, class: Process::TypeA do
type {"Process::TypeA"}
end
factory :type_b_process, class: Process::TypeB do
type {"Process::TypeB"}
end
end
end
FactoryBot.create(:type_b_process)
This is better:
initialize_with { type.present? ? type.constantize.new : Invoice.new }
https://dev.to/epigene/simple-trick-to-make-factorybot-work-with-sti-j09
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
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!
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