Published on

junit&mockito基礎

Authors
  • avatar
    Name
    Kikusan
    Twitter

Refs

install

junitのjarにクラスパスを通してjunit5テストするか,
mavenもしくはgradleでテスト
↓サンプルのpom.xml project以下に追記する

  <!-- junit5をmvn testするために必要なプラグイン -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- junit api, params, engineを包括したライブラリ -->
    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.7.1</version>
      <scope>test</scope>
    </dependency>
    <!-- mockito -->
    <!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-all</artifactId>
      <version>1.10.19</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

Junit

public class JUnit5Test {
    /**
     * BeforeAllは各クラスの生成時に一回実行
     */
    @BeforeAll
    static void beforeAll() {
        System.out.println("JUnit5Test#beforeAll()");
    }

    /**
     * BeforeEachは各メソッドの前に一回実行
     */
    @BeforeEach
    void beforeEach() {
        System.out.println("  JUnit5Test#beforeEach()");
    }

    @Test
    @DisplayName("失敗例") // 統計時の出力名を設定
    @Disabled // 実行しなくする(クラスにつけると全て実行しない)
    @EnabledOnOs(OS.WINDOWS) // WINDOWSでのみ実行 OS.LINUX MACも
    @EnabledOnJre(JRE.JAVA_11) // java11でのみ実行
    @EnabledIfEnvironmentVariable(named = "JAVA_HOME", matches = "path") // 環境変数がマッチしたらテスト
    void failTest() {
        Assertions.assertEquals(10, 8); // NG
    }

    @Test
    void sccessTest() {
        Assertions.assertEquals(10, 10); // OK
    }

    @Test
    void assertsTest() {
        Assertions.assertTrue(1 < 2); // trueなのでOK
        Assertions.assertNotNull("String"); // OK
        Assertions.assertAll("assertAllDemo",
                // 第二引数以降にラムダ式を取り、複数の判定を一つのテストとしてカウントできる(MultipleFailuresError)
                // 途中で失敗しても最後まで実行する。(他のassertは失敗したらその場でAssertionFailedErrorを投げる)
                () -> Assertions.assertTrue(false),
                () -> Assertions.assertEquals("aaa", "bbb"));
    }

    @Test
    void exceptionTest() {
        // 第二引数のラムダ式の中で例外が発生とき、そのクラスが第一引数と同じならOK
        Assertions.assertThrows(NullPointerException.class, () -> {
            throw new NullPointerException();
        });
    }

    @Test
    void assumeTest() {
        Assertions.assertEquals(10, 10);
        Assumptions.assumeTrue(true); // trueの時だけ続きをテストする
        Assertions.assertEquals(10, 8);
        Assumptions.assumingThat(true, () -> { // trueの時だけ第二引数の関数を実行する
            System.out.println("assumption that");
        });
    }

    @ParameterizedTest
    @ValueSource(strings = { "str1", "str2" }) // パラメータを渡したいとき{n}の数だけテストされる
    void paramsTest(String value) {
        Assertions.assertEquals("str1", value);
    }

    @Test
    void timeoutTest() { // 指定時間内に処理が完了する場合、OK
        Assertions.assertTimeout(Duration.ofSeconds(2), () -> Thread.sleep(1000));
    }

    /**
     * AfterEachは各メソッドの後に一回実行
     */
    @AfterEach
    void afterEach() {
        System.out.println("  JUnit5Test#afterEach()");
    }

    /**
     * AfterAllは各クラスの廃棄時に一回実行
     */
    @AfterAll
    static void afterAll() {
        System.out.println("JUnit5Test#afterAll()");
    }

}

単体テストの座学

ユニットテストの特徴はプログラムとして実行できる仕様書となることである。

その特性から先にテストを書き始める開発方針のことをテスト駆動開発という。

単体テストは自動化されるため何度も実行でき、安心してリファクタリングや機能拡張を行うことができる。

テストケースは小さな単位で可能な限り多く作るべし。影響範囲と条件が絞り込みやすくなる。

GUIテストのような難解なテストの場合は修正が多くなるので、テスト自動化コストとのトレードオフになる。

各テストは独立してあるべきであり、他のテストに影響するべきではない。前処理と後処理で独立性を担保する。
ex) 前処理でテーブル作成・インサート、検証、後処理でテーブル削除 外部ファイルで初期化は定義していい。

設定ファイルやシングルトンパターンはテストの独立性を損ないやすいので、状態の変更には十分注意する。

テストケースの分け方

  1. テストを行う前提条件
  2. テストに用いる入力値や操作
  3. テストを行った時に期待する値や動作

