42

私は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 つの製品は同じタイプであり、各製品が作成されると、製品タイプが再作成され、重複が作成されます。

生成されるエラーは次のとおりです。

回避策

この例の回避策は、使用されている製品タイプをオーバーライドし、特定のインスタンスを渡して、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 を使用する最良の方法は何ですか?

テストでアセンブルするのではなく、ファクトリ定義にすべてを含めたいと思いますが、それでも問題ありません。

背景と詳細

工場/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

同じ商品タイプの 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
4

10 に答える 10

48

参考までに、ファクトリ内でマクロを使用して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
于 2012-07-10T07:23:20.957 に答える
31

私は同じ問題に遭遇し、テスト/仕様の最後のラウンド以降にデータベースがクリアされた場合にもモデルを再生成するシングルトン パターンを実装する工場ファイルの先頭にラムダを追加しました。

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 遅延属性を使用してラムダを実行できます

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

出来上がり!

于 2010-08-25T18:27:08.967 に答える
3

編集:
この回答の下部にあるさらにクリーンなソリューションを参照してください。

元の回答:
これは、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'、つまりシングルトンを持ちます。

db クエリを保存したい場合は、代わりに次のようにします。

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 つのプラットフォーム バージョンがすべて同じプラットフォームであることが見えるようにすることさえできるからです。

于 2011-12-01T14:44:46.170 に答える
2

簡単に言えば、「いいえ」です。ファクトリーガールにはそれを行うためのよりクリーンな方法がありません。私はファクトリーガールフォーラムでこれを確認したようです。

しかし、私は自分自身のために別の答えを見つけました。これには別の種類の回避策が含まれますが、すべてがはるかにクリーンになります。

ルックアップテーブルを表すモデルを変更して、欠落している場合は必要なエントリを作成するという考え方です。コードは特定のエントリが存在することを想定しているため、これは問題ありません。変更されたモデルの例を次に示します。

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つの利点は、移行時にルックアップテーブルデータをシードしたり入力したりする必要がないことです。それは、テストデータベースと本番用にそれ自体を処理します。

于 2010-01-13T17:37:31.170 に答える
2

これらの問題は、シングルトンが工場に導入されたときに解消されます-現在-http ://github.com/roderickvd/factory_girl/tree/singletons Issue- http: //github.com/thoughtbot/factory_girl/issues#issue/ 16

于 2010-01-19T19:34:45.327 に答える
2

私も似たような状況でした。シングルトンを定義するために自分の seed.rb を使用することになり、その後、spec_helper.rb 内の seed.rb を使用してオブジェクトをテスト データベースに作成する必要がありました。その後、工場内の適切なオブジェクトを検索できます。

デシベル/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"

仕様/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
于 2012-03-04T19:26:24.693 に答える
1

私はこれと同じ問題を抱えており、ここで参照されているものと同じだと思います: http://groups.google.com/group/factory_girl/browse_frm/thread/68947290d1819952/ef22581f4cd05aa9?tvc=1& q=associations+validates_uniqueness_of#ef22581f4cd05aa9

私はあなたの回避策がおそらく問題の最良の解決策だと思います。

于 2010-01-07T04:57:18.217 に答える
0

私は少なくとももっときれいな方法を見つけたと思います。

推奨される「公式」ソリューションの入手について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

より良い解決策が見つかったら更新します。

于 2010-01-07T16:05:01.703 に答える
0

factory_girl のシーケンスを製品タイプ名とコード フィールドに使用してみてはいかがでしょうか。ほとんどのテストでは、製品タイプのコードが「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 
于 2010-01-13T23:55:53.453 に答える