FactoryGirl: Populate a has many relation preservi

2019-03-13 20:51发布

问题:

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?

回答1:

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


回答2:

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


回答3:

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


回答4:

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.


回答5:

This kind of thing works for me:

factory :offering do
  trait :stay do
    ...
    photos { |o| [o.association(:hotel_photo)] }
  end
end