How to extract FactoryGirl traits in a separate file - ruby-on-rails

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.

Related

How to make FactoryBot return the right STI sub class?

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

FactoryBot get available traits for a factory

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"]

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

Factory Girl - Manipulate Transient Attribute

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!

Should I stub the model in Factory girl or in the spec file while testing?

Almost every spec file I come accross I end up writing stuff like:
before :each do
#cimg = Factory.build :cimg_valid
#cimg.stub(:validate_img).and_return true
#cimg.stub(:validate_img_url).and_return true
#cimg.stub(:save_images).and_return true
#cimg.stub(:process_image).and_return true
#cimg.stub(:img).and_return true
end
I mean, the model I get from Factory.build is completely valid. But if I don't stub that stuff it saves things in the filesystem, and validates stuff I'm not testing...
What I mean, I think it would be cleaner to do something like this:
before :each do
#cimg = Factory.build :cimg_for_testing_tags
end
If stubbing within the Factory is even possible.
What is the proper way to stub the model?
#fkreusch's answer works great until you use the new RSpec expect() syntax (3.0+)
Putting this into rails_helper.rb works for me:
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
In the OP's example, you can now do:
FactoryBot.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after(:build) do |cimg|
allow(cimg).to receive(:validate_img) { true }
end
end
end
Credit: github.com/printercu, see: https://github.com/thoughtbot/factory_bot/issues/703#issuecomment-83960003
In recent versions of factory_girl you have an after_build callback, so I believe you could define your factory like this:
FactoryGirl.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after_build do |cimg|
cimg.stub(:validate_img).and_return true
end
end
end
UPDATE
After factory_girl 3.3.0, the syntax has changed to following:
FactoryGirl.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after(:build) do |cimg|
cimg.stub(:validate_img).and_return true
end
end
end
A factory should produce "real world" objects therefore it's a bad practice (and error prone) to change behaviour (i.e. stub) in a factory.
You can do
let(:user) instance_double(User, FactoryGirl.attributes_for(:user))
before do
allow(user).to receive(:something).and_return('something')
end
and if your before clause gets too big you may want to extract it to a separate method or create a mock child class that overrides methods you want to stub.
You might also consider using FactoryGirl#build_stubbed.

Resources