40

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

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

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

4

3 に答える 3

126

TL;DR

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

残念ながら、ActiveRecord はこれを使用して、永続性を最新の状態に保つ必要があるかどうかを判断 します。そのため、スタブ化されたモデルはレコードをデータベースに永続化しようとします。

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

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

RSpec を say 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_many 関連付けを作成しようとすることはお勧めできませんFactoryGirl。これにより、コードがより緊密に結合され、多くのネストされたオブジェクトが不必要に作成される可能性があります。

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

  • データベース永続層/gem (つまりActiveRecord、、、、Mongoidなど )DataMapperROM
  • スタブ/モッキング ライブラリ (mintest/mocks、rspec、mocha など)
  • モック/スタブの目的

データベース永続層

各データベース永続層は異なる動作をします。実際、多くはメジャー バージョン間で動作が異なります。FactoryGirl、そのレイヤーがどのように設定されているかについて仮定を立てないようにしています。これにより、長期にわたって最大限の柔軟性が得られます。

前提:ActiveRecordこの議論の残りの部分で使用していると思います。

これを書いている時点で、 の現在の GA バージョンActiveRecordは 4.1.0 です。has_manyそれに関連付け を設定すると多く ことが進行ます。

これも、古い AR バージョンでは若干異なります。Mongoid などでは大きく異なります。FactoryGirl がこれらすべての宝石の複雑さやバージョン間の違いを理解することを期待するのは合理的ではありません。たまたま、has_many協会のライターが永続性を最新の状態に保と うとしています。

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

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

li = build_stubbed(:line_item)

ええ、それは本当です。単純に、ARが存続しないことにしたから です。この振る舞いは良いことであることがわかりました。そうしないと、データベースに頻繁にアクセスせずに一時オブジェクトをセットアップすることが非常に困難になります。さらに、複数のオブジェクトを 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#buildActiveRecord::Associations::CollectionAssocation#concat. これらは、現在保持するか後で保持するかを決定するために、基本モデル インスタンスの現在の状態を考慮に入れます。

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

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

...[a factory] ​​は最初に関連付けを保存し、依存モデルに外部キーが適切に設定されるようにします。インスタンスを作成するには、引数なしで 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 が好みのライブラリを使用していない場合、それは何をしているのでしょうか?

モック/スタブの目的

内部の詳細に入る前に、 「スタブ」とは何か およびその意図された目的を定義する必要があります。

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

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

モック...: 受け取ることが期待される呼び出しの仕様を形成する期待値で事前にプログラムされたオブジェクト。

スタブは、返信定型文を使用してコラボレーターをセットアップする方法として機能します。特定のテストのために触れる共同作業者のパブリック 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 に非常によく似ています。ただし、 で見たようにhas_many、これはかなり漏れやすい抽象化です。ActiveRecord パブリック API の表面積は非常に大きいです。ライブラリがそれを完全にカバーすることを期待するのは、まったく合理的ではありません。

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

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

def new_record?
  id.nil?
end

いくつかの修正を行う前に、 a の定義に戻りたいと思いますstub

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

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

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

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

独自の非常に単純なスタブ ファクトリを作成することもできます (もちろん、これには問題があります。これは、既定の応答を使用してオブジェクトを作成する簡単な方法の例にすぎません)。

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

FactoryGirl を使用すると、本当に必要なときに 100 個のモデル オブジェクトを非常に簡単に作成できます。確かに、これは責任ある使用の問題です。いつものように、大きな力が責任を生み出します。実際にはスタブに属さない、深くネストされたアソシエーションを見落とすのは非常に簡単です。

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

モデル属性ロジックを 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 つのものは取得できません。

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

同じことが、単一のオブジェクトと呼ばれるメソッドの長いチェーン、または後続のオブジェクトのメソッドのチェーンにも当てはまります。おそらく停止する時が来ました。デメテルの 法則はあなたを助けるものであり、邪魔するものではありません。

修正 #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

ANDを返すスタブで関連付けidを設定することはできませんhas_manynew_record?その FactoryGirl セットアップの定義は、これを完全に防ぎます。

修正 #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

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

于 2014-04-25T04:03:54.493 に答える
1

Bryce のソリューションが最も洗練されていることがわかりましたが、新しいallow()構文に関する非推奨の警告が生成されます。

新しい(よりクリーンな)構文を使用するために、次のようにしました。

UPDATE 06/05/2014 : 私の最初の提案は、プライベート API メソッドを使用することでした。より優れたソリューションを提供してくれた Aaraon K に感謝します。詳細についてはコメントをお読みください。

#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
...
于 2014-04-24T11:29:21.960 に答える