White Box技術部

WEB開発のあれこれ(と何か)

JUnitとMockito+PowerMockでテストケースを書いてみよう

最近テストばっかり書いていたので、 いい機会ですし、学んだり、考えたりしたことを、 私がテストを書くときに気にしていることと合わせて、まとめてみます。

あと、今回初めてMockitoとPowerMockを使ったので、 テストはそれらを使う場合にフォーカスして書いてます。

事前準備

関連FWの構成は以下です。

  • JUnit 4.12
  • Mockito 1.10.19
  • PowerMock 1.6.2

バージョンはPowerMockの都合に合わせています。

ファイル自体は、以下から「powermock-mockito-junit-1.6.2.zip」をダウンロードすると全て揃います。 (バージョンは上がっている可能性があります)

カバレッジ測定

テストを実施するだけであれば、必須ではありませんが、 JaCoCo(Emma)のEclipseプラグインを導入しておくと、 コードの実行漏れやテスト実行時のエラー発生個所が視覚的にわかって便利です。

私はテストケースのカバレッジ100%を実現するためではなく、 あくまでルート確認のツールとしてカバレッジツールを利用しています。
もちろん、ある程度のカバレッジが取れていないと、テストの意味がないので カバレッジ率も気にする必要はあると思いますが、たとえ100%になったとしても 入力データの想定パターンが網羅できている保障にはならないので、 そこに時間を割くぐらいなら、リファクタリングに時間を割いた方がいいと思っています。


テストのクラス名・クラス構成を決める

最近は以下のルールで、テストのクラス構成を策定しています。

1.テスト対象クラスのAllTestクラスを作成

初めに、テスト対象クラスのテストケースをまとめて実行する、AllTestクラスを作成します。

クラス名は「テスト対象クラス名 + AllTest」です。

// テスト対象クラスがSomeTestClassの場合
SomeTestClassAllTest

2.メソッド単位でテストクラスを作成

私はテストをメソッド単位で実施します。後述するように、メソッド単位のテストを実現するためにモックを使うわけですしね。

このテストスコープを明確にするため、テストクラスをメソッド単位で作成します。
作成するテストクラス名は「SomeTestClass_ + メソッド名(先頭大文字) + Test」です。

// テスト対象のメソッドがsomeMethodの場合
SomeTestClass_SomeMethodTest

3.モックを使わないテストを分ける

モックを使わずに頭から実行するテストや、実際に通信処理を行って確認するテストなどは、 通常の回帰試験対象としたくないこともあるので、それがわかるように命名しておきます。

モックを使わないテストクラス名は「テスト対象クラス名 + ActualTest」、
または「テスト対象クラス名 + _ + メソッド名(先頭大文字) + ActualTest」です。

SomeTestClassActualTest
SomeTestClass_SomeMethodActualTest

AllTestのサンプル

これらの考えをもとにAllTestクラスを作成すると、以下のようになります。

  • SomeTestClassAllTest.java
package blog;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

/**
 * SomeTestClassのテストスイート。<br>
 * SomeTestClassのテストをメソッド単位でテストクラスを作成し、SuiteClassesに追加すること。<br>
 * 
 * 作成するテストクラスは「SomeTestClass_ + メソッド名(先頭大文字) + Test」という名前にする。
 * 
 * @author seri
 */
@RunWith(Suite.class)
@SuiteClasses({
    SomeTestClass_SomeMethodTest.class,
})
public class SomeTestClassAllTest {
}

次からは、実際のテストクラスの書き方についてです。


Mockitoを使って、テスト範囲を限定させる

モックを使う利点は、やはり試験単位を本当に試したい部分に絞ることができることではないでしょうか。 テスト対象メソッドから呼び出している別機能をモックしておくことにより、 別機能の状態(開発中やバグ)に影響を受けることなく、作成した処理の正当性を確認することができるようになります。

もちろん最終的には、モックを利用しないテストも、どこかのタイミングで実施しておく必要があるので、忘れないように注意してください。

