简体   繁体   中英

Factory Girl with polymorphic association for has_many and has_one

I am currently working on a project and I wanted to create tests using factory girl but I'm unable to make it work with polymorphic has_many association. I've tried many different possibilities mentioned in other articles but it still doesn't work. My model looks like this:

class Restaurant < ActiveRecord::Base
  has_one :address, as: :addressable, dependent: :destroy
  has_many :contacts, as: :contactable, dependent: :destroy

  accepts_nested_attributes_for :contacts, allow_destroy: true
  accepts_nested_attributes_for :address, allow_destroy: true

  validates :name, presence: true
  #validates :address, presence: true
  #validates :contacts, presence: true
end

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true

  # other unimportant validations, address is created valid, the problem is not here
end

class Contact < ActiveRecord::Base
  belongs_to :contactable, polymorphic: true
  # validations ommitted, contacts are created valid
end

So bassically I want to create factory for Restaurant with address and contacts (with validations on Restaurant for presence, but if it's not possible, even without them) but I'm unable to do so. Final syntax should be like:

let(:restaurant) { FactoryGirl.create(:restaurant) }

Which should also create associated address and contacts. I have read many articles but I always get some sort of error. Currently my factories (sequences are defined correctly) are like this:

factory :restaurant do
  name
  # address {FactoryGirl.create(:address, addressable: aaa)}
  # contacts {FactoryGirl.create_list(:contact,4, contactable: aaa)}
  # Validations are off, so this callback is possible
  after(:create) do |rest|
    # Rest has ID here
    rest.address = create(:restaurant_address, addressable: rest)
    rest.contacts = create_list(:contact,4, contactable: rest)
  end
end

factory :restaurant_address, class: Address do
  # other attributes filled from sequences...

  association :addressable, factory: :restaurant
  # addressable factory: restaurant
  # association(:restaurant)
end

factory :contact do
  contact_type
  value

  association :contactable, :factory => :restaurant
end

Is there a way to create restaurant with one command in test with addresses and contacts set? Do I really need to get rid off my validations because of after(:create) callback? Current state is as fllowing:

  1. Restaurant is created with name and id.
  2. Than the address is being created - all is correcct, it has all the values including addressable_id and addressable_type
  3. After that all contacts are being creaed, again everything is fine, cntacts has the right values.
  4. After that, restaurant doesn't have any ids from associated objects, no association to adddress or contacts
  5. After than, for some reason restaurant is build again (maybe to add those associations?) and it fails: I get ActiveRecord:RecordInvalid.

I'm using factory_girl_rails 4.3.0, ruby 1.9.3 and rails 4.0.1. I will be glad for any help.

UPDATE 1:

Just for clarification of my goal, I want to be able to create restaurant in my spec using one command and be able to access associated address and contacts, which should be created upon creation of restaurant. Let's ignore all validations (I had them commented out in my example from the beginning). When I use after(:build) , first restaurant is created, than address is created with restaurant's ID as addressable_id and class name as addressable_type . Same goes to contacts, all are correct. The problem is, that restaurant doesn't know about them (it has no IDs of address or contacts), I can't access them from restaurant which I want to.

After really thorough search I have found an answer here - stackoverflow question .This answer also point to this gist . The main thing is to build associations in after(:build) callback and then save them in after(:create) callback. So it looks like this:

factory :restaurant do
  name

  trait :confirmed do
    state 1
  end

  after(:build) do |restaurant|
    restaurant.address = build(:restaurant_address, addressable: restaurant)
    restaurant.contacts = build_list(:contact,4, contactable: restaurant)
  end

  after(:create) do |restaurant|
    restaurant.contacts.each { |contact| contact.save! }
    restaurant.address.save!
  end
end

I had also a bug in my rspec, because I was using before(:each) callback instead of before(:all). I hope that this solution helps someone.

The Problem

Validating the length of a related list of rows is a difficult problem to frame in SQL, so it's a difficult problem to frame in ActiveRecord as well.

If you're storing a restaurant foreign key on the addresses table, you can't ever actually create a restaurant that has addresses by the time it's saved, because you need to save the restaurant to get its primary key. You can get around this problem in ActiveRecord by building up the associated objects in memory, validating against those, and then committing the entire object graph in one SQL transaction.

How to do what you're asking

You can generally get around this by moving things into an after(:build) hook instead of after(:create) . ActiveRecord will save its dependent has_one and has_many associations once it saves itself.

You're getting errors now because you can't modify an object to satisfy validations in an after(:create) block, because validations have already run by the time the callback runs.

You can change your restaurant factory to look something like this:

factory :restaurant do
  name

  after(:build) do |restaurant|
    restaurant.address = build(:restaurant_address, addressable: nil)
    restaurant.contacts = build(:contact, 4, contactable: nil)
  end
end

The nil s there are to break the cyclic relationship between the factories. If you do it this way, you can't have a validation on the addressable_id or contactable_id keys, because they won't be available until the restaurant is saved.

Alternatives

Although you can get both ActiveRecord and FactoryGirl to do what you're asking, it sets up a precarious list of dependencies which are difficult to understand and are likely to result in leaky validations or unexpected errors like the ones you're seeing now.

If you're validating contacts this way from the restaurant model because of a form in which you create both a restaurant and its corresponding contacts, you can save yourself a lot of pain by creating a new ActiveModel object to represent that form. You can collect the attributes you need for each object there, move some of the validations (especially the ones which validate the length of the contacts list), and then create the object graph on that form in a way that's much clearer and less likely to break.

This has the added benefit of making it easy to create lightweight restaurant objects in other tests which don't need to worry about contacts or addresses. If you force your factories to create these dependent objects every time, you'll quickly run into two problems:

  • Your tests will be painfully slow. Creating five dependent records every time you want to work with a restaurant won't scale very far.
  • If you ever want to specify different contacts or addresses in your tests, you'll constantly be fighting with your factories.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM