読者です 読者をやめる 読者になる 読者になる

Tbpgr Blog

Ruby プログラマ tbpgr(てぃーびー) のブログ

テストダブル(スタブ、モック、スパイ)とサンプルコード

Test

テストダブル(スタブ、モック、スパイ)とサンプルコードについてまとめます。 この記事は既出情報です。個人メモ的なものなのであまり役にたたないと思います。

テストダブル(Test Double)とは?

テストダブルはテストの代役である。

テスト用に本物ではない代役のオブジェクトを利用する。
以下にテストダブルの種類をあげます。

f:id:tbpg:20161212222253p:plain

テストスタブ(Test Stub)とは?

f:id:tbpg:20161212222259p:plain

テストスタブとはテストの呼び出しに対してあらかじめ用意された結果を返す手法である。
そのままではテスト時に取扱いにくい内容をStub out(もみ消す)ために利用される。
DBや外部サービスとの連携などが典型的な例。

状態をテストする。
スタブを利用する側がテストをしたい場所になる。

モックオブジェクト(Mock Object)とは?

f:id:tbpg:20161212222305p:plain

モックオブジェクトとは期待値の一連の呼び出し仕様を表した手法である。

振る舞いをテストする。
モックの利用箇所そのものがテストになる。

テストスパイ(Test Spy)とは?

f:id:tbpg:20161212222309p:plain

スパイは間接的な出力を検証するために、出力を記録しておく手法である。
記録を保持するが検証は行わない点がモックと異なる。

フェイクオブジェクト(Fake Object)とは?

f:id:tbpg:20161212222314p:plain

フェイクオブジェクトとは実物よりも単純な実装を使う手法である。
DBに対するインメモリデータベースが典型例。

ダミーオブジェクト(Dummy Object)とは?

f:id:tbpg:20161212222318p:plain

利用しないパラメータに対して数合わせのためなどに受け渡されるオブジェクト。
動きさえすれば値はなんでもいい。

相互の関係

  • モックオブジェクトはスタブの機能を含む
  • スパイはスタブの機能を含む
  • スタブはその他は状態の検証を行う。
  • モックオブジェクトはテストダブルの中で唯一振る舞いの検証を行う

スタブ・モック・スパイのサンプルコード

RSpec のサンプルコードです

テスト対象

require 'sample/version'
require 'pp'

class FizzBuzz
  def fizzbuzz(limit, printer)
    printer.start
    ret = init_range.init(limit).each_with_object([]) do |e, memo|
      printer.loop
      memo << case
      when e % 15 == 0 then 'FizzBuzz'
      when e % 5 == 0 then 'Buzz'
      when e % 3 == 0 then 'Fizz'
      else e.to_s
      end
    end.join(',')
    printer.end
    ret
  end

  def init_range
    Limit
  end
end

class Limit
  def self.init(limit)
    (1..limit)
  end
end

class Printer
  def start
    print "start"
  end

  def loop
    print "loop"
  end

  def end
    print "end"
  end
end

テストコード

require 'spec_helper'
require 'sample'

describe FizzBuzz do
  it 'no test double' do
    expect(subject.fizzbuzz(15, Printer.new)).to eq('1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz')
  end

  it 'use test stub' do
    # ループ範囲に利用している Range をスタブに差し替えてみます
    limit5_mock = double("Limit 5")
    allow(limit5_mock).to receive(:init).and_return((1..5))
    allow(subject).to receive(:init_range).and_return(limit5_mock)
    expect(subject.fizzbuzz(15, Printer.new)).to eq('1,2,Fizz,4,Buzz')
  end

  it 'use mock object' do
    # ループ範囲に利用している Range をモックに差し替えてみます
    limit3_mock = double("Limit 3")
    # initが引数 15 で呼び出されていることを確認しつつ (1..3) を返却します
    expect(limit3_mock).to receive(:init).with(15).and_return((1..3))
    expect(subject).to receive(:init_range).and_return(limit3_mock)
    expect(subject.fizzbuzz(15, Printer.new)).to eq('1,2,Fizz')
  end

  it 'use spy object' do
    # 処理開始時、処理中、処理終了時にテキストを出力する機能をスパイに置き換えます
    printer = spy("Printer")
    expect(subject.fizzbuzz(3, printer)).to eq('1,2,Fizz')
    # 処理開始時に呼び出されていることを確認
    expect(printer).to have_received(:start)
    # 処理中に3回呼び出されていることを確認
    expect(printer).to have_received(:loop).exactly(3).times
    # 処理終了時に呼び出されていることを確認
    expect(printer).to have_received(:end)
  end
end

関連資料