在具有唯一约束的关联的Rails中使用factory_girl。 获取重复的错误

我正在使用Rails 2.2项目来更新它。 我正在用工厂replace现有的灯具(使用factory_girl),并有一些问题。 问题在于使用查找数据表示表的模型。 当我创build具有相同产品types的两种产品的购物车时,每个创build的产品都将重新创build相同的产品types。 产品types模型上的唯一validation的这个错误。

问题示范

这是从一个unit testing,我创build一个车,把它放在一起。 我必须这样做来解决这个问题。 这仍然表明了问题。 我会解释。

cart = Factory(:cart) cart.cart_items = [Factory(:cart_item, :cart => cart, :product => Factory(:added_users_product)), Factory(:cart_item, :cart => cart, :product => Factory(:added_profiles_product))] 

所添加的两个产品是相同的types,每个产品创build时都会重新创build产品types并创build重复项。

生成的错误是:“ActiveRecord :: RecordInvalid:validation失败:名称已被占用,代码已被占用”

解决方法

此示例的解决方法是覆盖正在使用的产品types并传入特定的实例,因此只使用一个实例。 “add_product_type”提前获取并传入每个购物车项目。

 cart = Factory(:cart) prod_type = Factory(:add_product_type) #New cart.cart_items = [Factory(:cart_item, :cart => cart, :product => Factory(:added_users_product, :product_type => prod_type)), #New Factory(:cart_item, :cart => cart, :product => Factory(:added_profiles_product, :product_type => prod_type))] #New 

使用factory_girl与“pick-list”types的关联的最佳方式是什么?

我希望工厂定义包含所有的东西,而不是在testing中集合它,尽pipe我可以忍受它。

背景和额外的细节

工厂/ product.rb

 # Declare ProductTypes Factory.define :product_type do |t| t.name "None" t.code "none" end Factory.define :sub_product_type, :parent => :product_type do |t| t.name "Subscription" t.code "sub" end Factory.define :add_product_type, :parent => :product_type do |t| t.name "Additions" t.code "add" end # Declare Products Factory.define :product do |p| p.association :product_type, :factory => :add_product_type #... end Factory.define :added_profiles_product, :parent => :product do |p| p.association :product_type, :factory => :add_product_type #... end Factory.define :added_users_product, :parent => :product do |p| p.association :product_type, :factory => :add_product_type #... end 

ProductType的“代码”的目的是使应用程序可以赋予它们特殊的含义。 ProductType模型看起来像这样:

 class ProductType < ActiveRecord::Base has_many :products validates_presence_of :name, :code validates_uniqueness_of :name, :code #... end 

工厂/ cart.rb

 # Define Cart Items Factory.define :cart_item do |i| i.association :cart i.association :product, :factory => :test_product i.quantity 1 end Factory.define :cart_item_sub, :parent => :cart_item do |i| i.association :product, :factory => :year_sub_product end Factory.define :cart_item_add_profiles, :parent => :cart_item do |i| i.association :product, :factory => :add_profiles_product end # Define Carts # Define a basic cart class. No cart_items as it creates dups with lookup types. Factory.define :cart do |c| c.association :account, :factory => :trial_account end Factory.define :cart_with_two_different_items, :parent => :cart do |o| o.after_build do |cart| cart.cart_items = [Factory(:cart_item, :cart => cart, :product => Factory(:year_sub_product)), Factory(:cart_item, :cart => cart, :product => Factory(:added_profiles_product))] end end 

当我尝试用相同产品types的两个项目来定义购物车时,出现上述相同的错误。

 Factory.define :cart_with_two_add_items, :parent => :cart do |o| o.after_build do |cart| cart.cart_items = [Factory(:cart_item, :cart => cart, :product => Factory(:added_users_product)), Factory(:cart_item, :cart => cart, :product => Factory(:added_profiles_product))] end end 

我遇到了同样的问题,并在我的工厂文件的顶部添加了一个lambda,它实现了一个singleton模式,如果自上一轮testing/规范以来数据库已经被清除,它也会重新生成模型:

 saved_single_instances = {} #Find or create the model instance single_instances = lambda do |factory_key| begin saved_single_instances[factory_key].reload rescue NoMethodError, ActiveRecord::RecordNotFound #was never created (is nil) or was cleared from db saved_single_instances[factory_key] = Factory.create(factory_key) #recreate end return saved_single_instances[factory_key] end 

