Ruby で Stub と Mock を使ったテスト

Written by @dr_taka_n at 2011/02/21 14:05 []

テストの無いプログラムは、クリープの無いコーヒー・・・。いや、趣向の問題ではない、おっかなくて使う気になれない。

ここではテストを行う際に、実施条件下で変わる情報、外部リソースを扱う場合などに有効な Stub と Mock の簡単な使い方をメモしておく。

Ruby では、Stub と Mock を扱う場合の選択肢はいろいろあり、また、いろいろ熱すぎて何を選択していいのかわからなくなる。。

他にも RSpec、Mocha などあるが、ここでは、FlexMock を使うことを前提とする。

RubyGems でインストールできるので、インストールしておく。

$ sudo gem install flexmock

のスライドなどは、使い方のサンプルとしても参考になる。

Stub と Mock とテスト対象サンプルプログラム

過去、

でもメモを残したことはあるが、時々会話をしていて、Stub と Mock を混同して話をされている方がいる。Stub と Mock は同義ではない。

Stub は、ある手続きから返される結果だけを担保しており、Mock はその結果が返される手続きも含めて担保するものである。

テスト対象となる簡単なスクリプトを考えてみたが、以下のあまり意味のないレポートを生成するプログラムをテスト対象プログラムのサンプルとした。

class SenselessReport
  def initialize(title)
    @title = title
    @data = []
  end

  def text(str = nil)
    return @data.join("\n") unless str
    @data << str
  end
  alias_method :text=, :text

  def create_report
    header = "#{@title}\n#{'-' * @title.size}"
    body = @data.join("\n") + "\n"
    footer = "作成日時: #{Time.now.strftime('%Y/%m/%d %H:%M:%S')}\n"
    [header, body, footer].join("\n")
  end

  def report(options = {})
    options = { :out => $stdout }.merge(options)
    options[:out].puts create_report
  end

  def self.generate(title, *args, &block)
    report = self.new(title)
    report.instance_eval(&block)
    report.report(args.last ||= {})
  end
end

使い方は、以下のような感じになる。

report = SenselessReport.new("03/01 日報")
report.text "今日は一杯仕事したはず。"
report.text "成果もまずまずのはず。"
report.report

# >> 03/01 日報
# >> ------------
# >> 今日は一杯仕事したはず。
# >> 成果もまずまずのはず。
# >> 
# >> 作成日時: 2011/02/21 01:08:56

次のような書き方もできる。

SenselessReport.generate("03/01 日報") do
  text "今日は一杯仕事したはず。"
  text "成果もまずまずのはず。"
end

# >> 03/01 日報
# >> ------------
# >> 今日は一杯仕事したはず。
# >> 成果もまずまずのはず。
# >> 
# >> 作成日時: 2011/02/21 01:10:32

実行時のタイムスタンプとなる「作成日時」以外、出力結果は全く同じとなる。

Stub (スタブ)

まずは、Stub のサンプル。

サンプルプログラムで SenselessReport#create_report の返す文字列の確認を行おうとした。 この文字列には、Time#now により「作成日時」として、SenselessReport#create_report 動作時のタイムスタンプが埋め込まれる。テストを行う度に変わる文字列になる。

Time#now をスタブ化する。

FlexMock などのライブラリを使わずに、動的な言語特性を利用して Time.now をそのまま上書いてしまう方法もありかもしれないが、FlexMock では、スタブ用途でメソッドを上書いて偽装したとしても、その適用範囲はそのテストメソッドだけで、テストメソッドが終了したところでその後始末(偽装を元通りに)をちゃんとやってくれるので便利だ。

まずは、期待する結果の文字列を仕込んでおく。

require 'test/unit'

require 'rubygems'
require 'flexmock/test_unit'

require 'senseless_report'

class SenselessReportTest < Test::Unit::TestCase

  def setup
    @exptected =<<EOS
03/01 日報
------------
今日は一杯仕事したはず。
成果もまずまずのはず。

作成日時: 2011/02/21 01:02:34
EOS
  end
end

上記のように、作成日時は、 “2011/02/21 01:02:34” になることを期待している。Time#now がこのテストメソッドの中ではいつ実行されても 2011/02/21 01:02:34 を返すようにスタブ化する。

flexmock(Time, :now => Time.local(2011, 2, 21, 1, 2, 34))

テストを仕上げる。

class SenselessReportTest < Test::Unit::TestCase

  def setup
    @exptected =<<EOS
03/01 日報
------------
今日は一杯仕事したはず。
成果もまずまずのはず。

作成日時: 2011/02/21 01:02:34
EOS
  end

  def test_create_report_text
    flexmock(Time, :now => Time.local(2011, 2, 21, 1, 2, 34))

    report = SenselessReport.new("03/01 日報")
    report.text "今日は一杯仕事したはず。"
    report.text "成果もまずまずのはず。"

    assert_equal(@exptected, report.create_report)
  end
end

実行する。

$ ruby test_senseless_report_sample1.rb 
Loaded suite test_senseless_report_sample1
Started
.
Finished in 0.001127 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

OK だ。

Mock (モック)

このサンプルプログラムでは、デフォルトでは、レポートの出力は標準出力に行うようにしている。 ファイルなどに出力したい場合には、SenselessReport#report の引数オプションにファイルへの I/O オブジェクトを渡すことで出力先を変えることができる。

引数オプションに指定した I/O オブジェクトを正しく使ってプログラムが動いていることを確認したい。

Mock を使うことにする。

output = flexmock("output")
output.should_receive(:puts).with(@exptected).once

I/O オブジェクトの Mock である output が、@expected という文字列を引数に puts メソッドが一度だけ呼び出されること、を定義している。

この定義通りにプログラム側が動いていなければ、テストは失敗する。

偽装した output の I/O オブジェクトもどきを引数のオプションに渡したテストを仕上げる。

  def test_use_option_io
    output = flexmock("output")
    output.should_receive(:puts).with(@exptected).once
    stub_time_now

    SenselessReport.generate("03/01 日報", { :out => output }) do
      text "今日は一杯仕事したはず。"
      text "成果もまずまずのはず。"
    end
  end

  def stub_time_now
    flexmock(Time, :now => Time.local(2011, 2, 21, 1, 2, 34))
  end

実行する。

$ ruby test_senseless_report_sample1.rb 
Loaded suite test_senseless_report_sample1
Started
..
Finished in 0.002015 seconds.

2 tests, 1 assertions, 0 failures, 0 errors

OK だ。

と、自分でサンプルを用意していて何なのだが、上記のテスト結果を見てもわかるように、今回の例で書いた Mock を使用したテストでは、明示的に assertion を書いていない。

確認(テスト)したいことはテストしているので、このケースのテストの場合はこれでもよいように思っているのだが、どうなんだろう。

blog comments powered by Disqus