また長くなりがちな初期化処理は、「テスト対象機能から呼び出される別機能の処理内で必要だから記述している」 ということが多いので、別機能をモックすることにより、初期化処理をばっさりカットできたりします。

結果として可読性のよいテストコードができあがり、もうドヤリング待ったなしです。

冗談はさておき、可読性はコードを引き継いだ人がテストをメンテしてくれるかどうかに直結するので、 例えレビュー対象にならなくても、いやむしろプロダクトコードよりも注意して、 わかりやすく書こうと心がけることをお奨めします。


以下はMockitoを使い、試験対象外のクラスをモックして、JUnitのテストケースを書いていく流れです。

アノテーション

まずは利用するアノテーションの説明です。

@Mock

Mockするクラスに付与します。Mockするクラスはクラス変数として定義します。
@Mockを使わずにローカル変数としてMockクラスを作成することも可能です(mockメソッドを利用する)。

import org.mockito.Mock;

public class SomeTestClass_SomeMethodTest {

    @Mock
    private MockTargetClass fieldName;
    ...
@InjectMocks

試験対象のクラスに付与します。これもクラス変数として定義します。
試験対象クラス内の試験対象外メソッドをMockする場合は、インスタンスをspyメソッドを使って作成します。

import static org.mockito.Mockito.spy;

import org.mockito.InjectMocks;
import org.mockito.Mock;

public class SomeTestClass_SomeMethodTest {

    @Mock
    private MockTargetClass fieldName;
    
    @InjectMocks
    private SomeTestClass testSuite = spy(new SomeTestClass());
    ...

testSuiteのところは、以下のように書いても同じ動作になります。

    @InjectMocks
    @Spy
    private SomeTestClass testSuite = new SomeTestClass();
@Test

テスト対象にするアノテーションです。
Mockitoとは関係なくJUnit4のテストターゲットを指定するために必要です。

import static org.mockito.Mockito.spy;

import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;

public class SomeTestClass_SomeMethodTest {

    @Mock
    private MockTargetClass fieldName;
    
    @InjectMocks
    private SomeTestClass testSuite = spy(new SomeTestClass());

