web-dev-qa-db-ja.com

has_many関連を持つFactoryGirl build_stubbed戦略

2つのオブジェクト間の標準のhas_many関係を考えます。簡単な例として、次のようにします。

class Order < ActiveRecord::Base
  has_many :line_items
end

class LineItem < ActiveRecord::Base
  belongs_to :order
end

私がやりたいのは、スタブ化されたラインアイテムのリストを使ってスタブ化された注文を生成することです。

FactoryGirl.define do
  factory :line_item do
    name 'An Item'
    quantity 1
  end
end

FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      order.line_items = build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
    end
  end
end

上記のコードは機能しません。Railsは、line_itemsが割り当てられ、FactoryGirlが例外を発生させるときに、注文に対してsaveを呼び出す必要があるためです:RuntimeError: stubbed models are not allowed to access the database

それで、has_mayコレクションがスタブ化されているスタブ化オブジェクトをどのように(またはそれを可能に)生成するのでしょうか。

36
Jared

TL; DR

FactoryGirlは、「スタブ」オブジェクトを作成するときに非常に大きな仮定を行うことで、役立つようにしています。つまり、それは あなたはidを持っています。これはあなたが新しいレコードではないため、すでに永続化されていることを意味します!

残念ながら、ActiveRecordはこれを使用して 永続性を最新に保つ かどうかを決定します。したがって、スタブモデルはレコードをデータベースに永続化しようとします。

しないでくださいFactoryGirlファクトリにRSpecスタブ/モックをシムしてみてください。そうすることで、同じオブジェクトに2つの異なるスタブ哲学が混在します。どちらかを選択してください。

RSpecモックは、スペックライフサイクルの特定の部分でのみ使用されることになっています。それらを工場に移すと、設計違反を隠す環境が整います。これに起因するエラーは、わかりにくく、追跡するのが困難です。

RSpecを言う test/unit に含めるためのドキュメントを見ると、モックが適切にセットアップされ、テスト間で破棄されることを保証するためのメソッドが提供されていることがわかります。工場にモックを入れても、これが行われるという保証はありません。

ここにはいくつかのオプションがあります:

  • スタブの作成にFactoryGirlを使用しないでください。スタブライブラリ(rspec-mocks、minitest/mocks、mocha、flexmock、rrなど)を使用する

    モデル属性ロジックをFactoryGirlに保持したい場合は問題ありません。その目的で使用し、別の場所にスタブを作成します。

    stub_data = attributes_for(:order)
    stub_data[:line_items] = Array.new(5){
      double(LineItem, attributes_for(:line_item))
    }
    order_stub = double(Order, stub_data)
    

    はい、手動で関連付けを作成する必要があります。これは悪いことではありません。詳細については、以下を参照してください。

  • idフィールドをクリアする

    after(:stub) do |order, evaluator|
      order.id = nil
      order.line_items = build_stubbed_list(
        :line_item,
        evaluator.line_items_count,
        order: order
      )
    end
    
  • new_record?の独自の定義を作成する

    factory :order do
      ignore do
        line_items_count 1
        new_record true
      end
    
      after(:stub) do |order, evaluator|
        order.define_singleton_method(:new_record?) do
          evaluator.new_record
        end
        order.line_items = build_stubbed_list(
          :line_item,
          evaluator.line_items_count,
          order: order
        )
      end
    end
    

何が起きてる?

IMO、 "[スタブ]" has_manyFactoryGirlの関連付けを作成することは、一般的にはお勧めできません。これは、より密に結合されたコードにつながり、不必要に多くのネストされたオブジェクトが作成される可能性があります。

この位置、およびFactoryGirlで何が起こっているかを理解するには、いくつかのことを確認する必要があります。

  • データベース永続化レイヤー/ gem(つまり、ActiveRecordMongoidDataMapperROMなど)
  • すべてのスタブ/モックライブラリ(mintest/mocks、rspec、mochaなど)
  • モック/スタブが役立つ目的

データベース永続性レイヤー

各データベース永続化レイヤーの動作は異なります。実際、多くはメジャーバージョン間で異なる動作をします。 FactoryGirl 試行は、そのレイヤーのセットアップ方法を想定しないようにします。これにより、長期にわたって最も柔軟性が高まります。

仮定:この説明の残りの部分ではActiveRecordを使用していると思います。

これを書いている時点では、現在のGA ActiveRecordのバージョンは4.1.0です。それにhas_manyアソシエーションをセットアップすると、 あり]alotthatgoeson

これは、古いARバージョンでも少し異なります。 Mongoidなどでは非常に異なります。FactoryGirlがこれらすべてのgemの複雑さやバージョン間の違いを理解することを期待するのは合理的ではありません。 has_manyアソシエーションのライター永続性を最新に保つ を試みるのは、たまたま起こります。

あなたは考えているかもしれません:「しかし、私はスタブで逆を設定できます」

FactoryGirl.define do
  factory :line_item do
    association :order, factory: :order, strategy: :stub
  end