然后,使用示例工厂,可以使用factory_girl lazy属性来运行lambda

 Factory.define :product do |p| p.product_type { single_instances[:add_product_type] } #...this block edited as per comment below end 

瞧!

只是FYI,你也可以在你的工厂中使用initialize_withmacros,并检查对象是否已经存在,然后不要再创build它。 使用lambda(它真棒,但是!)的解决scheme正在复制已经存在于find_or_create_by中的逻辑。 这也适用于通过关联工厂创build联盟的联盟。

 FactoryGirl.define do factory :league, :aliases => [:euro_cup] do id 1 name "European Championship" rank 30 initialize_with { League.find_or_create_by_id(id)} end end 

简短的回答是,“不”,工厂女孩没有一个更干净的方式来做到这一点。 我似乎在工厂女孩论坛上validation这一点。

不过,我为自己find了另一个答案。 它涉及另一种解决方法,但使一切更清洁。

这个想法是改变表示查找表的模型,以便在缺失的情况下创build所需的条目。 这是好的,因为代码需要特定的条目存在。 这是一个修改模型的例子。

 class ProductType < ActiveRecord::Base has_many :products validates_presence_of :name, :code validates_uniqueness_of :name, :code # Constants defined for the class. CODE_FOR_SUBSCRIPTION = "sub" CODE_FOR_ADDITION = "add" # Get the ID for of the entry that represents a trial account status. def self.id_for_subscription type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_SUBSCRIPTION]) # if the type wasn't found, create it. if type.nil? type = ProductType.create!(:name => 'Subscription', :code => CODE_FOR_SUBSCRIPTION) end # Return the loaded or created ID type.id end # Get the ID for of the entry that represents a trial account status. def self.id_for_addition type = ProductType.find(:first, :conditions => ["code = ?", CODE_FOR_ADDITION]) # if the type wasn't found, create it. if type.nil? type = ProductType.create!(:name => 'Additions', :code => CODE_FOR_ADDITION) end # Return the loaded or created ID type.id end end 

“id_for_addition”的静态类方法会加载模型和ID,如果找不到,它会创build它。

缺点是“id_for_addition”方法可能不清楚它的名字。 这可能需要改变。 唯一的影响正常使用的其他代码是一个额外的testing,看看是否find模型。

这意味着用于创build产品的工厂代码可以像这样改变…

 Factory.define :added_users_product, :parent => :product do |p| #p.association :product_type, :factory => :add_product_type p.product_type_id { ProductType.id_for_addition } end 

这意味着修改后的工厂代码可能看起来像这样…

 Factory.define :cart_with_two_add_items, :parent => :cart do |o| o.after_build do |cart| cart.cart_items = [Factory(:cart_item_add_users, :cart => cart), Factory(:cart_item_add_profiles, :cart => cart)] end end 

这正是我想要的。 我现在可以清楚地expression我的工厂和testing代码。

此方法的另一个好处是查找表数据不需要在迁移中进行种子或填充。 它将处理testing数据库以及生产。

当单身人士被引入工厂时,这些问题将被消除 – 目前在 – http://github.com/roderickvd/factory_girl/tree/singletons问题; – http://github.com/thoughtbot/factory_girl/issues#issue/ 16

编辑:
在这个答案的底部看到一个更干净的解决scheme。

原文答案:
这是我创buildFactoryGirl单身人士协会的解决scheme:

 FactoryGirl.define do factory :platform do name 'Foo' end factory :platform_version do name 'Bar' platform { if Platform.find(:first).blank? FactoryGirl.create(:platform) else Platform.find(:first) end } end end 

你称它为例如:

 And the following platform versions exists: | Name | | Master | | Slave | | Replica | 

通过这种方式,所有3个平台版本将具有相同的平台“Foo”,即单身。

如果你想保存一个数据库查询,你可以做:

 platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end } 

你可以考虑使单身人士协会成为一个特质:

 factory :platform_version do name 'Bar' platform trait :singleton do platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end } end factory :singleton_platform_version, :traits => [:singleton] end 

如果您想要设置多个平台,并且拥有不同的platform_versions集,您可以创build更具体的不同特性,即:

 factory :platform_version do name 'Bar' platform trait :singleton do platform { search = Platform.find(:first) if search.blank? FactoryGirl.create(:platform) else search end } end trait :newfoo do platform { search = Platform.find_by_name('NewFoo') if search.blank? FactoryGirl.create(:platform, :name => 'NewFoo') else search end } end factory :singleton_platform_version, :traits => [:singleton] factory :newfoo_platform_version, :traits => [:newfoo] end 

