記事が読みやすくなるように、記事の趣旨は末尾に記載しています。
1.用語を整理する
モック周りで出てくる用語を、RSpecにおける使われ方に合わせて整理する。
用語 | 説明 | 利用方法 |
---|---|---|
ダブル | テスト用のダミーオブジェクトのこと。初期状態では何のメソッドも呼び出すことができないシンプルなオブジェクト | doubleメソッドを呼び出す。 |
スパイ | テスト用のダミーオブジェクトのこと。初期状態でどんなメソッドでも呼び出すことが可能。ただし、メソッドの返却値は自分自身となる。 | spyメソッドを呼び出す。 |
スタブ | 決まりきった挙動をするように設定されたオブジェクトのこと。 | オブジェクトに対してstubメソッドを呼び出す。 |
モック | 挙動に対して検証が行われるオブジェクトのこと。 | オブジェクトに対してexpectメソッドを呼び出す。 |
スタブとモックの利用用途の違い
用語 | 利用用途 |
---|---|
スタブ | 「テスト対象のオブジェクトだけに注目するために、テスト対象以外のオブジェクトをスタブにする」といった使われ方をする。 |
モック | 「テスト対象のオブジェクトがある処理の中で他のオブジェクトに与える副作用を検証するために、その『他のオブジェクト』をモックにする」といった使われ方をする。 |
2.ダブル
スパイとは違い、明示的に定義していないメソッドは呼び出しできない。
[ダブル]検証機能のないダブル
# ※識別子をオプションとして渡す(なくてもOK)
book = double("Book")
[ダブル]検証機能のあるダブル
# 下記のインスタンスメソッドを呼び出すことができないダブル
# ・引数で指定したクラスに存在しない
# ・引数で指定したクラスに存在するが、引数の数が異なる
book = instance_double("Book", :pages => 250)
# 下記のクラスメソッドを呼び出すことができないダブル
# ・引数で指定したクラスに存在しない
# ・引数で指定したクラスに存在するが、引数の数が異なる
book = class_double("Book", :pages => 250)
# 下記のメソッドを呼び出すことができないダブル
# ・引数で指定したインスタンスに存在しない
# ・引数で指定したインスタンスに存在するが、引数の数が異なる
book = object_double(Book.new, :pages => 250)
[ダブル]どんなメソッドの呼び出しも許容する
book = double("Book").as_null_object
3.スパイ
ダブルとは違い、明示的に定義していないメソッドも呼び出し可能。
[スパイ]検証機能のないスパイ
# `double("invitation").as_null_object`と同じ
spy("invitation")
[スパイ]検証機能のあるスパイ
# `instance_double("Invitation").as_null_object`と同じ
instance_spy("Invitation")
# `class_double("Invitation").as_null_object`と同じ
class_spy("Invitation")
# `object_double("Invitation").as_null_object`と同じ
object_spy("Invitation")
4.スタブ
book = double("Book")
allow(book).to receive(:title) { "The RSpec Book" }
allow(book).to receive(:title).and_return("The RSpec Book")
# book.title => "The RSpec Book"
allow(book).to receive_messages(
:title => "The RSpec Book",
:subtitle => "Behaviour-Driven Development with RSpec, Cucumber, and Friends"
)
# book.title => "The RSpec Book"
# book.subtitle => "Behaviour-Driven Development with RSpec, Cucumber, and Friends"
[スタブ]ダブルとスタブを同時に定義する
book = double("book", :title => "The RSpec Book")
# book.title => "The RSpec Book"
[スタブ]呼び出し回数に応じて戻り値を変更する
# 1回目の呼び出し => value1を返す
# 2回目の呼び出し => value2を返す
# 3回目の呼び出し => value3を返す
allow(double).to receive(:msg).and_return(value1, value2, value3)
[スタブ]メソッドを呼び出すと例外が発生するようにする
# `error`には下記のどちらかを設定する
# ・インスタンスオブジェクト(e.g. `StandardError.new(some_arg)`)
# ・クラス(e.g. `StandardError`)
# クラスの場合は、引数なしでインスタンス化可能でないといけない
allow(double).to receive(:msg).and_raise(error)
[スタブ]メソッドを呼び出すとthrowされるようにする
allow(double).to receive(:msg).and_throw(:msg)
cf. https://docs.ruby-lang.org/ja/latest/method/Kernel/m/throw.html
[スタブ]メソッドのブロック引数を指定する
and_yieldを複数回指定すると、ブロックが複数回呼び出される。
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)
[スタブ]メソッドチェーンを定義する
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
[スタブ]連続する戻り値を返させる
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_any_instance_of(Widget).to receive(:name).and_return("Wibble")
5.モック
# validatorに対してvalidateが呼び出されればテスト成功
validator = double("validator")
expect(validator).to receive(:validate) { "02134" }
zipcode = Zipcode.new("02134", validator)
zipcode.valid?
# validatorに対してvalidateが呼び出されればテスト成功
validator = double("validator")
expect(validator).to receive(:validate).and_return("02134")
zipcode = Zipcode.new("02134", validator)
zipcode.valid?
[モック]呼び出し回数に応じて戻り値を変更する
# 1回目の呼び出し => value1を返す
# 2回目の呼び出し => value2を返す
# 3回目の呼び出し => value3を返す
expect(double).to receive(:msg).exactly(3).times.and_return(value1, value2, value3)
[モック]メソッドを呼び出すと例外が発生するようにする
# `error`には下記のどちらかを設定する
# ・インスタンスオブジェクト(e.g. `StandardError.new(some_arg)`)
# ・クラス(e.g. `StandardError`)
# クラスの場合は、引数なしでインスタンス化可能でないといけない
expect(double).to receive(:msg).and_raise(error)
[モック]メソッドを呼び出すとthrowされるようにする
expect(double).to receive(:msg).and_throw(:msg)
[モック]メソッドのブロック引数を指定する
and_yieldを複数回指定すると、ブロックが複数回呼び出される。
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)
[モック]呼び出し回数も検証する
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
[モック]引数も検証する
# msgメソッドが下記の引数で呼び出されればテスト成功
# "A", 1, 3
expect(double).to receive(:msg).with("A", 1, 3)
# 「引数がNumericクラスであることを検証」といったように
# 引数を抽象的に確認したい場合は下記のようにする
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).ordered
expect(double).to receive(:other_msg).ordered
# 下記の場合、同じメソッドの異なる引数での呼び出しの順番を検証できる
expect(double).to receive(:msg).with("A", 1, 3).ordered
expect(double).to receive(:msg).with("B", 2, 4).ordered
[モック]オリジナルの実行に移譲する
expect(Person).to receive(:find).and_call_original
Person.find # => オリジナルのfindメソッドを実行し、結果を返す
[モック]クラスの全てのインスタンスにモックを設定する
expect_any_instance_of(Widget).to receive(:name).and_return("Wobble")
趣旨
以前に、RSpec MocksのREADMEを和訳しました。
和訳することで、読みたいときに英語脳を利用せずに楽に読めるようになりました。
ただ、和訳しただけではREADMEに書いてあることにパパッとたどり着くことはできません。
そこで、本記事ではRSpec MocksのREADMEの和訳結果をチートシートとしてまとめ直します。
※READMEの内容以外の内容も適宜追加しています