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
など
)DataMapper
ROM
- スタブ/モッキング ライブラリ (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#build
とActiveRecord::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_double
object_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_many
。new_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
モデルごとに手動で追加する必要があります。