end

li = build_stubbed(:line_item)

はい、そうです。それは単にARが しないpersist を決定したからです。この動作は良いことです。そうしないと、データベースに頻繁にアクセスせずに一時オブジェクトをセットアップすることが非常に困難になります。さらに、1つのトランザクションで複数のオブジェクトを保存できるため、問題が発生した場合はトランザクション全体をロールバックできます。

今、あなたは考えているかもしれません:「データベースにアクセスすることなく、has_manyにオブジェクトを完全に追加できます」

order = Order.new
li = order.line_items.build(name: 'test')
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 1

li = LineItem.new(name: 'bar')
order.line_items << li
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 2

li = LineItem.new(name: 'foo')
order.line_items.concat(li)
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 3

order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 5

はい、しかしここではorder.line_itemsは実際には ActiveRecord::Associations::CollectionProxy です。独自の build#<< 、および #concat メソッドを定義しています。もちろん、これらすべては、定義された関連付けにすべて委譲します。これは、has_manyの場合は同等のメソッドです ActiveRecord::Associations::CollectionAssocation#build および ActiveRecord::Associations::CollectionAssocation#concat 。これらは、現在持続するか後で持続するかを決定するために、ベースモデルインスタンスの現在の状態を考慮に入れます。

ここでFactoryGirlが実際に実行できることは、基礎となるクラスの動作に何が起こるかを定義させることだけです。実際、これにより、FactoryGirlを使用してデータベースモデルだけでなく 任意のクラスを生成 することができます。

FactoryGirlはオブジェクトの保存を少し手助けしようとします。これは主に工場のcreate側にあります。 ActiveRecordとの相互作用 のWikiページによると:

... [ファクトリ]は最初に関連付けを保存して、外部キーが依存モデルに適切に設定されるようにします。インスタンスを作成するには、引数なしでnewを呼び出し、各属性(関連付けを含む)を割り当ててから、save!を呼び出します。 factory_girlは、ActiveRecordインスタンスを作成するために特別なことは何もしません。データベースとやり取りしたり、ActiveRecordやモデルを拡張したりすることはありません。

待つ!お気づきかもしれませんが、上記の例では、次の点に注意しました。

order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 5

はい、そうです。 order.line_items=を配列に設定しても、永続化されません!だから何を与えるのですか?

ライブラリのスタブ/モッキング

多くの異なるタイプがあり、FactoryGirlはそれらすべてで機能します。どうして? FactoryGirlはそれらのいずれに対しても何もしないからです。それはあなたがどのライブラリを持っているかを完全に認識していません。

選択したテストライブラリ にFactoryGirl構文を追加することを忘れないでください。ライブラリをFactoryGirlに追加しません。

では、FactoryGirlが優先ライブラリを使用していない場合、何をしているのでしょうか?

目的モック/スタブサービス

ボンネットの詳細に入る前に、 whata"stub"is を定義する必要があります。 目的

スタブは、テスト中に行われた呼び出しに対して定型の応答を提供します。通常、テスト用にプログラムされたもの以外ではまったく応答しません。スタブは、「送信」されたメッセージを記憶する電子メールゲートウェイスタブや、「送信」されたメッセージの数だけを記録するなど、呼び出しに関する情報を記録する場合もあります。

これは「モック」とは微妙に異なります。

Mocks...:期待して事前にプログラムされたオブジェクトは、それらが受け取ると予想される呼び出しの仕様を形成します。

スタブは、返信定型文を使用して共同編集者を設定する方法として機能します。特定のテストのためにタッチするコラボレーターのパブリックAPIのみに固執することで、スタブが軽量で小さく保たれます。

「スタブ」ライブラリがなくても、独自のスタブを簡単に作成できます。

stubbed_object = Object.new
stubbed_object.define_singleton_method(:name) { 'Stubbly' }
stubbed_object.define_singleton_method(:quantity) { 123 }

stubbed_object.name       # => 'Stubbly'
stubbed_object.quantity   # => 123

FactoryGirlは「スタブ」に関しては完全にライブラリにとらわれないので、これは 彼らが取るアプローチ です。

FactoryGirl v.4.4.0の実装を見ると、build_stubbedを実行すると、次のメソッドがすべてスタブされていることがわかります。

  • persisted?
  • new_record?
  • save
  • destroy
  • connection
  • reload
  • update_attribute
  • update_column
  • created_at

これらはすべて非常にActiveRecord-yです。ただし、has_manyで見たように、これはかなり漏れやすい抽象化です。 ActiveRecordパブリックAPIの表面積は非常に大きいです。ライブラリがそれを完全にカバーすることを期待することは正確に合理的ではありません。

has_many関連付けがFactoryGirlスタブで機能しないのはなぜですか?

上記のように、ActiveRecordは状態をチェックして 永続性を最新に保つ かどうかを決定します。 new_record? のスタブ定義により、has_manyを設定すると、データベースアクションがトリガーされます。

def new_record?
  id.nil?
end