テストケースを分けるかで迷ったときは、、、迷わず分けるべし。

命名規約

基本的に対象クラスに対応するテストクラスを作成する。
対象クラスはsut (System Under Test)変数とするとわかりやすい。
実測値をexpected, 期待値をactualとするとまたわかりやすい。

テストしやすいメソッド設計

  1. メソッドが戻り値をもつ
  2. メソッドの呼び出しの結果、副作用がない
  3. 同じ状態、同じパラメータで実行すれば、必ず同じ結果を返す

テストダブル

代役=テストダブル。

  1. スタブ : 依存オブジェクトに予測可能な振る舞いをさせる
    • 依存オブジェクトが予測できない振る舞いをするとき 乱数生成関数⇒固定値で代用
    • 依存オブジェクトのクラスがまだ存在しないとき
    • 依存オブジェクトの実行コストが高く、簡単に利用できないとき 例外など
    • 依存オブジェクトが実行環境に強く依存しているとき
  2. モック : 依存オブジェクトの呼び出しを検証する 呼び出し側を代役にする
  3. スパイ : 依存オブジェクトの呼び出しを監視する
    • 戻り値がないとき、状態の変更をするとき : ロガーなど。
      依存オブジェクトをラップしたオブジェクトを用意し、変更値を検証する。

mockito

mockといいつつstubとspyもできるライブラリ

public class MockitoTest {
    @Test
    void stubTest() {
        List<String> stub = mock(List.class); // スタブオブジェクトの作成
        when(stub.get(0)).thenReturn("Hello"); // スタブメソッドの定義
        // get(0)の戻り値が"Hello"になっている
        assertEquals(stub.get(0), "Hello"); // 検証

        when(stub.get(anyInt())).thenReturn("World"); //anyXXXでXXX型の引数で全てスタブ化
    }

    @Test
    void stubExceptionTest() {
        List<String> stub = mock(List.class);
        when(stub.get(1)).thenThrow(new IndexOutOfBoundsException());
        stub.get(1); // 例外が投げられる
        //stub<List>のvoid clear()メソッド時に投げる場合
        doThrow(new RuntimeException()).when(stub).clear();
    }

    @Test
    void mocktest() {
        List<String> mock = mock(List.class);
        mock.clear();
        mock.add("Hello");
        mock.add("Hello");
        verify(mock).clear(); // mock<List>のclear()が呼ばれているか検証
        verify(mock, times(2)).add("Hello"); // 2回 add("Hello")が呼ばれているか検証
        verify(mock, never()).add("World"); // add("World")が呼ばれていないことを検証
    }

    @Test
    void spyTest() {
        List list = new ArrayList();
        List spy = spy(list); // spyで実オブジェクトをmock化
        when(spy.size()).thenReturn(100);
        spy.add("Hello");
        assertEquals("Hello", spy.get(0)); // 実オブジェクトであり
        assertEquals(100, spy.size()); // mock化もできる

        // stub(定義されていないメソッド)はdoReturn whenでないとspyでは記述できない
        doReturn("Mockito").when(spy).get(1);
        assertEquals("Mockito", spy.get(1));
    }

    class SpyExample {
        Logger logger = Logger.getLogger("loggername");

        public void doSomething() {
            logger.info("doSomething");
        }
    }

    /**
     * ロガーが書き込みをしたことを検証する
     */
    @Test
    void spyAnswerTest() {
        SpyExample sut = new SpyExample();
        Logger spy = spy(sut.logger);
        final StringBuilder infoLog = new StringBuilder();
        doAnswer(new Answer<Void>() { // new Answerを渡す
            // answerメソッドをオーバーライド
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                // InvocationOnMockオブジェクトは実オブジェクトのメソッド実行を表す
                // 実行された引数を取っておく[0]は第一引数, 呼ばれるたびappend
                infoLog.append(invocation.getArguments()[0]);
                // callRealMethodで実メソッドを呼び出す
                invocation.callRealMethod();
                return null;
            }
        }).when(spy).info(anyString()); // spy.info(anyString())を実関数として登録
        sut.logger = spy;
        sut.doSomething(); // 実行
        assertEquals("doSomething", infoLog.toString()); // 検証
    }

}

@Mockを使用してMockオブジェクトを生成する場合は、openMocksで初期化をして、最後にcloseする。

public class MockitoTest {
    private AutoCloseable closeable;

    @Mock
    private MockClass mockClass;

    @BeforeEach
    void setUp() {
        closeable = MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void tearDown() throws Exception {
        closeable.close();
    }
}