FactoryGirl and polymorphic associations
Asked Answered
N

6

69

The design

I have a User model that belongs to a profile through a polymorphic association. The reason I chose this design can be found here. To summarize, there are many users of the application that have really different profiles.

class User < ActiveRecord::Base
  belongs_to :profile, :dependent => :destroy, :polymorphic => true
end

class Artist < ActiveRecord::Base
  has_one :user, :as => :profile
end

class Musician < ActiveRecord::Base
  has_one :user, :as => :profile
end

After choosing this design, I'm having a hard time coming up with good tests. Using FactoryGirl and RSpec, I'm not sure how to declare the association the most efficient way.

First attempt

factories.rb

Factory.define :user do |f|
  # ... attributes on the user
  # this creates a dependency on the artist factory
  f.association :profile, :factory => :artist 
end

Factory.define :artist do |a|
  # ... attributes for the artist profile
end

user_spec.rb

it "should destroy a users profile when the user is destroyed" do
  # using the class Artist seems wrong to me, what if I change my factories?
  user = Factory(:user)
  profile = user.profile
  lambda { 
    user.destroy
  }.should change(Artist, :count).by(-1)
end

Comments / other thoughts

As mentioned in the comments in the user spec, using Artist seems brittle. What if my factories change in the future?

Maybe I should use factory_girl callbacks and define an "artist user" and "musician user"? All input is appreciated.

Nannie answered 12/10, 2011 at 23:56 Comment(1)
Love this question, thanks!Bohi
O
13

Factory_Girl callbacks would make life much easier. How about something like this?

Factory.define :user do |user|
  #attributes for user
end

Factory.define :artist do |artist|
  #attributes for artist
  artist.after_create {|a| Factory(:user, :profile => a)}
end

Factory.define :musician do |musician|
  #attributes for musician
  musician.after_create {|m| Factory(:user, :profile => m)}
end
Ozalid answered 13/10, 2011 at 1:59 Comment(2)
Yeah, this looks nice. I had a feeling I could utilize a callback for this purpose. Two questions regarding this solution. 1) Should I just pick a random profile factory to use (ie, Musician or Artist) when testing the user model for the :dependent => destroy option? 2) Do you recommend testing the same functionality for each profile model? ie, should I have the test "it should retrieve the associated user object" for both Musician and Artist?Nannie
1) You could try something like 'user = Factory(:user) artist = Factory(:artist, :user => user) user.destroy! artist.reload!.valid?.should be_false' 2) IMO, less effort should be spent on testing application logic. I would restrict myself with test that Aritst 'has_one' user and likewise for user.Ozalid
D
149

Although there is an accepted answer, here is some code using the new syntax which worked for me and might be useful to someone else.

spec/factories.rb

FactoryGirl.define do

  factory :musical_user, class: "User" do
    association :profile, factory: :musician
    #attributes for user
  end

  factory :artist_user, class: "User" do
    association :profile, factory: :artist
    #attributes for user
  end

  factory :artist do
    #attributes for artist
  end

  factory :musician do
    #attributes for musician
  end
end

spec/models/artist_spec.rb

before(:each) do
  @artist = FactoryGirl.create(:artist_user)
end

Which will create the artist instance as well as the user instance. So you can call:

@artist.profile

to get the Artist instance.

Dropping answered 27/7, 2012 at 18:36 Comment(3)
Thanks! To further clarify, the '#attributes for user' does not need to be a block (I was confused). It can simply be username 'user1' email '[email protected]' etc., listed as regular attributes of the musical_user/artist_user factories.Bohi
Don't use this answer. You'll have to copy the same attributes for :musician_user and :artist_user. Not DRY. Use kuboon's or Kingsley Ijomah's answer. That said, I'm sure this answer was a helpful stepping stone for getting us to the right answer.Francesco
@Arcolye, By not DRY, I assume you're referring to there are more factories that need to be created. However, I can't see how the other 2 answers you suggested reduce the number of factories/traits. Don't you end up with the same thing?Haem
N
40

Use traits like this;

FactoryGirl.define do
    factory :user do
        # attributes_for user
        trait :artist do
            association :profile, factory: :artist
        end
        trait :musician do
            association :profile, factory: :musician
        end
    end
end

now you can get user instance by FactoryGirl.create(:user, :artist)

Nofretete answered 2/9, 2014 at 16:34 Comment(0)
O
13

Factory_Girl callbacks would make life much easier. How about something like this?

Factory.define :user do |user|
  #attributes for user
end

Factory.define :artist do |artist|
  #attributes for artist
  artist.after_create {|a| Factory(:user, :profile => a)}
end

Factory.define :musician do |musician|
  #attributes for musician
  musician.after_create {|m| Factory(:user, :profile => m)}
end
Ozalid answered 13/10, 2011 at 1:59 Comment(2)
Yeah, this looks nice. I had a feeling I could utilize a callback for this purpose. Two questions regarding this solution. 1) Should I just pick a random profile factory to use (ie, Musician or Artist) when testing the user model for the :dependent => destroy option? 2) Do you recommend testing the same functionality for each profile model? ie, should I have the test "it should retrieve the associated user object" for both Musician and Artist?Nannie
1) You could try something like 'user = Factory(:user) artist = Factory(:artist, :user => user) user.destroy! artist.reload!.valid?.should be_false' 2) IMO, less effort should be spent on testing application logic. I would restrict myself with test that Aritst 'has_one' user and likewise for user.Ozalid
A
5

You can also solve this using nested factories (inheritance), this way you create a basic factory for each class then nest factories that inherit from this basic parent.

FactoryGirl.define do
    factory :user do
        # attributes_for user
        factory :artist_profile do
            association :profile, factory: :artist
        end
        factory :musician_profile do
            association :profile, factory: :musician
        end
    end
end

You now have access to the nested factories as follows:

artist_profile = create(:artist_profile)
musician_profile = create(:musician_profile)

Hope this helps someone.

Alonaalone answered 15/6, 2014 at 10:15 Comment(1)
Nested factories can become difficult to wade through. Traits here are preferable. If you need a factory for a subclass, create a child factory that defines a parent.Joappa
O
2

It seems that polymorphic associations in factories behave the same as regular Rails associations.

So there is another less verbose way if you don't care about attributes of model on "belongs_to" association side (User in this example):

# Factories
FactoryGirl.define do
  sequence(:email) { Faker::Internet.email }

  factory :user do
    # you can predefine some user attributes with sequence
    email { generate :email }
  end

  factory :artist do
    # define association according to documentation
    user 
  end
end

# Using in specs    
describe Artist do      
  it 'created from factory' do
    # its more naturally to starts from "main" Artist model
    artist = FactoryGirl.create :artist        
    artist.user.should be_an(User)
  end
end

FactoryGirl associations: https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#associations

Optometer answered 26/3, 2013 at 9:28 Comment(0)
C
1

I currently use this implementation for dealing with polymorphic associations in FactoryGirl:

In /spec/factories/users.rb:

FactoryGirl.define do

  factory :user do
    # attributes for user
  end

  # define your Artist factory elsewhere
  factory :artist_user, parent: :user do
    profile { create(:artist) }
    profile_type 'Artist'
    # optionally add attributes specific to Artists
  end

  # define your Musician factory elsewhere
  factory :musician_user, parent: :user do
    profile { create(:musician) }
    profile_type 'Musician'
    # optionally add attributes specific to Musicians
  end

end

Then, create the records as usual: FactoryGirl.create(:artist_user)

Consumable answered 10/5, 2017 at 3:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.