見出し画像

[RSpec]Mock/Stub/Null Object/Spy

くぼぴー / note inc.

こんにちは。kubopです。
最近、Effective Testing with RSpec 3: Build Ruby Apps with Confidence (English Edition)という本を読んでいて学んだことがあり、一部をメモ的に記します。(英文なので意訳あり)

Understanding Test Doubles

RSpec のようなテストフレームワークではダブルが、テストダブルが映画などで「スタンドダブル」と言われるようなシステムの一部を切り離して振舞う役割を担う。

Types of Test Doubles

  • スタブ(Stubs)
    意味のある計算やI/Oを避け、定型応答を返す。

  • モック(Mocks)
    特定のメッセージを期待する。例題の終わりまでに受け取らなければエラーを発生させる。

  • Nullオブジェクト (Null Object)
    任意のオブジェクトの代用となり、任意のメッセージに応答して自身を返す。

  • スパイ(Spy)
    受け取ったメッセージを記録し、後で確認できるようにする。

※ ここでは、Gerard Meszaros が開発した語彙をベースにしています。

利用方法に加えて、テストダブルにはオリジンがあり、その基礎となるRubyクラスが何であるかを示す。(本物のRubyオブジェクトもあれば、全くのフェイクもある!

  • Pure Double
    通常のモックオブジェクト。

  • Partial Double
    既存の Ruby オブジェクトにテスト用のダブルの振る舞いをさせたもの。

  • Verifying Double
    Pure Doubleのように完全に偽物ですが、Partial Doubleのように実際のオブジェクトに基づいてそのインターフェイスを制限されている。

  • Stubbed Constant
    クラス名やモジュール名などの Ruby の定数で、ひとつのテストのために作成、削除、置換を行う。

Usage Modes: Mocks, Stubs, and Spies

# 準備/スタンドアロンで実行する。

pry(main)> require 'rspec/mocks/standalone'
=> true

Generic Test Doubles

RSpec のdoubleメソッドは、どのモードでも使用できる汎用的なテスト用の double を作成。

pry(main)> ledger = double
=> #<Double (anonymous)>

このダブルは、普通のRubyのオブジェクトと同じように振る舞います。
メッセージは指定されたもののみ受け付け可能。

pry(main)> ledger.record(an: :expense)
RSpec::Mocks::MockExpectationError: #<Double (anonymous)> received unexpected message :record with ({:an=>:expense})
from /usr/local/bundle/gems/rspec-support-3.11.0/lib/rspec/support.rb:102:in `block in <module:Support>'

Stubs

あらかじめプログラムされた、定型的なレスポンスを返す。
スタブは、値を返すが副作用を実行しないメソッドをシミュレートするときに最適。

pry(main)> http_response = double('HTTPResponse', status: 200, body: 'OK')
=> #<Double "HTTPResponse">
pry(main)> http_response.status
=> 200
pry(main)> http_response.body
=> "OK"

-- もちろんメッセージの設定は別でもOK --
pry(main)> http_response = double('HTTPResponse')
=> #<Double "HTTPResponse">
pry(main)> allow(http_response).to receive_messages(status: 200, body: 'OK')
=> {:status=>200, :body=>"OK"}

Mocks

モックは、コマンドメソッドを扱うときに便利。

  1. システムからイベントを受け取る

  2. そのイベントに基づいて判断する

  3. 副作用のあるアクションを実行する

例えば、チャットボットの返信機能。
テキストメッセージを受信し、どのように返信するかを決定し、チャットルームにメッセージを投稿することができる。
この動作をテストするためには、決まった戻り値を提供するだけでは不十分
投稿するという副作用を正しくトリガーすることを確認する必要がある。

モックオブジェクトを使うには、そのオブジェクトが受け取るべきメッセージのセット、メッセージ期待値をあらかじめ用意しておく必要がある。

宣言は通常と同じように、expectメソッドとマッチャーを組み合わせて行う。

-- のちほど --

Null Object

これまで定義してきたダブルは厳密なもので、どのようなメッセージが許されるかをあらかじめ宣言する必要があります。
しかし、テストダブルが複数のメッセージを受け取る必要がある場合は、 その都度宣言しなければならず、テストがもろくなる可能性がある。

pry(main)> null_object = double('NullObject').as_null_object 
=> #<Double "NullObject">
pry(main)> null_object.fly
=> #<Double "NullObject">

このタイプのNullオブジェクトはブラックホールと呼ばれ、送られたメッセージに応答し、常に自分自身を返す。
つまり、次から次へとメソッドコールを連鎖させることができ、好きなだけ呼び出すことができる。

Spy

Spiesは従来のフローを復元する1つの方法。
以下のクラスのcharacter.jumpが呼ばれているか調べたい場合。

class Game
  def self.play(character) 
    character.jump
  end
end
pry(main)> character = instance_spy('Character')
=> #<InstanceDouble(Character) (anonymous)>
pry(main)> Game.play(character)
=> #<InstanceDouble(Character) (anonymous)>
pry(main)> expect(character).to have_received(:jump) 
=> nil

double の場合は呼び出されるメソッドすべてを明示的にスタブする必要がありましたが、spy の場合はその必要がない。
というのも、instance_spyはas_null_objectのエイリアスのようです。(RSpec3.1から利用可能)

spy(...)                 # double(...).as_null_object と同じ
instance_spy(...) # instance_double(...).as_null_object と同じ
class_spy(...)       # class_double(...).as_null_object と同じ
object_spy(...)     # object_double(...).as_null_object と同じ

http://blog.enogineer.com/2014/09/12/rspec-3.1-release/

Origins: Pure, Partial, and Verifying Doubles

Pure Doubles

これまでに書いたダブルは、全てPure Doublesである。
これらはrspec-mocksによって専用に作成されて、指定された動作のみで構成されている。

Partial Doubles

Pure Doublesでは簡単に使用出来、依存関係のあるコードをテストするのに適しているが、現実では一部をオーバーライドしてすり替えるという方法が最適だったりする。
例えば、Time.nowやRandom.randは、それをオーバーライドする方法がない。

その場合、部分的にメソッドをオーバーライドするように見せかけてダブルを用意することが出来る。

pry(main)> random = Random.new
=> #<Random:0x000055f047ba4608>
pry(main)> allow(random).to receive(:rand).and_return(0.1234)
=> #<RSpec::Mocks::MessageExpectation #<Random:0x000055f047ba4608>.rand(any arguments)>
pry(main)> random.rand
=> 0.1234

Verifying Doubles

上記のような例では、テストダブルと、本番コードの依存関係がずれてしまうことがある。例えば、Randomのメソッドが変更されたら、削除されたら…。
この場合は、Verifying Doublesを利用し、インターフェースを制限する。Verifying Doublesを利用すると、元クラスのメソッドに変更があった場合は即座にテストが失敗する。

  • instance_double('SomeClass')
    SomeClass のインスタンスメソッドを使用して double のインターフェイスを制限する。

  • class_double('SomeClass')
    SomeClass のクラスメソッドを使用して double のインターフェイスを制限する。

  • object_double(some_object)
    クラスではなく、some_object のメソッドを使用して double のインターフェイスを制限する。(※ method_missing を使用する動的なオブジェクトに便利。

Stubbed Constants

スタブ定数を使用すると、1つの例の間、定数を別のものに置き換えることができる。

class Hoge
  CONST_EXAMPLE = 10
  ...
end
stub_const('Hoge::CONST_EXAMPLE', 1)

stub_const を使って、いろいろなことができます。

  • 新しい定数を定義する

  • 既存の定数を置き換える

  • モジュールやクラス全体を置き換える (これらも定数であるため)

  • 重いクラスの読み込みを避け、代わりに軽量な偽物を使用する

hide_const('ActiveRecord')

上記のように、利用させたくないモジュールなどを制限させることも可能。

この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
くぼぴー / note inc.
note/サーバサイドエンジニアです。 ここ最近はQA活動をしております。 哲学とかそういうのが好きです。 何か新しい考えが浮かんだら記事を書いていきたいです。