    @Test
    public void OKが返却されることを確認() {
        ...

テストメソッド

では本格的にテストを書いていきます。

テストメソッドの構成

テストメソッドの構成を予め決めて、該当する処理のコメントを入れておきます。
そして、それぞれブロックごとに該当する処理を書いていきます。

以下は私がよく使う4つのコメントです。

コメント文 説明
入力値 テストするメソッドに対して渡す入力値を定義する箇所を示すコメント。
Mockの設定 試験する上で必要なMock処理を記述する箇所。
期待値 試験結果として期待する値を定義する箇所。期待値がない場合(例外発生の確認のみなど)は、省略する。
検証 試験するメソッドを実行し、結果を検証する箇所。試験結果を保持して、assertEquals、assertThatなどのメソッドで検証する。

検証時に使うassertThatですが、これを使った場合、 例えばassertThat(actual, is(expected));のようなコードになると思います。 ですが、これ、エラーとなった場合にどういう値を比較したのかが表示されません。 is(expected)を満たしていないということがわかるだけです。
なので、JUnit4的にはassertThatを使うのがいいのかもしれませんが、 値比較による検証は今まで通りassertEqualsを使う方がわかりやすくて良いと思います。

@Test
public void OKが返却されることを確認() {

    // 入力値

    // Mockの設定

    // 期待値

    // 検証
}

触れてませんでしたが、テストメソッド名には日本語を使っています。
こうすることによりJavadocでのテスト内容補足がなくても、テストしたい内容がわかるようになりますし、 Jenkins等で回帰試験を実行した際に、どこを試験してエラーになったのか、メソッド名だけからわかるので、とても便利です。

最初は抵抗があるかもしれませんが、騙されたと思って試しに実施してみてください。 一度この書き方に慣れると「testXXXX_01」のようなテストメソッド名に、残念な思いを抱くようになること請け合いです。

入力値

テスト対象のメソッドのパラメータを宣言します。
変数名は、呼び出すテストメソッドの変数名と一致させるとわかりやすいと思います (メソッド宣言からコピペしてくると楽です)。

Mockの設定

アノテーションで記載したMock設定を有効にするためには、以下の一文が必要です。

MockitoAnnotations.initMocks(this);

なので「Mockの設定」では、最初にこれを書いてから必要なコードを記載していくことになります。

Spring Framworkを利用している場合、テスト対象クラス内で@Autowiredなどを使ってクラスをDIしていると思います。

以前ブログでネタにしたように、Springのテスト用Runnerを使うことで実際にDIさせることもできますが、テストスコープを考えると、モックさせた方が勝手がいいはずです。

何よりApplicationContextをロードすると、テストおっっっそいですし

Mockitoを利用するとDIクラスのモックが簡単にできてしまいます。

手順は先に示した方法と変わりなく、DIクラスを@Mockや@Spyで宣言し、テストクラスに@InjectMocksで注入するだけです。

モックしたクラスはテスト対象クラス内でvoidメソッドとして呼ばれるだけで、特に副作用を期待しないのであれば、これ以上手を加える必要がありません。 特定の戻り値を返すメソッドの場合は、その振る舞いを定義して、テスト対象クラスに設定する必要があります。

以下はそのサンプルです。

// fieldName#someProcess(String, int)をモックする例
// fieldName.someProcess("firstParam", 100)を実行した際に"OK"の文字列を返却する例
doReturn("OK").when(fieldName).someProcess("firstParam", 100);
// どんなパラメータで呼ばれても"OK"を返す場合は以下のように書く
// doReturn("OK").when(fieldName).someProcess(anyString(), anyInt());

// testSuiteのsomeFieldに、fieldNameの値を設定する
// setInternalStateのパラメータは
// リフレクション対象クラス、リフレクション対象のフィールド名、リフレクションで設定するフィールド、の順
Whitebox.setInternalState(testSuite, "someField", fieldName);

ここで1つ注意する点は、モック条件となるパラメータに、一つでもanyString()のようなワイルドカード(Matchersクラスのメソッド)を利用した場合は、 その他のパラメータも全てany系のメソッドでパラメータ指定する必要があることです。そうしないと、実行時エラーになります。

自作のDTO(ここではSomeDTO)などをワイルドカード指定する場合は、any(SomeDTO.class)のようにanyメソッドを使うか、 ArgumentCaptorを定義して、anySomeDTO.capture()のようにするかになります。

anySomeDTOの定義例は以下になります。

ArgumentCaptor<SomeDTO> anySomeDTO = ArgumentCaptor.forClass(SomeDTO.class);

期待値

ここではテスト対象メソッドを実行した結果、返却値として期待される値を定義します。 期待値の変数名は、expectedのような名前に統一すると可読性が良くなります。

検証

ここではテスト対象メソッドの実行と、その結果の返却値、または副作用の検証を行います。
返却値の変数名は、actualのような名前に統一すると可読性が良くなります。

テストケースサンプル

ここまでの内容からテストケースを作成すると、以下のような感じになります。

  • SomeTestClass_SomeMethodTest.java
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;

import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.internal.util.reflection.Whitebox;

public class SomeTestClass_SomeMethodTest {

    @Mock
    private MockTargetClass fieldName;

    @InjectMocks
    private SomeTestClass testSuite = spy(new SomeTestClass());

    @Test
    public void OKが返却されることを確認() {

        // 入力値
        String arg1 = "firstParam";
        int arg2 = 100;

        // Mockの設定
        {
            MockitoAnnotations.initMocks(this);

            // fieldName#someProcess(String, int)をモックする
            // fieldName.someProcess("firstParam", 100)を実行した際に"OK"の文字列を返却する例
            doReturn("OK").when(fieldName).someProcess("firstParam", 100);
            // どんなパラメータで呼ばれても"OK"を返す場合は以下のように書く
            //     doReturn("OK").when(fieldName).someProcess(anyString(), anyInt());

            // testSuiteのsomeFieldに、fieldNameの値を設定する
            // setInternalStateのパラメータは
            // リフレクション対象クラス、リフレクション対象のフィールド名、リフレクションで設定するフィールド、の順
            Whitebox.setInternalState(testSuite, "someField", fieldName);
        }

        // 期待値
        String expected = "OK";

        // 検証
        String actual = testSuite.someMethod(arg1, arg2);
        assertEquals(expected, actual);
    }
}

ちなみにテスト対象クラス関連は、以下のような感じです。

  • SomeTestClass.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class SomeTestClass {

    @Autowired
    @Qualifier("mockTargetClass")
    private MockTargetClass someField;

    public String someMethod(String arg1, int arg2) {

        return someField.someProcess(arg1, arg2);
    }
}
  • MockTargetClass.java
import org.springframework.stereotype.Component;

@Component
public class MockTargetClass {

    public String someProcess(String arg1, int arg2) {

        return "No implementation";
    }
}

ここまではJUnitとMockitoだけでテストが書けます。
次からは、PowerMockを使う必要が出てくるテストに関してです。


@RunWithの使いどころ

上記テストでは@RunWithアノテーションを利用していませんでした。使う必要がなかったのもありますが、 @RunWithアノテーションはTestクラスに対して1つしか使えないため、必要になるまでは極力指定を避けた方がいいです。

指定しなくてもデフォルトでBlockJUnit4ClassRunnerが付くだろ、という話は置いておいて。

では、いったいどのようなところが使いどころなのかというと、例えばPowerMockを使う場合です。

PowerMock

PowerMockはMockitoではできなかった、以下を実現するために使用します。

これ以外にもPowerMockでできることを知りたい場合は、以下の公式ドキュメントを参照してください。

では簡単にそれぞれの実装例を記載します。 (以下のテストクラス名は、PowerMockを使った何のテストかがわかりやすいように、決めておいた命名規則から若干外れています)

ちなみに、テスト対象クラスはこれらの試験をするため、少し修正しています。

  • SomeTestClass.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

public class SomeTestClass {

    @Autowired
    @Qualifier("mockTargetClass")
    private MockTargetClass someField;

    public static String getHoge() { return "hoge"; }

    public String someMethod(String arg1, int arg2) {

        System.out.println(addLocal(arg1));

        if ("localParam".equals(arg1)) {
            someField = new MockTargetClass();
        }

        return someField.someProcess(arg1, arg2);
    }

    private String addLocal(String str) {

        return "local:" + str;
    }
}

staticメソッドのモック

staticメソッドをモックする場合は、@PrepareForTestにstaticメソッドを持つクラスを記述し、 そのクラスをPowerMockito.mockStaticメソッドのパラメータに渡す必要があります。 その後、whenメソッドを使い、返却値をthenReturnで書くなどすればOKです。(doReturn系のメソッドは使えません)

  • SomeTestClass_StaticMethodTest.java
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SomeTestClass.class })
public class SomeTestClass_StaticMethodTest {

