My problem seems very common, but I haven't found any answer in the documentation or the internet itself.
It might seem a clone of this question has_many while respecting build strategy in factory_girl but 2,5 years after that post factory_girl changed a lot.
I have a model with a has_many relation called photos. I want to populate this has many relation preserving my choice of build strategy.
If I call offering = FactoryGirl.build_stubbed :offering, :stay
I expect offering.photos
to be a collection of stubbed models.
The only way i've found to achieve this is this one:
factory :offering do
association :partner, factory: :named_partner
association :destination, factory: :geolocated_destination
trait :stay do
title "Hotel Gran Vía"
description "Great hotel in a great zone with great views"
offering_type 'stay'
price 65
rooms 70
stars 4
event_spaces 3
photos do
case @build_strategy
when FactoryGirl::Strategy::Create then [FactoryGirl.create(:hotel_photo)]
when FactoryGirl::Strategy::Build then [FactoryGirl.build(:hotel_photo)]
when FactoryGirl::Strategy::Stub then [FactoryGirl.build_stubbed(:hotel_photo)]
end
end
end
end
No need to say that IT MUST EXIST a better way of do that.
Ideas?
Here's a slightly cleaner version of Flipstone's answer:
factory :offering do
trait :stay do
...
photos do
association :hotel_photo, :strategy => @build_strategy.class
end
end
end
You can use the various FactoryGirl callbacks:
factory :offering do
association :partner, factory: :named_partner
association :destination, factory: :geolocated_destination
trait :stay do
title "Hotel Gran Vía"
description "Great hotel in a great zone with great views"
offering_type 'stay'
price 65
rooms 70
stars 4
event_spaces 3
after(:stub) do |offering|
offering.photos = [build_stubbed(:hotel_photo)]
end
after(:build) do |offering|
offering.photos = [build(:hotel_photo)]
end
after(:create) do |offering|
offering.photos = [create(:hotel_photo)]
end
end
end
You can also invoke the FactoryRunner class directly and pass it the build strategy to use.
factory :offering do
trait :stay do
...
photos do
FactoryGirl::FactoryRunner.new(:hotel_photo, @build_strategy.class, []).run
end
end
end
Other answers have a flaw, the inverse association is not being properly initialized, e.g. offering.photos.first.offering == offering
is false
. Even worse that being incorrect, the offering
is a new Offering
for each of the photos
.
Also, explicitly specifying a strategy is redundant.
To overcome the flow and to simplify things:
factory :offering do
trait :stay do
...
photos do
association :hotel_photo, offering: @instance
end
end
end
@instance
is an instance of the Offering
being created by the factory at the moment. For the curious, context is FactoryGirl::Evaluator
.
If you don't like the @instance
like I do, you may look in evaluator.rb
and find the following:
def method_missing(method_name, *args, &block)
if @instance.respond_to?(method_name)
@instance.send(method_name, *args, &block)
I really like how itself
looks:
factory :offering do
trait :stay do
...
photos do
association :hotel_photo, offering: itself
end
end
end
Do be able to use itself
, undefine it on the Evaluator
:
FactoryGirl::Evaluator.class_eval { undef_method :itself }
It will be passed to the @instance
and will return the @instance
itself.
For the sake of providing a full example with several photos:
factory :offering do
trait :stay do
...
photos do
3.times.map do
association :hotel_photo, offering: itself
end
end
end
end
Usage:
offering = FactoryGirl.build_stubbed :offering, :stay
offering.photos.length # => 3
offering.photos.all? { |photo| photo.offering == offering } # => true
Be careful, some things might not work as expected:
offering.photos.first.offering_id
will surprisingly be nil
;
offering.photos.count
will hit the database with a SELECT COUNT(*) FROM hotel_photos ...
(and will return 0 in most cases), please use length
or size
in assertions.
This kind of thing works for me:
factory :offering do
trait :stay do
...
photos { |o| [o.association(:hotel_photo)] }
end
end