翻訳する目的
Everyday Rails - RSpecによるRailsテスト入門を読み、モック関連と、FactoryBotについて更に知識をつけたいと思いました。どちらも技術本で学ぶのは難しいので、リポジトリのREADMEを通じて学ぶのが良いと考えました。
Google翻訳やDeepLを用いて全文翻訳を行うと、日本語が不自然で読みにくかったので、今後何度も参照することを考えて、自分で翻訳することにしました。
FactoryBotのREADMEの翻訳は既に存在していたので、RSpec MocksのREADMEを翻訳することにしました。
翻訳時のルール
- Google翻訳やDeepLは利用しない。
- わからない単語を辞書で調べるのはOK。
翻訳してみてどうだったか?
良い点
- 英語のドキュメントを読む練習になる。
- ドキュメントを細部までしっかり読む経験ができる。わからなくても投げ出さない経験ができる。
- 他の人が参照しても参考になる可能性が高い。
悪い点
- 時間がかかる。(本記事で5~6時間程度)
ここから下が翻訳した内容です。
RSpec Mocks
rspec-mocksはRSpec向けのテストダブルのフレームワークであり、テストダブルや本物のオブジェクトにおけるメソッドのスタブやフェイク、message expectationをサポートしています。
インストール
gem install rspec # rspec-core, rspec-expectations, rspec-mocks
gem install rspec-mocks # rspec-mocksだけ
main
ブランチに対して実行しますか?依存するRSpecのリポジトリも含める必要があります。次のようにGemfile
に追加してください。
%w[rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib|
gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'main'
end
コントリビューティング
環境をセットアップした後には、作業を行いたいリポジトリのディレクトリに移動する必要があります。移動先において、スペックやCucmberのテストを実行したり、パッチを作成することができます。
NOTE: 任意のRSpecリポジトリで作業する際、rspec-devを利用する必要はありません。それぞれのRSpecリポジトリを独立したプロジェクトとして扱うことができます。
RSpecへのコントリビューションについての情報は、下記のmarkdownファイルを参照してください。
テストダブル
テストダブルは、RSpecのexampleにおいて皆さんのシステムの全く別のオブジェクトの代役となるオブジェクトです。テストダブルを作成するにはdouble
メソッドを利用してください。引数には識別子をオプションとして渡します。
book = double("book")
大抵の場合、作成したダブルがシステム上に既に存在するオブジェクトとそっくりであるかが気になるはずです。そのために、検証付きダブルが提供されています。もし既存のオブジェクトが利用可能な状態であれば、既存のオブジェクトには存在しない、もしくは引数の数が不適切なメソッドを追加することはできません。
book = instance_double("Book", :pages => 250)
検証付きダブルには巧妙なトリックが利用されています。本物のオブジェクトと同じようにバリデーションを行える一方で、依存関係とは隔離された環境でテストを行うことができます。詳細はドキュメントで確認できます。
検証付きダブルもdouble()の場合と同様に、カスタム識別子を渡すことができます。下記が例です。
books = []
books << instance_double("Book", :rspec_book, :pages => 250)
books << instance_double("Book", "(Untitled)", :pages => 5000)
puts books.inspect # 名前が付けられていれば、どれが追加されたのかがわかりやすいです。
メソッドのスタブ
メソッドのスタブとは、予め決められた値を返却することです。スタブはテストダブルに対しても、本物のオブジェクトに対しても同じ文法を用いて定義することができます。rspec-mocksはスタブを定義するための3つの方法をサポートしています。
allow(book).to receive(:title) { "The RSpec Book" }
allow(book).to receive(:title).and_return("The RSpec Book")
allow(book).to receive_messages(
:title => "The RSpec Book",
:subtitle => "Behaviour-Driven Development with RSpec, Cucumber, and Friends")
また、下記のようなショートカットを利用することもできます。1つの宣言でダブルを作成しつつ、スタブを定義しています。
book = double("book", :title => "The RSpec Book")
第一引数は名前であり、ドキュメンテーションやテスト失敗時のメッセージに利用されます。もし、名前付けが不要である場合は、それを取り除いて、インスタンスとスタブの定義を簡潔にすることができます。
double(:foo => 'bar')
この書き方は、ダブルのリストを生成し、そのリストをイテレートさせるメソッドに渡したい場合に特に有効です。
order.calculate_total_price(double(:price => 1.99), double(:price => 2.99))
メソッドチェーンをスタブする
receive
を利用していた箇所でreceive_message_chain
を使うと、メッセージのチェーンをスタブすることができます。
allow(double).to receive_message_chain("foo.bar") { :baz }
allow(double).to receive_message_chain(:foo, :bar => :baz)
allow(double).to receive_message_chain(:foo, :bar) { :baz }
# 上記のいずれかの形式の場合
double.foo.bar # => :baz
チェーンは任意の長さにすることができるため、デメテルの法則に違反しやすいです。そのため、コードスメルとなるreceive_message_chain
の利用には注意が必要です。fluent interfacesをイメージしてください。全てのコードスメルが問題の存在を指し示す訳ではありませんが、それでもreceive_message_chain
は脆いテストケースの原因となってしまうでしょう。例えば、スペック内でallow(foo).to receive_message_chain(:bar, :baz => 37)
と記述し、実行時にfoo.baz.bar
を呼び出すと、このスタブは動作しません。
連続する戻り値
スタブが1回以上呼び出される場合、and_return
にさらに引数を渡すことができます。呼び出される度にリストを循環し、さらに後続の呼び出しに対しては最後の値を返し続けます。
allow(die).to receive(:roll).and_return(1, 2, 3)
die.roll # => 1
die.roll # => 2
die.roll # => 3
die.roll # => 3
die.roll # => 3
一度の呼び出しに対して配列を返したい場合は、配列を定義してください。
allow(team).to receive(:players).and_return([double(:name => "David")])
Message Expectations
message expectationはexampleが終了するまでの間にダブルがメッセージを受け取ることをテストします。メッセージを受ければテストは成功であり、受け取らなければテストは失敗です。
validator = double("validator")
expect(validator).to receive(:validate) { "02134" }
zipcode = Zipcode.new("02134", validator)
zipcode.valid?
テストスパイ
オブジェクトがテストを実行する過程で期待しているメッセージを受けとることを検証します。メッセージを検証するためには、オブジェクトがスパイになるために明示的にスタブされる、もしくはnullオブジェクトのダブル(e.g. double(...).as_null_object
)になる必要があります。そのために、nullオブジェクトのダブルを簡単に作成するメソッドが提供されています。
spy("invitation") # => `double("invitation").as_null_object`と同じ
instance_spy("Invitation") # => `instance_double("Invitation").as_null_object`と同じ
class_spy("Invitation") # => `class_double("Invitation").as_null_object`と同じ
object_spy("Invitation") # => `object_double("Invitation").as_null_object`と同じ
この方法で受け取ったメッセージを検証すると、テストスパイのパターンを実装することができます。
invitation = spy('invitation')
user.accept_invitation(invitation)
expect(invitation).to have_received(:accept)
# 他の一般的なメッセージエクスペクテーションも利用できます。下記が例です。
expect(invitation).to have_received(:accept).with(mailer)
expect(invitation).to have_received(:accept).twice
expect(invitation).to_not have_received(:accept).with(mailer)
# ダブルの場合と同じように戻り値を指定することができます。
invitation = spy('invitation', :accept => true)
expect(invitation).to have_received(:accept).with(mailer)
expect(invitation.accept).to eq(true)
スパイにメッセージが渡された後に、その引数が変更された場合、have_received(...).with(...)
は正しく動作しないことに注意してください。例えば、下記は正しく動作しません。
greeter = spy("greeter")
message = "Hello"
greeter.greet_with(message)
message << ", World"
expect(greeter).to have_received(:greet_with).with("Hello")
命名法
モックオブジェクトとテストスタブ
モックオブジェクトとテストスタブという名前は特別なテストダブルを表します。例えば、テストスタブはメソッドのスタブのみを扱うテストダブルであり、モックオブジェクトはmessage expectationとメソッドのスタブの両方を扱うテストダブルです。
命名法には重複するものがたくさんあり、その重複パターン(フェイクやスパイなど)にも多くのバリエーションがあります。大抵の場合、メソッドのスタブやmessage expectationsのバリエーションについてのメソッドレベルの概念について扱っていること、そしてそれらを汎用オブジェクトであるテストダブルに当てはめていることを覚えておいてください。
Test-Specific Extension
Test-Specific Extensionはテストのコンテキストにおいてテストダブルのような振る舞いとなるように実装された、本物のオブジェクトの拡張のことであり、別名、Partial Doubleと呼ばれます。クラスオブジェクトがメソッドのグローバル名前空間として機能することがよくあるため、この手法はRubyではとても一般的です。
person = double("person")
allow(Person).to receive(:find) { person }
このケースでは、Personがfind
メッセージを受け取ると定義したpersonオブジェクトを必ず返すように実装しています。また、もしfind
が呼び出されなかった場合にexampleが失敗するようにmessage expectationを設定することもできます。
person = double("person")
expect(Person).to receive(:find) { person }
RSpecはスタブ、もしくはモックしたメソッドをtest-double-likeなメソッドに置き換えます。exampleの最後には、RSpecはmessage expectationを検証し、その後に元のメソッドを復元します。
引数を期待する
expect(double).to receive(:msg).with(*args)
expect(double).to_not receive(:msg).with(*args)
また、同じmessageに対して複数のexpectationを設定することも可能です。
expect(double).to receive(:msg).with("A", 1, 3)
expect(double).to receive(:msg).with("B", 2, 4)
引数のマッチャー
with
に渡された引数は実際に渡された引数と===によって比較されます。もし、引数そのものよりも、引数に関するモノを指定したい場合、 rspec-expectationsに含まれるマッチャーを利用することができます。それらは全てが文法的に意味をなすわけではありません(それらはRSpec::Expectationsで使用することを第一に設計されています)。しかし、独自のRSpec::Matchersを自由に作成することができます。
expect(double).to receive(:msg).with(no_args)
expect(double).to receive(:msg).with(any_args)
expect(double).to receive(:msg).with(1, any_args) # any argsはsplat演算子を利用した引数のように振る舞う
expect(double).to receive(:msg).with(1, kind_of(Numeric), "b") # 第2引数はNumericクラスのあらゆる値になりうる
expect(double).to receive(:msg).with(1, boolean(), "b") # 第2引数はtrueもしくはfalseになりうる2nd argument can be true or false
expect(double).to receive(:msg).with(1, /abc/, "b") # 第2引数は正規表現にマッチするStringクラスのあらゆる値になりうる
expect(double).to receive(:msg).with(1, anything(), "b") # 第2引数はどんな値にもなりうる
expect(double).to receive(:msg).with(1, duck_type(:abs, :div), "b") # 第2引数はabsとdivに応答するオブジェクトになりうる
expect(double).to receive(:msg).with(hash_including(:a => 5)) # 第1引数はkey-valuesの1つがa: 5であるハッシュ
expect(double).to receive(:msg).with(array_including(5)) # 第1引数はkey-valuesの1つが5である配列
expect(double).to receive(:msg).with(hash_excluding(:a => 5)) # 第1引数はkey-valuesにa: 5を含まないハッシュ
受け取る回数
expect(double).to receive(:msg).once
expect(double).to receive(:msg).twice
expect(double).to receive(:msg).exactly(n).time
expect(double).to receive(:msg).exactly(n).times
expect(double).to receive(:msg).at_least(:once)
expect(double).to receive(:msg).at_least(:twice)
expect(double).to receive(:msg).at_least(n).time
expect(double).to receive(:msg).at_least(n).times
expect(double).to receive(:msg).at_most(:once)
expect(double).to receive(:msg).at_most(:twice)
expect(double).to receive(:msg).at_most(n).time
expect(double).to receive(:msg).at_most(n).times
順番
expect(double).to receive(:msg).ordered
expect(double).to receive(:other_msg).ordered
# messageを順番通りに受け取らないと、これは失敗します。
これは異なる引数の同じmessageを含めることもできます。
expect(double).to receive(:msg).with("A", 1, 3).ordered
expect(double).to receive(:msg).with("B", 2, 4).ordered
レスポンスを設定する
message expectationもしくはスタブを設定する場合、オブジェクトにどのようにレスポンスすべきかを正確に伝えることができます。最も汎用的な方法はreceive
にブロックを渡すことです。
expect(double).to receive(:msg) { value }
ダブルがmsg
というmessageを受け取った場合、ブロックが評価され、結果が返されます。
expect(double).to receive(:msg).and_return(value)
expect(double).to receive(:msg).exactly(3).times.and_return(value1, value2, value3)
# value1を最初に返し、value2を2番目に返す、など
expect(double).to receive(:msg).and_raise(error)
# `error`はインスタンスオブジェクト(e.g. `StandardError.new(some_arg)`)、もしくはクラス(e.g. `StandardError`)になりうる
# クラスの場合は、引数なしでインスタンス化可能でないといけない
expect(double).to receive(:msg).and_throw(:msg)
expect(double).to receive(:msg).and_yield(values, to, yield)
expect(double).to receive(:msg).and_yield(values, to, yield).and_yield(some, other, values, this, time)
# ブロックに対して複数回yieldされるメソッドに対して
これらのレスポンスはスタブに対しても同様に適用できます。
allow(double).to receive(:msg).and_return(value)
allow(double).to receive(:msg).and_return(value1, value2, value3)
allow(double).to receive(:msg).and_raise(error)
allow(double).to receive(:msg).and_throw(:msg)
allow(double).to receive(:msg).and_yield(values, to, yield)
allow(double).to receive(:msg).and_yield(values, to, yield).and_yield(some, other, values, this, time)
恣意的に扱う
解決したい問題を利用可能なexpectationで解決できないこともあると思います。特定の長さの配列が含まれるmessageを期待しているが、その配列の内容については意識しないことをイメージしてください。下記のようにすることができます。
expect(double).to receive(:msg) do |arg|
expect(arg.size).to eq 7
end
もし、スタブされたメソッドがブロックを受け取る場合、特別な方法でyieldする必要があります。下記のようにできます。
expect(double).to receive(:msg) do |&arg|
begin
arg.call
ensure
# cleanup
end
end
オリジナルの実行に移譲する
部分的なモックオブジェクトを利用する場合、メッセージに対してどのように応答するかに干渉することなくmessage expectationを設定したいことがあると思います。これを実現するためにand_call_original
を利用することができます。
expect(Person).to receive(:find).and_call_original
Person.find # => オリジナルのfindメソッドを実行し、結果を返す
expectationの詳細を連結する
message名を特定の引数、受け取る回数、レスポンスと連結させることで、expectationにおいて詳細を得ることができます。
expect(double).to receive(:<<).with("illegal value").once.and_raise(ArgumentError)
これは必要な場面では良いですが、おそらく実際には必要ないでしょう!あなたのコードの振る舞いにおいて重要なことだけを指定するように気をつけましょう。
定数をスタブし、隠す
この機能の情報は可変定数のREADMEを確認してください。
before(:context)
ではなく、before(:example)
を利用してください
before(:context)
内におけるスタブはサポートされていません。なぜなら、全てのスタブやモックはそれぞれのexampleの後に消去されるので、before(:context)
で設定されたスタブはグループの中で偶然実行された最初のexampleで機能し、他のexampleでは機能しないからです。
before(:context)
の代わりに、before(:example)
を利用してください。
あるクラスのどんなインスタンスにもモック、もしくはスタブを設定する
rspec-mocksはあるクラスのどんなインスタンスでもスタブ、モックするためにallow_any_instance_of
とexpect_any_instance_of
という2つのメソッドを提供しています。それらはallow
やexpect
の位置で利用されます。
allow_any_instance_of(Widget).to receive(:name).and_return("Wibble")
expect_any_instance_of(Widget).to receive(:name).and_return("Wobble")
これらのメソッドはWidget
の全てのインスタンスに適切なスタブ、もしくはexpectationを追加します。
この機能はレガシーコードでは便利に機能することもありますが、一般的にはいくつかの理由で利用することを推奨しません。
-
rspec-mocks
のAPIは個別のインスタンスオブジェクト向けに設計されています。しかし、この機能はオブジェクトのクラス全体を扱います。結果として、意味的に混乱させるようなエッジケースが存在しています。例えば、expect_any_instance_of(Widget).to receive(:name).twice
において、特定のインスタンスがそれぞれname
を2回受け取ることを期待しているのか、それとも合計で2回受け取ることを期待しているのかがわかりにくいです。(この場合は前者です) - この機能を利用は、しばしばデザインのスメルになります。それは、テストが過度に多くのことをテストしようとしているか、テスト対象のオブジェクトが過度に複雑である可能性があるということです。
- これは
rspec-mocks
において最も複雑な機能であり、歴史的に最もバグ報告を受け取っています。(コアチームも誰も、この役に立たない機能を積極的に利用していません)
参考文献
モックやスタブの意味について、様々な異なる観点があります。もし、更に学ぶことに興味があるのであれば、下記がオススメの記事です。
- Mock Objects: http://www.mockobjects.com/
- Endo-Testing: http://www.ccs.neu.edu/research/demeter/related-work/extreme-programming/MockObjectsFinal.PDF
- Mock Roles, Not Objects: http://www.jmock.org/oopsla2004.pdf
- Test Double: http://www.martinfowler.com/bliki/TestDouble.html
- Test Double Patterns: http://xunitpatterns.com/Test%20Double%20Patterns.html
- Mocks aren't stubs: http://www.martinfowler.com/articles/mocksArentStubs.html