FactoryGirl和多态关联

69

设计

我有一个用户模型,它通过多态关联属于一个个人资料。我选择这种设计的原因可以在这里找到。简而言之,该应用程序有许多具有非常不同资料的用户。

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

选择了这个设计后,我很难想出好的测试。使用FactoryGirl和RSpec,我不确定最有效的声明关联的方式。

第一次尝试

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

评论/其他想法

如用户规范中所述,在使用Artist时似乎很脆弱。如果我的工厂将来发生了变化怎么办?

也许我应该使用factory_girl callbacks并定义“artist user”和“musician user”?非常感谢您的建议。


喜欢这个问题,谢谢! - Micah
6个回答

148
尽管已有一个被接受的答案,但这里提供一些使用新语法的代码,对我很有效,也许对其他人也有用。 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

这将创建一个艺术家实例和用户实例。因此,您可以调用:

@artist.profile

获取Artist实例。


谢谢!为了进一步澄清,“#attributes for user”不需要是一个块(我感到困惑)。它可以简单地作为musical_user/artist_user工厂的常规属性列出,例如用户名“user1”电子邮件“user@test.com”等。 - Micah
1
不要使用这个答案。你将不得不为:musician_user和:artist_user复制相同的属性。不符合DRY原则。使用kuboon或Kingsley Ijomah的答案。话虽如此,我相信这个答案对于让我们找到正确答案是一个有用的垫脚石。 - Arcolye
@Arcolye,你提到的不DRY,我理解为需要创建更多的工厂。然而,我无法看出你建议的另外两个答案如何减少工厂/特征的数量。难道最终不是得到相同的结果吗? - Leo Lei

40

像这样使用特征(trait):

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

现在您可以通过FactoryGirl.create(:user, :artist)获得用户实例。


13

Factory_Girl的回调可以让生活变得更加轻松。这样怎么样?

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

1
是的,这看起来不错。我有一种感觉可以利用回调来实现这个目的。关于这个解决方案有两个问题。1)在测试用户模型的:dependent => destroy选项时,我应该随机选择一个配置文件工厂来使用(例如,音乐家或艺术家)吗?2)您是否建议为每个配置文件模型测试相同的功能?即,我是否应该对音乐家和艺术家都进行“它应该检索相关的用户对象”的测试? - Feech
  1. 你可以尝试这样做: 'user = Factory(:user) artist = Factory(:artist, :user => user) user.destroy! artist.reload!.valid?.should be_false'
  2. 在我看来,测试应用逻辑的工作量应该少一些。我只会限制在测试Aritst是否'has_one' user,以及user同理。
- membLoper

5
你也可以使用嵌套工厂(继承)来解决这个问题,这样你就可以为每个类创建一个基本工厂,然后嵌套从这个基本父工厂继承的工厂。
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

您现在可以按照以下方式访问嵌套工厂:
artist_profile = create(:artist_profile)
musician_profile = create(:musician_profile)

希望这能帮助到有需要的人。

2
嵌套工厂可能会变得难以理解。在这里,特征更可取。如果您需要一个子类的工厂,请创建一个定义父类的子工厂。 - steel

2

在工厂中,多态关联似乎表现得与常规的Rails关联相同。

因此,如果您不关心“belongs_to”关联方(例如此示例中的用户)的模型属性,则有另一种更简洁的方法:

# 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的关联: https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#associations


1

我目前使用以下实现来处理FactoryGirl中的多态关联:

在/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

然后,像往常一样创建记录:FactoryGirl.create(:artist_user)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接