希望这对一些有用的。

编辑:
在上面提交了我的原始解决scheme之后,我又给了代码看了一遍,发现了一个更清晰的方法:您不要在工厂中定义特征,而是在调用testing步骤时指定关联。

定期工厂:

 FactoryGirl.define do factory :platform do name 'Foo' end factory :platform_version do name 'Bar' platform end end 

现在你用指定的关联调用testing步骤:

 And the following platform versions exists: | Name | Platform | | Master | Name: NewFoo | | Slave | Name: NewFoo | | Replica | Name: NewFoo | 

当这样做的时候,平台“NewFoo”的创build是使用“find_or_create_by”function,所以第一个调用创build平台,接下来的两个调用find已经创build的平台。

通过这种方式,所有3个平台版本将具有相同的平台“NewFoo”,您可以根据需要创build多个平台版本。

我认为这是一个非常干净的解决scheme,因为你保持工厂的清洁,甚至让你的testing步骤读者可以看到这3个平台版本都具有相同的平台。

我也有类似的情况。 我最终使用我的seeds.rb来定义单例,然后需要spec_helper.rb中的seeds.rb将对象创build到testing数据库中。 然后,我可以在工厂中search适当的对象。

DB / seeds.rb

 RegionType.find_or_create_by_region_type('community') RegionType.find_or_create_by_region_type('province') 

投机/ spec_helper.rb

 require "#{Rails.root}/db/seeds.rb" 

投机/ factory.rb

 FactoryGirl.define do factory :region_community, class: Region do sequence(:name) { |n| "Community#{n}" } region_type { RegionType.find_by_region_type("community") } end end 

我遇到了同样的问题,我认为这是同一个引用: http : //groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc= 1& q=associations+validates_uniqueness_of#ef22581f4cd05aa9

我认为你的解决方法可能是最好的解决scheme。

也许你可以尝试使用factory_girl的序列产品types的名称和代码字段? 对于大多数testing,我猜你不会在乎产品types的代码是“code 1”还是“sub”,对于那些你关心的人,你总是可以明确地指定。

 Factory.sequence(:product_type_name) { |n| "ProductType#{n}" } Factory.sequence(:product_type_code) { |n| "prod_#{n}" } Factory.define :product_type do |t| t.name { Factory.next(:product_type_name) } t.code { Factory.next(:product_type_code) } end 

我想我至lessfind了一个更清洁的方式。

我喜欢联系ThoughtBot关于获得推荐的“官方”解决scheme的想法。 目前来看,这个效果很好。

我只是将在testing代码中完成的方法与工厂定义中的所有方法结合起来。

 Factory.define :cart_with_two_add_items, :parent => :cart do |o| o.after_build do |cart| prod_type = Factory(:add_product_type) # Define locally here and reuse below cart.cart_items = [Factory(:cart_item, :cart => cart, :product => Factory(:added_users_product, :product_type => prod_type)), Factory(:cart_item, :cart => cart, :product => Factory(:added_profiles_product, :product_type => prod_type))] end end def test_cart_with_same_item_types cart = Factory(:cart_with_two_add_items) # ... Do asserts end 

我会更新,如果我find一个更好的解决scheme。

受到这里答案的启发,我发现@Jonas Bang的build议最接近我的需求。 下面是2016年年中对我有用的东西(FactoryGirl v4.7.0,Rails 5rc1):

 FactoryGirl.define do factory :platform do name 'Foo' end factory :platform_version do name 'Bar' platform { Platform.first || create(:platform) } end end 

使用相同平台参考创build四个platform_version的示例:

 FactoryGirl.create :platform_version FactoryGirl.create :platform_version, name: 'Car' FactoryGirl.create :platform_version, name: 'Dar' => ------------------- platform_versions ------------------- name | platform ------+------------ Bar | Foo Car | Foo Dar | Foo 

如果你需要在不同的平台上使用“Dar”

 FactoryGirl.create :platform_version FactoryGirl.create :platform_version, name: 'Car' FactoryGirl.create :platform_version, name: 'Dar', platform: create(:platform, name: 'Goo') => ------------------- platform_versions ------------------- name | platform ------+------------ Bar | Foo Car | Foo Dar | Goo 

感觉像两个世界的最好的没有弯曲factory_girl太变形了。