テスト対象を切り離す方法

Access count since 2012/3/11

実用的なプログラムの開発の中でUnit Test(UT)を作成しようとするとすぐに、「テストしたいオブジェクトとは別のオブジェクトがじゃまになってテストが書けない。」という問題にぶつかります。この問題の解決にはいくつか定石がありますので、基本的な3つの定石を紹介します。例はRubyで書いていますが、他の言語の場合も考え方は変わりません。

1.サブクラスを作成し、メソッドをオーバーライドする方法

ちょっと無理やりな例ですが、現在時刻の時(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の作成が可能です。

 


2.スタブ(Stub)を使う方法

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を使ったほうが、設計意図が明確になるかもしれません。


3.モック(Mock)を使う方法

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

------------------------------------------------------------------------------------------------------

上記の実装のように、「予めモックオブジェクトに意図したインタラクションを設定しておき、モックオブジェクトにインタラクションが設定どおり行われたかどうか検証させる。」という形式が通常のモックです。

 


4.モックオブジェクト・フレームワーク、アイソレーション・フレームワーク(Isolation Framework)を使う方法

今まで手動でスタブやモックを作る例を示してきましたが、これを自動化する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

------------------------------------------------------------------------------------------------------

モックオブジェクト・フレームワークを使えばずいぶんシンプルに書くことができます。自分たちの開発環境に合ったフレームワークを探し、自プロジェクトで使うかどうか検討してみるといいでしょう。

 

 


更新履歴

  1. 2011年10月28日 初版作成
  2. 2012年03月10日 公開

勝田 均


ホーム