mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5の標準のassertとDynamicTestの用い方

JUnit5でわりと便利だと思っているのが、JUnit5に標準でついてくる Assertions#assertAll@TestFactory で返す Iterable<DynamicTest> です。


エンタープライズな現場でよく見かけるテストとして、こういうのがあるかと思います。

@Test
void firstTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    assumeTrue(u1 != null);
    assertEquals(1L, u1.getId());
    assertEquals("ユーザー1", u1.getName());
    assertEquals("test1@example.com", u1.getEmail());

    final UserEntity u2 = map.get(2L);
    assumeTrue(u2 != null);
    assertEquals(2L, u2.getId());
    assertEquals("ユーザー2", u2.getName());
    assertEquals("test2@example.com", u2.getEmail());

    final UserEntity u3 = map.get(3L);
    assumeTrue(u3 != null);
    assertEquals(3L, u3.getId());
}

private Map<Long, UserEntity> getUsers() {
    return SampleCodesTest.mapOf(
                kv(1L, new UserEntity(1L, "ユーザー1", "test1@example.com", "password1")),// 
                kv(3L, new UserEntity(3L, "ユーザー3", "test3@example.com", "password3"))//
        );
}

なるべく一つのテストメソッドには一つのアサーションにするべきなのはわかってはいるけれど、どうしても各プロパティの値を調べたいし、 equals を実装して簡単にアサーションしたいけど何らかの制約により equals が実装できないので仕方なくプロパティの個数だけアサーションをするようなケースです。

この複数回アサーションを実行するのがまずいのは、アサーションが失敗するケースがいくつかある場合に、最初の一つだけが落ちて、残りのアサーションが実行されないので、何が落ちているのかがわからないということにあります。実際上に書いたテストも途中でabortされていて、何が通って何が落ちているのかがわかりません。

そこでJUnit5では、複数のアサーションを引数にとって、すべてのアサーションを実行するアサーション Assertions.assertAll が定義されています。それでは、上記のテストを assertAll を用いて書き換えてみます。

@Test
void secondTest() {
    final Map<Long, UserEntity> map = getUsers();

    final UserEntity u1 = map.get(1L);
    final UserEntity u2 = map.get(2L);
    final UserEntity u3 = map.get(3L);

    assertAll(
            () -> assumeTrue(u1 != null),
            () -> assertEquals(1L, u1.getId()),
            () -> assertEquals("ユーザー1", u1.getName()),
            () -> assertEquals("test1@example.com", u1.getEmail()),
            () -> assumeTrue(u2 != null),
            () -> assertEquals(2L, u2.getId()),
            () -> assertEquals("ユーザー2", u2.getName()),
            () -> assertEquals("test2@example.com", u2.getEmail()),
            () -> assumeTrue(u3 != null),
            () -> assertEquals(3L, u3.getId())
    );
}

さて、これでもう安心と言えるかというと、実はそんなことはありません。なぜかというと、 assertAll の中にテストをabortするものが入っていると、すべてを実行してくれなくなるためです。 assertAll は一つでも落ちるものがある場合にはアサーションが失敗になるように複数のアサーションをまとめてくれる機能ですが、一つでもabortがあると、途中で止まってしまい、現在のテストの状態がわからなくなります。上記のテストも途中でabortされてしまい、最初のパターンと何ら変わるところがありません。

そこで、用いるのが、これまたJUnit5の標準に入っている DynamicTest@TestFactory です。

DynamicTest@TestFactory は動的にテストを作ると説明されていることがありますが、複数のアサーションをまとめたい場合に適用するのが、最適な活用パターンではないかと思います。

では、先ほどのテストを書き換えてみましょう。

@TestFactory
List<DynamicTest> thirdTest() {
    final Map<Long, UserEntity> map = getUsers();
    final List<DynamicTest> tests = new ArrayList<>();

    final UserEntity u1 = map.get(1L);
    tests.add(dynamicTest("ユーザー1",//
            () -> assertAll(
                    () -> assumeTrue(u1 != null),
                    () -> assertEquals(1L, u1.getId()),
                    () -> assertEquals("ユーザー1", u1.getName()),
                    () -> assertEquals("test1@example.com", u1.getEmail())
            )));

    final UserEntity u2 = map.get(2L);
    tests.add(dynamicTest("ユーザー2",//
            () -> assertAll(
                    () -> assumeTrue(u2 != null),
                    () -> assertEquals(2L, u2.getId()),
                    () -> assertEquals("ユーザー2", u2.getName()),
                    () -> assertEquals("test2@example.com", u2.getEmail())
            )));

    final UserEntity u3 = map.get(3L);
    tests.add(dynamicTest("ユーザー3",//
            () -> assertAll(
                    () -> assumeTrue(u3 != null),
                    () -> assertEquals(3L, u3.getId())
            )));

    return tests;
}

このテストを実行すると、「ユーザー1」と「ユーザー3」というテストが通っていて、「ユーザー2」というテストがabortされている状態がわかります。これで現在のテストの状態がわかるようになりました。


という感じで、何らかの理由により複数のアサーションをせざるを得ない時に、JUnit5の機能を用いてテストを整理するとテストの状態がよくわかるようになるという話でした。