よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

JUnit5 のアサーション

アサーションの種類

JUnit の assert 系のメソッドには、以下のものがあります。

  • assertEquals:2 つの値が等しいかどうかをテスト
  • assertTrue:引数の値が true かどうかをテスト
  • assertFalse:引数の値が false かどうかをテスト
  • assertNull:引数の値が null かどうかをテスト
  • assertNotNull:引数の値が null でないかどうかをテスト
  • assertSame:2 つの参照が同じインスタンスかどうかをテスト
  • assertNotSame:2 つの参照が異なるインスタンスかどうかをテスト
  • assertArrayEquals:2 つの配列が等しいかどうかをテスト
  • assertThrows:指定した関数が例外をスローするかどうかをテスト
  • assertNotEquals:2 つの値が等しくないかどうかをテスト
  • assertThat:値が指定された条件を満たすかどうかを確認
  • assertThrows:例外がスローされるかどうかを確認
  • assertDoesNotThrow:例外がスローされないかどうかを確認
  • assertTimeout:指定された時間内に実行が終了するかどうかを確認
  • assertTimeoutPreemptively:指定された時間内に実行が終了するかどうかを確認
  • assertLinesMatch:2つの文字列が一致するかどうかを確認
  • assertAll:複数のアサーションをまとめて確認

assertTimeout と assertTimeoutPreemptively の違い

assertTimeoutPreemptivelyは、実行が長引いた場合にアサーションを実行して、実行を中断できるようになっています。

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class AssertionsDemo {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "省略可能なアサーションメッセージは最後のパラメーター");
        assertTrue('a' < 'b', () -> "アサーションメッセージは遅延評価できる -- "
                + "不必要に複雑なメッセージを構築するコストを割けるために");
    }

    @Test
    void timeoutNotExceeded() {
        // 次のアサーションは成功する。
        assertTimeout(ofMinutes(2), () -> {
            // 2分未満で終わるタスクを実行する。
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // 次のアサーションは成功し、指定されたオブジェクトを返す。
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // 次のアサーションは、メソッド参照を実行してオブジェクトを返す。
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // 次のアサーションは、以下のようなエラーメッセージを出して失敗する:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // 10ミリ秒より時間のかかるタスクをシミュレートする。
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // 次のアサーションは、以下のようなエラーメッセージを出して失敗する:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // 10ミリ秒より時間のかかるタスクをシミュレートする。
            Thread.sleep(100);
        });
    }

    private static String greeting() {
        return "Hello, World!";
    }

}

ラムダを使った assertAll()assertThrows()

引用元:初めてのJUnit 5

JUnit 5のorg.junit.gen5.Assertionsクラスには、テスト・メソッドの条件を扱うassertEquals、assertTrue、assertNull、assertSameなどのstaticアサーション・メソッドと、それぞれの否定版となるメソッドが含まれています。JUnit 5では、これらのアサーション・メソッドでラムダ式を活用できるように、各メソッドをオーバーロードしてjava.util.function.Supplierのインスタンスを受け取るようにしたメソッドが提供されています。これによってアサーション・メッセージを遅延評価できるようになるため、複雑になる可能性がある計算を実際にアサーションが失敗するまで遅らせることができます。

関数型Interface の Executable がラムダで実行されます。

  • Interface Executable
    Assertions.assertAll(Executable...)
    Assertions.assertAll(String, Executable...)
    Assertions.assertThrows(Class, Executable)

assertAll()

一つ一つの assert を途中で止めずに一気に評価したい場合に assertAll() を使用すると便利です。複数のアサーションの結果を一括で確認することができます。

assertEquals 等は、テストに失敗した時点でエラーが発生する(AssertionFailedErrorがスローされる)ので、それ以降のテストは実行されません。assert が多いとそれでは非常に面倒な場面があります。複数のテストが失敗しても、エラーがスローされるのは1回だけ(MultipleFailuresError)となります。

これでアサーションの結果を1つ直しては次のアサーションが失敗し、それを直すとまた次が、といったことを回避できます。また、アサーションのグループ化という側面もあります。

    @Test
    void groupedAssertions() {
        // アサーションをグループ化すると、すべてのアサーションが一度に実行され、
        // すべての失敗がまとめて報告される。
        assertAll("person",
            () -> assertEquals("John", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // コードブロック内でアサーションが失敗すると、同じブロック内の後続のコードはスキップされる。
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // 上のアサーションが成功した場合のみ実行される。
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("n"))
                );
            },
            () -> {
                // グループ化されたアサーションは、first name のアサーションとは独立して実行される。
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // 上のアサーションが成功した場合のみ実行される。
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }
org.opentest4j.MultipleFailuresError: person (2 failures)
    name ==> expected: <aaa> but was: <zzz>
    age ==> expected: <99> but was: <20>
    at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:66)
    at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:44)
    at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:38)
    at org.junit.jupiter.api.Assertions.assertAll(Assertions.java:1039)
    at com.example.AssertTest.all(AssertTest.java:115)

    ~~~~~~~~~~~~~~

assertThrows()

assertThrows は例外の発生を確認する assert メソッドです。JUnit4ではテストメソッドから例外で抜け出たときに検証する方式だったので、例外発生後の状態を確認するようなテストが書きにくかった面がありました。別記事の「モックの使用例」で記述している try-catch などで冗長に行う必要がありました。

モック入門『考え方と使い分けについて』 - よしたろうブログ

public class ServiceTest {

    @InjectMocks
    private UserRepository mockUserRepository;
    @Mock
    private UserDao mockUserDao;
    @BeforeEach
    public void before() {
        UserInfo entity = new UserInfo();
    }

   /**
   * ユーザ情報削除時の異常系テスト 
   * DomaException を発生させる
   */
    @Test
    public void DeleteExecutionErrorTest() {
        entity.setUserCd("userCd78901234567893");
        when(mockUserDao.delete(any())
            .thenThrow(new DomaException(Message.DOHA0001,""));

        try {
            mockUserRepository.delete(entity);
            fail();
        } catch (Exception expected){
            assertTrue(expected instanceof DomaException);
        }
}

assertThrows を使えばこの問題を解消することができます。発生した例外を受け取り、例外が保持している情報をチェックすることも出来る様になりました。assertThrowsの第1引数で「発生するであろう例外」のクラスを指定し、第2引数(ラムダ式)でテスト対象の処理を実行する構文です。

    @Test
    void exceptionTesting() {
        // 例外発生を期待する。例外発生後も処理ができる
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }