実用的なプログラムの開発の中でUnit Test(UT)を作成しようとするとすぐに、「テストしたいオブジェクトとは別のオブジェクトがじゃまになってテストが書けない。」という問題にぶつかります。この問題の解決にはいくつか定石がありますので、基本的な3つの定石を紹介します。例はRubyで書いていますが、他の言語の場合も考え方は変わりません。
ちょっと無理やりな例ですが、現在時刻の時(hour)の部分だけを12時間表記の文字列にするクラスCurrentHourPrinterを作ってみます。
ファイルCurrentHourPrinter.rb
---------------------------------------------------------------------------------------------------
require "Time"
# 現在時刻を12時間表示するクラス
class CurrentHourPrinter
# 現在時刻の時の部分を12時間表記の文字列にする
def now()
toString(Time.now())
end
# 例を作る都合上、privateに指定し、このメソッドはテストできないことにする。
private
def toString(time)
return time.strftime("%-I %P").sub("am", "a.m.").sub("pm", "p.m.")
end
end
------------------------------------------------------------------------------------------------------
しかし、このように実装してしまうと、CurrentHourPrinterのnow()は呼び出したときの時刻によって違う値を返すため、UTを書くことが出来ません。UTを書けないのは、このクラスがテストから制御することのできないTime.now()の返値と分かちがたく結びついている(依存している)からです。UTを書くためには、ここを切り離す必要があります。
依存関係を切り離すシンプルな方法は、依存部分を別のメソッドとして抽出し、UT用に作成したサブクラスでそのメソッドをオーバーライドすることです。この例なら以下のように書けます。
ファイルCurrentHourPrinter.rb
------------------------------------------------------------------------------------------------------
require "Time"
# 現在時刻を12時間表示するクラス
class CurrentHourPrinter
# 現在時刻の時の部分を12時間表記の文字列にする
def now()
toString(currentTime())
end
#例を作る都合上、privateに指定し、このメソッドはテストできないことにする。
private
def toString(time)
return time.strftime("%-I %P").sub("am", "a.m.").sub("pm", "p.m.")
end
# 現在時刻を取得するメソッド
protected
def currentTime()
return Time.now()
end
end
------------------------------------------------------------------------------------------------
これならUTを書くことができます。
ファイルCurrentHourPrinterTest.rb
------------------------------------------------------------------------------------------------------
require 'test/unit'
require "./CurrentHourPrinter"
# メソッドのオーバーライドを使用して依存関係を断ち切る例
class CurrentHourPrinterTest_methodOverride < Test::Unit::TestCase
def setup()
@currentHourPrinter = TestableCurrentHourPrinter.new()
end
def test_now_11_returns11am()
@currentHourPrinter .setHour(11)
assert_equal("11 a.m.", @currentHourPrinter .now())
end
def test_now_12_returns12pm()
@currentHourPrinter .setHour(12)
assert_equal("12 p.m.", @currentHourPrinter .now())
end
def test_now_13_returns1pm()
@currentHourPrinter .setHour(13)
assert_equal("1 p.m.", @currentHourPrinter .now())
end
# Timeにアクセスするメソッドをオーバーライドしたテスト用のCurrentHourPrinterクラス
class TestableCurrentHourPrinter < CurrentHourPrinter
def initialize
@time = Time.parse("2011/10/28 00:00");
end
def setHour(hour)
@time = Time.parse("2011/10/28 " + hour.to_s() +":00")
end
# Timeに依存するメソッドをオーバーライドする
# 設定された時間を返すことしかしていない
protected
def currentTime()
return @time
end
end
end
------------------------------------------------------------------------------------------------------
上記の実装では、CurrentHourPrinterのnow()の実装からTimeにアクセスする部分をメソッドcurrentTime()として抽出し、テスト対象用のクラスとして作成したTestableCurrentHourPrinterでcurrentTime()をオーバーライドしています。TestableCurrentHourPrinterが返す時刻はUTの中で自由に設定できますのでUTの作成が可能です。
1では、依存するオブジェクトにアクセスしないようにメソッドをオーバーライドすることで依存関係を切り離しましたが、依存するオブジェクトを入れ替えることで切り離す方法もあります。その実装例を示します。
ファイルCurrentHourPrinter.rb
------------------------------------------------------------------------------------------------------
require "Time"
# Timeのラッパークラス
class Clock
def current()
Time.now
end
end
# 現在時刻を12時間表示するクラス
class CurrentHourPrinter
def initialize(clock=Clock.new())
@clock = clock
end
# 現在時刻の時の部分を12時間表記の文字列にする
def now()
toString(currentTime())
end
#例を作る都合上、privateに指定し、このメソッドはテストできないことにする。
private
def toString(time)
return time.strftime("%-I %P").sub("am", "a.m.").sub("pm", "p.m.")
end
# 現在時刻を取得するメソッド
private
def currentTime()
return @clock.current()
end
end
------------------------------------------------------------------------------------------------------
ファイルCurrentHourPrinterTest.rb
------------------------------------------------------------------------------------------------------
require 'test/unit'
require "./CurrentHourPrinter"
# スタブを使用して依存関係を断ち切る例
class CurrentHourPrinterTest < Test::Unit::TestCase
def setup()
@clock = StubClock.new()
@currentHourPrinter = CurrentHourPrinter.new(@clock)
end
def test_now_11_returns11am()
@clock.setTime(Time.parse("2011/10/28 11:00"))
assert_equal("11 a.m.", @currentHourPrinter .now())
end
def test_now_12_returns12pm()
@clock.setTime(Time.parse("2011/10/28 12:00"))
assert_equal("12 p.m.", @currentHourPrinter .now())
end
def test_now_13_returns1pm()
@clock.setTime(Time.parse("2011/10/28 13:00"))
assert_equal("1 p.m.", @currentHourPrinter .now())
end
# Clockのスタブクラス
class StubClock < Clock
def initialize()
@time = Time.parse("2011/10/28 00:00")
end
def setTime(time)
@time = time
end
# Time.now()に依存するメソッドをオーバーライドして、設定された時間を返すようにする
def current()
return @time
end
end
end
------------------------------------------------------------------------------------------------------
上記のCurrentHourPrinterの実装では、クラスTimeにアクセスする部分をClockのインスタンス(@clock)へのアクセスに置き換え、@clockがTime.now()を呼び出しています。これによって、CurrentHourPrinter からTime.now()への直接の依存関係がなくなりますので、UTでClockをStubClockに入れ替えれば、Time.now()への依存関係を排除できます。このStubClockのように、メソッドが呼ばれたときには設定された値を返す(voidなら何もしない)だけのオブジェクトをスタブといいます。
スタブを使うと、テスト対象のクラスをそのままテストすることが出来ます。また、スタブを使用できるように設計することで、クラス間のインタフェースが明確に分離された設計になります。
Java、C++、C#のような言語なら、Interfaceを使ったほうが、設計意図が明確になるかもしれません。
2で紹介したスタブを拡張し、インタラクションの記録を持つようにしたのがモックです。スタブを使ったUTではテスト対象オブジェクトの状態やメソッドの返値をassertしますが、モックを使ったUTではテスト対象オブジェクトと関連するオブジェクトとのインタラクションをassertします。
モックを使ってCurrentHourPrinter.now()を呼ぶとClock.Current()が呼び出されることをassertしてみます。
ファイルCurrentHourPrinterTest.rb
------------------------------------------------------------------------------------------------------
require 'test/unit'
require "./CurrentHourPrinter"
# モックを使用して依存関係を断ち切ってインタラクションをテストする例
class CurrentHourPrinterTest < Test::Unit::TestCase
def setup()
@clock = MockClock.new()
@currentHourPrinter = CurrentHourPrinter.new(@clock)
end
(スタブの例と同じテストは省略)
def test_now_11_currentOfTimeCalled()
@clock.setTime(Time.parse("2011/10/28 11:00"))
@currentHourPrinter .now()
assert_equal("current ", @clock.getLog())
end
# Clockのモッククラス
class MockClock < Clock
def initialize()
@time = Time.parse("2011/10/28 00:00")
# 外部からのインタラクション履歴を状態に持つ
@log = ""
end
def setTime(time)
@time = time
end
# Time.now()に依存するメソッドをオーバーライドする
# 呼ばれると、インタラクション履歴状態を更新する
def current()
@log += "current "
return @time
end
def getLog()
return @log
end
end
end
------------------------------------------------------------------------------------------------------
上記の実装がスタブの実装と違うのは、MockClockのインスタンス変数@logです。スタブを使ったUTの例では、@currentHourPrinter を対象としてassertいましたが、モックの例では@clockを対象にassertしています。ここで行っているのは、@clockに対して意図したインタラクションが行われたかどうかの検証です。この@logのように呼び出し履歴を記録する文字列をログ文字列(Log String)と呼びます。
しかし、上記のようなモックの実装例は、モックではなく、スパイ(Spy)と呼ぶ場合もありますので、一般的にモックと呼ばれる形に書き直してみます。
ファイルCurrentHourPrinterTest.rb
------------------------------------------------------------------------------------------------------
require 'test/unit'
require "./CurrentHourPrinter"
# モックを使用して依存関係を断ち切ってインタラクションをテストする例
class CurrentHourPrinterTest < Test::Unit::TestCase
def setup()
@clock = MockClock.new()
@currentHourPrinter = CurrentHourPrinter.new(@clock)
end
(中略)ここはスタブの例と同じテスト
def test_now_11_currentOfTimeCalled ()
@clock.setTime(Time.parse("2011/10/28 11:00"))
@clock.setExpectedLog("current ")
@currentHourPrinter .now()
@clock.verify()
end
# Clockのモッククラス
class MockClock < Clock
include Test::Unit::Assertions
def initialize()
@time = Time.parse("2011/10/28 00:00")
# 外部からのインタラクション履歴を状態に持つ
@log = ""
@expectedLog = ""
end
def setTime(time)
@time = time
end
# インタラクション履歴状態の期待値を設定する
def setExpectedLog(expectedLog)
@expectedLog = expectedLog
end
# Time.now()に依存するメソッドをオーバーライドする
# 呼ばれると、インタラクション履歴状態を更新する
def current()
@log += "current "
return @time
end
# インタラクション履歴が期待通りかどうかを検査するメソッド
def verify()
assert_equal(@expectedLog, @log)
end
end
end
------------------------------------------------------------------------------------------------------
上記の実装のように、「予めモックオブジェクトに意図したインタラクションを設定しておき、モックオブジェクトにインタラクションが設定どおり行われたかどうか検証させる。」という形式が通常のモックです。
今まで手動でスタブやモックを作る例を示してきましたが、これを自動化するSWが世の中にはたくさんあり、モックオブジェクト・フレームワークとかアイソレーション・フレームワークとか呼ばれています。
Ruby用のモックオブジェクト・フレームワークの一つであるMochaを使って前の例を書くとこうなります。手動で特別なクラスを定義しなくても手動で書いたクラスと同じことが出来ています。
ファイルCurrentHourPrinterTest.rb
------------------------------------------------------------------------------------------------------
require 'test/unit'
require 'mocha'
require "./CurrentHourPrinter"
# Mochaを使用する例
class CurrentHourPrinterTest < Test::Unit::TestCase
def setup()
@clock = Clock.new()
@currentHourPrinter = CurrentHourPrinter.new(@clock)
end
def test_now_11_returns11am()
@clock.stubs(:current).returns(Time.parse("2011/10/28 11:00"))
assert_equal("11 a.m.", @currentHourPrinter .now())
end
def test_now_12_returns12pm()
@clock.stubs(:current).returns(Time.parse("2011/10/28 12:00"))
assert_equal("12 p.m.", @currentHourPrinter .now())
end
def test_now_13_returns1pm()
@clock.stubs(:current).returns(Time.parse("2011/10/28 13:00"))
assert_equal("1 p.m.", @currentHourPrinter .now())
end
def test_now_11_currentOfTimeCalled()
@clock.expects(:current).returns(Time.parse("2011/10/28 11:00")).once()
@currentHourPrinter .now()
end
end
------------------------------------------------------------------------------------------------------
モックオブジェクト・フレームワークを使えばずいぶんシンプルに書くことができます。自分たちの開発環境に合ったフレームワークを探し、自プロジェクトで使うかどうか検討してみるといいでしょう。
更新履歴