Rails 2.2プロジェクトで更新を行っています。既存のフィクスチャをファクトリに置き換えて(factory_girlを使用)、いくつかの問題が発生しました。問題は、ルックアップデータ。同じ製品タイプを持つ2つの製品でカートを作成すると、作成された各製品は同じ製品タイプを再作成します。これは、ProductTypeモデルでの一意の検証によるエラーです。
これは、カートを作成してバラバラにまとめる単体テストからのものです。私は問題を回避するためにこれをしなければなりませんでした。しかし、これはまだ問題を示しています。説明します。
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))]
追加される2つの製品は同じタイプであり、各製品が作成されると、製品タイプが再作成され、重複が作成されます。
生成されるエラーは次のとおりです:「ActiveRecord :: RecordInvalid:検証に失敗しました:名前はすでに取得されています、コードはすでに取得されています」
この例の回避策は、使用されている製品タイプをオーバーライドし、特定のインスタンスを渡して、1つのインスタンスのみが使用されるようにすることです。 「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を使用する最良の方法は何ですか?
私はlikeテストでアセンブルする代わりに、ファクトリ定義にすべてを含めるようにしますが、それを使用することはできます。
factorys/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
factorys/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
同じ商品タイプの2つのアイテムでカートを定義しようとすると、上記と同じエラーが発生します。
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
同じ問題が発生し、ファクトリファイルの先頭にシングルトンパターンを実装するラムダを追加しました。これにより、最後のテスト/仕様以降にデータベースがクリアされた場合にもモデルが再生成されます。
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_girllazy属性を使用してラムダを実行できます。
Factory.define :product do |p|
p.product_type { single_instances[:add_product_type] }
#...this block edited as per comment below
end
出来上がり!
参考までに、 initialize_with
ファクトリ内のマクロで、オブジェクトがすでに存在するかどうかを確認してから、再度作成しないでください。ラムダを使用するソリューション(素晴らしいですが!)は、find_or_create_byにすでに存在するロジックを複製することです。これは、関連するファクトリを介して:leagueが作成されているアソシエーションでも機能します。
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
私も同じような状況でした。最終的に、seeds.rbを使用してシングルトンを定義し、spec_helper.rbのseeds.rbにオブジェクトをテストデータベースに作成するように要求しました。次に、工場内の適切なオブジェクトを検索できます。
db/seeds.rb
RegionType.find_or_create_by_region_type('community')
RegionType.find_or_create_by_region_type('province')
spec/spec_helper.rb
require "#{Rails.root}/db/seeds.rb"
spec/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://github.com/roderickvd/factory_girl/tree/singletons 問題-- http:// github .com/thinkbot/factory_girl/issues#issue/16
編集:
この回答の下部にあるさらにクリーンなソリューションを参照してください。
元の回答:
これは、FactoryGirlシングルトンアソシエーションを作成するための私のソリューションです。
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のセットが異なる場合は、より具体的な異なる特性を作成できます。
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
これが他の人に役立つことを願っています。
編集:
上記の元のソリューションを送信した後、コードに別の外観を与え、これを行うさらにクリーンな方法を見つけました。ファクトリで特性を定義するのではなく、テストステップを呼び出すときに関連付けを指定します。
通常の工場を作る:
FactoryGirl.define do
factory :platform do
name 'Foo'
end
factory :platform_version do
name 'Bar'
platform
end
end
ここで、指定された関連付けを使用してテストステップを呼び出します。
And the following platform versions exists:
| Name | Platform |
| Master | Name: NewFoo |
| Slave | Name: NewFoo |
| Replica | Name: NewFoo |
このように行う場合、プラットフォーム「NewFoo」の作成では「find_or_create_by」機能を使用するため、最初の呼び出しでプラットフォームが作成され、次の2回の呼び出しですでに作成されているプラットフォームが検索されます。
このようにして、3つのプラットフォームバージョンすべてに同じプラットフォーム「NewFoo」があり、必要な数のプラットフォームバージョンのセットを作成できます。
これは非常にクリーンなソリューションだと思います。工場をクリーンに保ち、テスト手順の読者に、これら3つのプラットフォームバージョンがすべて同じプラットフォームであることがわかるようにするためです。
簡単に言えば、「いいえ」です。ファクトリーガールにはそれを行うためのよりクリーンな方法がありません。ファクトリーガールフォーラムでこれを確認したようです。
しかし、私は自分自身のために別の答えを見つけました。これには別の種類の回避策が含まれますが、すべてがはるかにクリーンになります。
ルックアップテーブルを表すモデルを変更して、不足している場合は必要なエントリを作成するという考え方です。コードは特定のエントリが存在することを想定しているため、これは問題ありません。変更されたモデルの例を次に示します。
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をロードし、見つからなかった場合はモデルを作成します。
欠点は、「id_for_addition」メソッドがその名前で何をするのか明確でない可能性があることです。それは変更する必要があるかもしれません。通常の使用に対する他の唯一のコードの影響は、モデルが見つかったかどうかを確認するための追加のテストです。
これは、製品を作成するためのファクトリコードを次のように変更できることを意味します...
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
これはまさに私が欲しかったものです。これで、ファクトリとテストコードをきれいに表現できます。
このアプローチのもう1つの利点は、移行時にルックアップテーブルデータをシードしたり入力したりする必要がないことです。それは、テストデータベースと本番用にそれ自体を処理します。
私はこれと同じ問題を抱えていました、そしてそれはここで参照されているものと同じだと思います: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1&q=associations+ validates_uniqueness_of#ef22581f4cd05aa9
私はあなたの回避策がおそらく問題の最良の解決策だと思います。
私は少なくとももっときれいな方法を見つけたと思います。
推奨される「公式」ソリューションの入手についてThoughtBotに連絡するというアイデアが好きです。今のところ、これはうまく機能します。
テストのコードでそれを行うアプローチと、すべてをファクトリ定義で行うアプローチを組み合わせただけです。
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
より良い解決策が見つかったら更新します。
ここでの回答に触発されて、@ JonasBangからの提案が私のニーズに最も近いことがわかりました。 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
これを使用して、同じプラットフォーム参照で4つの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を曲げすぎずに、両方の長所のように感じます。
たぶん、製品タイプ名とコードフィールドにfactory_girlのシーケンスを使用してみることができますか?ほとんどのテストでは、製品タイプのコードが「コード1」であるか「サブ」であるかは気にしないと思います。気になるテストでは、いつでも明示的に指定できます。
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