    @Mock
    private MockTargetClass fieldName;

    @InjectMocks
    private SomeTestClass testSuite = spy(new SomeTestClass());

    @Test
    public void hogeをHOGEに変えてみる() {

        // Mockの設定
        {
            MockitoAnnotations.initMocks(this);

            PowerMockito.mockStatic(SomeTestClass.class);

            when(SomeTestClass.getHoge()).thenReturn("HOGE");
        }

        // 期待値
        String expected = "HOGE";

        // 検証
        String actual = SomeTestClass.getHoge();
        assertEquals(expected, actual);
    }
}

特定クラスのインスタンス生成をモック

以下ではインスタンス生成に失敗するように書いていますが、 「特定のモック処理を持ったインスタンスを生成させるようにする」など、 別の使い方をするのにも有効です。

このテストを書く際に勘違いしやすいのが、@PrepareForTestに指定するクラスは モックしたいインスタンスのクラスではなく、インスタンス生成の実際に行うクラスだということでしょうか。 うまくモックされない場合は、ここを確認してみてください。

  • SomeTestClass_WhenNewTest.java
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.spy;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SomeTestClass.class })
public class SomeTestClass_WhenNewTest {

    @Mock
    private MockTargetClass fieldName;

    @InjectMocks
    private SomeTestClass testSuite = spy(new SomeTestClass());

