テストの無いプログラムは、クリープの無いコーヒー・・・。いや、趣向の問題ではない、おっかなくて使う気になれない。
ここではテストを行う際に、実施条件下で変わる情報、外部リソースを扱う場合などに有効な Stub と Mock の簡単な使い方をメモしておく。
Ruby では、Stub と Mock を扱う場合の選択肢はいろいろあり、また、いろいろ熱すぎて何を選択していいのかわからなくなる。。
他にも RSpec、Mocha などあるが、ここでは、FlexMock を使うことを前提とする。
RubyGems でインストールできるので、インストールしておく。
$ sudo gem install flexmock
のスライドなどは、使い方のサンプルとしても参考になる。
Table of Contents
Open Table of Contents
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 を書いていない。
確認(テスト)したいことはテストしているので、このケースのテストの場合はこれでもよいように思っているのだが、どうなんだろう。