FactoryGirl: Populate a has many relation preserving build strategy
Asked Answered
Y

5

10

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?

Yell answered 9/11, 2012 at 13:13 Comment(0)
T
9

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
Tammany answered 8/11, 2013 at 14:34 Comment(3)
It seems to me this works for associations that are not has_many?Malita
I don't think you need to specify strategy here.Ogilvy
Inverse does not work for me, e.g. offering.photos.first.offering == offering is false.Ogilvy
D
10

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
Diorite answered 15/11, 2012 at 20:35 Comment(5)
It's a better way, indeed. I was specting the existence of a method like make(:hotel_photo) that respects your preferences, but this seems good enought. AcceptedYell
Thanks! I removed a little duplication with send(hook, :thing). It'd be nice if this were available in a generic helper for any factory.Squarerigger
I've created an issue github for exposing that idea. github.com/thoughtbot/factory_girl/issues/458Yell
after(:stub) do |offering| offering.photos = [build_stubbed(:hotel_photo)] end This does not work. FactoryGirl raises the error: stubbed models are not allowed to access the databaseCozen
@Cozen Hmm, which version of factory girl are you using? Also, any chance you could provide a link to a gist with a reproduction of that exception?Diorite
T
9

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
Tammany answered 8/11, 2013 at 14:34 Comment(3)
It seems to me this works for associations that are not has_many?Malita
I don't think you need to specify strategy here.Ogilvy
Inverse does not work for me, e.g. offering.photos.first.offering == offering is false.Ogilvy
G
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
Guss answered 11/2, 2013 at 15:35 Comment(0)
O
1

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.
Ogilvy answered 29/4, 2017 at 19:32 Comment(0)
O
0

This kind of thing works for me:

factory :offering do
  trait :stay do
    ...
    photos { |o| [o.association(:hotel_photo)] }
  end
end
Oreilly answered 8/4, 2014 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.