修正を破棄する前に、stubの定義に戻りたいと思います。

スタブは、テスト中に行われた呼び出しに対する定型の回答を提供します。通常、テスト用にプログラムされているもの以外にはまったく応答しません。スタブは、「送信」されたメッセージを記憶する電子メールゲートウェイスタブや、「送信」されたメッセージの数だけを記録するなど、呼び出しに関する情報を記録する場合もあります。

スタブのFactoryGirl実装はこの原則に違反しています。テストや仕様で何をしようとしているのかわからないので、データベースへのアクセスを阻止しようとするだけです。

修正#1:FactoryGirlを使用してスタブを作成しない

スタブを作成/使用する場合は、そのタスク専用のライブラリを使用してください。既にRSpecを使用しているようですので、double機能を使用してください(そして、新しい検証 instance_doubleclass_double として、同様に object_double RSpecの3)。または、Mocha、Flexmock、RRなどを使用します。

独自の非常にシンプルなスタブファクトリをロールすることもできます(これには問題があります。これは、返信定型文を含むオブジェクトを作成する簡単な方法の例にすぎません)。

require 'ostruct'
def create_stub(stubbed_attributes)
  OpenStruct.new(stubbed_attributes)
end

FactoryGirlを使用すると、本当に必要な場合に100個のモデルオブジェクトを非常に簡単に作成できます。常に大きな力が来ると、責任が生まれます。深くネストされたアソシエーションを見過ごすのは非常に簡単です。これらは実際にはスタブに属していません。

さらに、お気づきのように、FactoryGirlの「スタブ」抽象化は少しリークが多く、その実装とデータベース永続化レイヤーの内部の両方を理解する必要があります。スタブlibを使用すると、この依存関係から完全に解放されます。

モデル属性ロジックをFactoryGirlに保持したい場合は問題ありません。その目的で使用し、別の場所にスタブを作成します。

stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
  double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)

はい、手動で関連付けを設定する必要があります。ただし、テスト/仕様に必要な関連付けのみをセットアップします。必要のない他の5つは取得しません。

これは、実際のスタブlibを使用することで、明確にするのに役立ちます。これは、設計の選択に関するフィードバックを提供するテスト/仕様です。このような設定で、仕様の読者は質問をすることができます:「なぜ5つのラインアイテムが必要なのですか?」 それが仕様にとって重要であるなら、それはすぐそこにあり、明白です。そうでなければ、それはそこにあるべきではありません。

同じことは、単一のオブジェクトと呼ばれる長い一連のメソッド、または後続のオブジェクトの一連のメソッドにも当てはまります。 demeterの法則 はあなたを助けるためのものであり、あなたを妨げるものではありません。

修正#2:idフィールドをクリアする

これはもっとハックです。デフォルトのスタブはidを設定することがわかっています。したがって、単に削除します。

after(:stub) do |order, evaluator|
  order.id = nil
  order.line_items = build_stubbed_list(
    :line_item,
    evaluator.line_items_count,
    order: order
  )
end

idを返し、かつhas_manyアソシエーションをセットアップするスタブを作成することはできません。 FactoryGirlセットアップでのnew_record?の定義は、これを完全に防ぎます。

修正#3:new_record?の独自の定義を作成する

ここでは、idの概念を、スタブがnew_record?である場所から分離します。これをモジュールにプッシュして、他の場所で再利用できるようにします。

module SettableNewRecord
  def new_record?
    @new_record
  end

  def new_record=(state)
    @new_record = !!state
  end
end

factory :order do
  ignore do
    line_items_count 1
    new_record true
  end

  after(:stub) do |order, evaluator|
    order.singleton_class.prepend(SettableNewRecord)
    order.new_record = evaluator.new_record
    order.line_items = build_stubbed_list(
      :line_item,
      evaluator.line_items_count,
      order: order
    )
  end
end

それでも、モデルごとに手動で追加する必要があります。

116
Aaron K

私はこの答えが浮かんでいるのを見ましたが、あなたが持っていたのと同じ問題に遭遇しました: FactoryGirl:Populate a have many relations preserving build strategy

私が見つけた最もクリーンな方法は、関連付けの呼び出しも明示的にスタブ化することです。

require 'rspec/mocks/standalone'

FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      order.stub(line_items).and_return(FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order))
    end
  end
end

お役に立てば幸いです。

12
Bryce

ブライスのソリューションが最もエレガントであることがわかりましたが、新しいallow()構文に関する非推奨警告が表示されます。

新しい(よりクリーンな)構文を使用するために、私はこれを行いました:

2014年6月5日更新:私の最初の命題はプライベートAPIメソッドの使用でした。AaraonKのおかげでより優れたソリューションが得られました。詳細についてはコメントを読んでください

#spec/support/initializers/factory_girl.rb
...
#this line enables us to use allow() method in factories
FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods)
...

 #spec/factories/order_factory.rb
...
FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      items = FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
      allow(order).to receive(:line_items).and_return(items)
    end
  end
end
...
1
systho