テストダブルとは
演者の代役や影武者を意味する英単語 "double" から派生した言葉。
一般的にはモック、スタブと呼ばれる。
なぜテストダブルを使用するのか
システムの単体テストにおいて、テスト対象が
他のクラスや外部要素に依存していないケースはほとんどない。
依存関係のあるテストは、対象だけでなく、その依存対象についてもテストしたことになる。
メリットとしては、よりシステム稼動時と同じ状態でテストが行われるため、
テスト結果が保証する内容の価値が上がるという側面がある。
一方のデメリットとしては、テスト失敗時に原因分析の範囲が広くなる、
依存対象の処理結果が予想できない場合は検証の精度が落ちるといった側面がある。
上記のデメリットに対する対策として、テストに対して影響を与える要素の代わりに
テストしやすい代役を立てて、テストの独立性を高めるという方法がある。
これらを実現するのが、スタブやモック(テストダブル)の使用である。
スタブとモックの違い
両者とも、テスト対象が依存するクラスやモジュールの代用として使用する
クラスやモジュールという点では違いがない。
よって、しばしば同義の言葉として使用されるが、狭義的には以下の違いがある。
スタブ
依存オブジェクトに予測可能な振る舞いをさせることが目的。
その他にも、以下のようなオブジェクトの代用として使用される。
- 予測できない振る舞いをする
- 実装が完了していない
- 実行コストが高く、簡単に利用できない
- 実行環境に強く依存している
モック
依存オブジェクトが実行中に呼び出されたかを確認することが目的。
様々なテストダブル
固定値を返すスタブ
乱数やシステム日付など、思った値を取得するのが難しい処理は、
メソッドおよびクラスとして抽出しておき、スタブで切り替えるとテストしやすくなる。
スタブの実装の仕方は様々あるが、詳細は「JUnitにおけるリファクタリングの手法」を参照。
以下の例では、乱数の取得の処理をメソッドとして切り出し、
テストコードにおいてスタブメソッドを作成し、オーバーライドしている。
public class GetRandomElementFromList {
public <T> T choice(List<T> elements) {
if (elements.size() == 0) {
return null;
}
int idx = nextInt() % elements.size();
return elements.get(idx);
}
int nextInt() {
return new Random().nextInt();
}
}
public class GetRandomElementFromListTest {
private List<String> elements = new ArrayList<>();
@Before
public void setUp() {
elements.add("A");
elements.add("B");
}
@Test
public void choiceでAを返す() {
GetRandomElementFromList sut = new GetRandomElementFromList() {
@Override
int nextInt() {
return 0;
}
};
assertThat(sut.choice(elements), is("A"));
}
@Test
public void choiceでBを返す() {
GetRandomElementFromList sut = new GetRandomElementFromList() {
@Override
int nextInt() {
return 1;
}
};
assertThat(sut.choice(elements), is("B"));
}
}
例外を送出するスタブ
データベースの接続エラーなど、例外を発生させることが困難なケースは多い。
そういった場合にも、例外を送出するメソッドを
小さく切り出すことで、スタブによる切り替えが可能となる。
スパイ :オブジェクトの呼び出しを監視するモック
基本的なユニットテストは、assertThatメソッドのように入力値と出力値の検証を行う。
そのため、テスト対象が戻り値を返し、状態を保持しないオブジェクトはテストが容易である。
対して、戻り値のないメソッドや対象オブジェクトの状態が変わる場合のテストケースは
検証が困難となる。依存オブジェクトの状態が変わる場合、検証はさらに困難になる。
依存オブジェクトへの副作用の典型的な例としては、
標準出力やロガーへ書き込まれた内容の検証がある。
(テスト対象の実行により依存オブジェクト(標準出力など)の状態が変わるため)
そういった場合は、書き込みを行うロガーや標準出力を行うオブジェクトを
スパイと呼ばれるオブジェクトにすり替え、隠蔽された処理を監視する。
スパイは、オリジナルのオブジェクトをラップして実装する。
以下は、ログ出力を行うメソッドの検証を行うためのテストコードの実装例である。
public class OutputLog {
Logger logger = Logger.getLogger(OutputLog.class.getName());
public void outputLog() {
logger.info("doSomething");
}
}
public class SpyLogger extends Logger{
final Logger base;
// ログ出力とは別にテスト側からログを参照するためのクラス変数
final StringBuffer log = new StringBuffer();
public SpyLogger(Logger base) {
super(base.getName(), base.getResourceBundleName());
this.base = base;
}
// スタブとなるメソッド
@Override
public void info(String message) {
base.info(message);
// 追加の処理 → スパイが独自に持っているStringBufferに出力する
log.append(message);
}
}
public class SpyExampleTest {
@Test
public void test() {
OutputLog sut = new OutputLog();
SpyLogger spyLogger = new SpyLogger(sut.logger);
// テスト対象のロガーをスパイロガーにすり替える
sut.logger = spyLogger;
sut.outputLog();
assertThat(spyLogger.log.toString(), is("doSomething"));
}
}
参考文献
この記事は以下の情報を参考にして執筆しました。