スタブとモックの違い

Qiitaにも同じのを投稿しました

スタブとモックの違い

オブジェクト指向設計実践入門を読んで学んだことのまとめです。 具体的に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
  # hogeが一回呼ばれたら、#logが正しい引数で呼ばれることをテストしたい
  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のタイプミスが見つかりました。

まとめ

ソフトウェアテストは、

  • 受信メッセージのテスト: オブジェクトがメッセージを受け取ったとき適切な返事をするか
  • 送信メッセージのテスト: オブジェクトが副作用のあるメッセージを送信するとき、適切な引数・回数で送信しているか

を検証します。

受信メッセージのテストをするときは注目しているオブジェクト以外のオブジェクトは偽物を注入し、テストの成功不成功が注目オブジェクトの実装のみに依存するようにする。これをスタブという。

送信メッセージのテストをするときはメッセージの受け手を偽物にすり替えておき、この偽物にメッセージの引数や呼び出し回数が想定通りか検証させる。これをモックという。

スタブはテストそれ自体とは無関係だが、モックはテストの一部である。