    @Test
    public void インスタンス生成でエラー発生した場合を確認() throws Exception {

        // 入力値
        String arg1 = "localParam";
        int arg2 = 200;

        // Mockの設定
        {
            MockitoAnnotations.initMocks(this);

            PowerMockito.whenNew(
                    MockTargetClass.class).withNoArguments().thenThrow(
                            new RuntimeException("error message"));
        }

        // 期待値
        String expected = "error message";

        // 検証
        try {
            testSuite.someMethod(arg1, arg2);
            fail("exception is not occur.");

        } catch (RuntimeException re) {
            assertEquals(expected, re.getMessage());
        }
    }
}

自クラスのprivateメソッドのモック

テスト対象クラスのprivateメソッドをモックする場合、気を付けなければいけないのが、 spyメソッドはPowerMockitoクラスのものを利用する必要があるところです。 Mockitoクラスのspyを利用した場合、実行時にNullPointerExceptionが発生します。

  • SomeTestClass_LocalMethodTest.class
import static org.junit.Assert.assertEquals;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SomeTestClass.class })
public class SomeTestClass_LocalMethodTest {

    @Mock
    private MockTargetClass fieldName;

    @InjectMocks
    private SomeTestClass testSuite = PowerMockito.spy(new SomeTestClass());

    @Test
    public void local以外が付与されることを確認() throws Exception {

        // 入力値
        String arg1 = "firstParam";
        int arg2 = 100;

        // Mockの設定
        {
            MockitoAnnotations.initMocks(this);

            PowerMockito.doReturn("not local").when(testSuite, "addLocal", "firstParam");
        }

        // 期待値
        String expected = "not local";

        // 検証
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos);
        PrintStream old = System.out;
        System.setOut(ps);

        testSuite.someMethod(arg1, arg2);

        System.out.flush();
        System.setOut(old);

        String actual = baos.toString().replaceAll(System.getProperty("line.separator"), "");

        assertEquals(expected, actual);
    }
}

PowerMockを使った場合の注意

上記例のように、テスト対象のクラスに対してPowerMockを適用させると、JaCoCo(Emma)のカバレッジ対象外になります。 カバレッジの網羅率を気にする場合は、その点に気を付けてください。


可読性を維持するために

長くなりましたが最後に少しだけ。

テストは、なるべくテストメソッド内で処理が完結するように書くのが良いと思います。
初期化処理やテスト用の独自ユーティリティクラスを外に作りたくなることがあると思いますが、 テストケースでこれらをやると、他の人が理解しにくいテストになりがちです。

ここら辺はテスト対象のプログラムごとに異なりますし、バランスの問題になってくるので一概には言えませんが、 他のメンバーがいる場合は、適時相談しながらルール作りをしていくことをお勧めします。

コーディング規約やコードレビューでも見逃されがちなテストの書き方ですが、 だからこそ、各人が一層気を付けてわかりやすいコードとなるように、頭をしぼりましょう!

本記事中のリンクについて

PowerMockはGoogle Codeで管理されているプロジェクトですが、先日発表があったようにGoogle Codeも終わるときが来たようですので、 今回参考リンクで記載したものは、遅くとも1年後にはあてにならなくなっていると思います。 リンク切れになっていたらすみません。