2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JUnitで外部ライブラリを使用せずテストダブルを実装する

Posted at

テストダブルとは

演者の代役や影武者を意味する英単語 "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"));
    }
}

参考文献

この記事は以下の情報を参考にして執筆しました。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?