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コレクションがスタブ化されているスタブ化オブジェクトをどのように(またはそれを可能に)生成するのでしょうか。
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_many
とFactoryGirl
の関連付けを作成することは、一般的にはお勧めできません。これは、より密に結合されたコードにつながり、不必要に多くのネストされたオブジェクトが作成される可能性があります。
この位置、およびFactoryGirlで何が起こっているかを理解するには、いくつかのことを確認する必要があります。
ActiveRecord
、Mongoid
、DataMapper
、ROM
など)各データベース永続化レイヤーの動作は異なります。実際、多くはメジャーバージョン間で異なる動作をします。 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実装はこの原則に違反しています。テストや仕様で何をしようとしているのかわからないので、データベースへのアクセスを阻止しようとするだけです。
スタブを作成/使用する場合は、そのタスク専用のライブラリを使用してください。既にRSpecを使用しているようですので、double
機能を使用してください(そして、新しい検証 instance_double
、 class_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の法則 はあなたを助けるためのものであり、あなたを妨げるものではありません。
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?
の定義は、これを完全に防ぎます。
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
それでも、モデルごとに手動で追加する必要があります。
私はこの答えが浮かんでいるのを見ましたが、あなたが持っていたのと同じ問題に遭遇しました: 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
お役に立てば幸いです。
ブライスのソリューションが最もエレガントであることがわかりましたが、新しい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
...