スタブとモックの違い
オブジェクト指向設計実践入門を読んで学んだことのまとめです。
具体的にRspecでモックを書くときはこうしましょう、といった具体的な話ではなく言葉の意味の説明がメインです。
ソフトウェアテストの対象
スタブもモックもテストコード内で使うものです。違いを考える前に、テストについて振り返ってみます。
テストを行うべきなのは、次の2つについてです。
-
オブジェクトがほかのオブジェクトからメッセージを受け取ったとき、期待する答えを返すことができるか
- 要するにパブリックメソッドに対するユニットテスト
- 「オブジェクト指向設計実践ガイド」には、プライベートメソッドに対するテストは書くべきではない、さらにいうとプライベートメソッドを書くべきではない(プライベートメソッドを他のオブジェクトに切り出して注入せよ)とあります。
-
オブジェクトが副作用のあるメッセージ送信を行うとき、その回数や引数が適切か
- ログを書く、とかネットワークアクセスを行うといった副作用が伴う場合は回数も検証しないといけません。「メッセージをTwitterに投稿する」メソッドを使う場合、このメソッドが想定していない形で何回も繰り返し呼ばれるようなことはあってはなりません。
ここでは、「オブジェクト指向設計実践ガイド」にならい前者を「受信メッセージのテスト」、後者を「送信メッセージのテスト」と呼ぶことにします。
スタブとモックの違い
では本題に戻りましょう。スタブとモックの最大の違いは、スタブは「受信メッセージのテスト」のために使うためのもので、モックは「送信メッセージのテスト」のために使うものであるという点です。
また、スタブ自体はテストを円滑にすすめるための周辺ツールという位置づけですが、モックはテストそのものの一部といってよいでしょう。
スタブ
スタブは、受信メッセージのテストに使います。たとえば、次のような場合を考えてみます。
hogeというメソッドは、引数として受け取ったオブジェクトの#some_number
を呼び出してその結果に1を加えて返すという仕様です。これが正しく実装されているかのテストを書きたいと思っています。
require 'minitest/autorun'
MiniTest.autorun
def hoge(some_object)
some_object.some_number + 1
end
class TestHoge < MiniTest::Test
# hogeの挙動をテストしたい
end
しかし、このメソッドはsome_object
に依存しているため、テストが成功するかどうかは hoge
メソッドの実装だけでなく何が来るかわからない some_object
の実装にも依存してしまいます。
some_object.some_number
はきっと他のところでテストされているはずですし、その正しさまで検証するのは test_hoge
の責務ではありません。なので、今書こうとしているテストコードでは hoge
メソッドの正しさだけに注目すべきです。そのために some_object
としてテストのために決まりきった「正しい」挙動をするオブジェクトを注入します。すると、テスト内でsome_object
は正しい動きをするとわかっているわけですから、もしテストが失敗したとしたら「他のどこか」ではなく hoge
メソッドの実装に問題があるとわかるわけです。
このように、テストを書くときは他のオブジェクトの実装に依存しないテストを作るべきです。このために使うが「スタブ」です。
require 'minitest/autorun'
MiniTest.autorun
def hoge(some_object)
some_object.some_number + 1
end
class TestHoge < MiniTest::Test
StubbedObject = Struct.new(:some_number)
def setup
@stubbed_object = StubbedObject.new(1)
end
def test_hoge
assert_equal 2, hoge(@stubbed_object)
end
end
雑に言うとスタブはテストで注目しているオブジェクトが依存するものを、決まりきった動きしかしない偽物に置き換え、テストの合否が注目しているオブジェクト実装の正しさだけに依存するようにすることです。
スタブはテストフレームワークの便利機能を使わなくても比較的かんたんに作ることができます。
モック
モックは送信メッセージのテストに使います。
今回は自分の処理の内容をログに書き出すようになっているあるメソッドをテストします。ログですから当然形式が仕様と異なっていたり、そもそもデータが間違っていたりされると困ります。
require 'minitest/autorun'
MiniTest.autorun
def hoge(logger, message)
res = message.upcase
logger.log("converted #{message} -> #{res}")
res
end
class TestHoge < MiniTest::Test
# hogeが一回呼ばれたら、logger#logが正しい引数で呼ばれることをテストしたい
end
モックの方は、引数の検証や回数の検証といった機能が必要になってきます。なのでテストフレームワークの機能を使わずに自分で作るのは難しいでしょう。
Minitestを使っている場合は、次のようになります。(Minitestのモックはメソッド呼び出しの回数までは検証できないようです…)
require 'minitest/autorun'
MiniTest.autorun
def hoge(logger, message)
res = message.upcase
logger.log("converted #{message} -> #{res}")
res
end
class TestHoge < MiniTest::Test
def setup
@logger = MiniTest::Mock.new.expect(:log, nil, args=['converted aaa -> AAA'])
end
def test_hoge
hoge(@logger, 'aaa')
@logger.verify
end
end
↑を実行すると問題ないですが
require 'minitest/autorun'
MiniTest.autorun
def hoge(logger, message)
res = message.upcase
logger.log("conberted #{message} -> #{res}")
res
end
class TestHoge < MiniTest::Test
def setup
@logger = MiniTest::Mock.new.expect(:log, nil, args=['converted aaa -> AAA'])
end
def test_hoge
hoge(@logger, 'aaa')
@logger.verify
end
end
のほうはエラーになります。モックに対して想定されていない引数で log
メソッドが呼ばれてしまっていることを表します。
失敗している方のコードをよく見ると、hoge
メソッド内にlogger.log("conberted #{message} -> #{res}")
とconvert
のタイプミスが見つかりました。
まとめ
ソフトウェアテストは、
- 受信メッセージのテスト: オブジェクトがメッセージを受け取ったとき適切な返事をするか
- 送信メッセージのテスト: オブジェクトが副作用のあるメッセージを送信するとき、適切な引数・回数で送信しているか
を検証します。
受信メッセージのテストをするときは注目しているオブジェクト以外のオブジェクトは偽物を注入し、テストの成功不成功が注目オブジェクトの実装のみに依存するようにする。これをスタブという。
送信メッセージのテストをするときはメッセージの受け手を偽物にすり替えておき、この偽物にメッセージの引数や呼び出し回数が想定通りか検証させる。これをモックという。
スタブはテストそれ自体とは無関係だが、モックはテストの一部である。