JUnitで例外の発生を検証する

## はじめに
[ひとりJUnitアドベントカレンダー](https://qiita.com/advent-calendar/2022/junit)1日目の記事です。
初日から遅刻しています。

## ユースケース
以下のような、例外送出処理を持つクラスに対して
想定通りの例外が投げられているかを検証したい場面を想定します。
```java
public class HogeService {

  public String execute(String str) {
    if (str == null) {
      throw new IllegalArgumentException("argument cannot be null.");
    } else if (str.equals("")) {
      throw new IllegalArgumentException("argument cannot be empty.");
    }
    return str + "hoge";
  }
}
```

## JUnit4の場合
JUnit4では以下のように、`@Test`のアノテーションで想定例外を設定することができます。

```java
import org.junit.Test;

public class HogeServiceTest {

  private HogeService hogeService = new HogeService();

  @Test(expected = IllegalArgumentException.class)
  public void 異常系_引数がnullの場合() {
    hogeService.execute(null);
  }

  @Test(expected = IllegalArgumentException.class)
  public void 異常系_引数が空文字の場合() {
    hogeService.execute("");
  }
}
```

ただしこの場合、アノテーションが検証してくれるのは例外の型のみであって
今回の場合のように、投げられた例外に対して設定された情報までは検証が行えません。

そのためより詳細な検証を行いたい場合は、
以下のように`try`で囲み、例外の情報を`catch`する必要があります。

```java
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.fail;

public class HogeServiceTest {

  private HogeService hogeService = new HogeService();

  @Test
  public void 異常系_引数がnullの場合() {
    try {
      hogeService.execute(null);
      fail();
    } catch (IllegalArgumentException e) {
      assertThat(e.getMessage(), is("argument cannot be null."));
    }
  }

  @Test
  public void 異常系_引数が空文字の場合() {
    try {
      hogeService.execute("");
      fail();
    } catch (IllegalArgumentException e) {
      assertThat(e.getMessage(), is("argument cannot be empty."));
    }
  }
}
```

このときに忘れてはならないのは、
`try`句の中の例外発生を期待する処理の直後に、`fail()`等のテストを失敗させる処理を記述することです。

JUnitでの検証が失敗するのは`assert`系の処理で期待値と実測値に差異があった場合のみです。
例外を期待しているにも関わらず、例外が発生せずに`catch`句へ処理が移らなかった場合、
`assert`の処理が全く動かないため、検証に成功した場合と同じようにテストメソッドが正常終了します。

よってテスト対象処理の直後の行、
すなわち例外が発生すれば本来は到達しないはずの場所に`fail()`を記述し、
例外が発生しなかったことをテスト失敗として検知する必要があります。

今回のコードでは`org.junit.Assert.fail`を使っていますが、
他の同機能類似メソッドであっても、大抵は失敗理由を引数で受け取って出力できるため、
どういう経緯のテスト失敗かを与えてトレーサビリティ向上を図るのも一案と思います。
```java
    try {
      hogeService.execute(null);
      fail("期待していた例外が発生しませんでした");
    } catch (IllegalArgumentException e) {
```

もしくは最初に紹介したアノテーションでの期待値設定をしつつ、例外を投げ直す手もあります。
こっちの方が一般的かも。
```java
  @Test(expected = IllegalArgumentException.class)
  public void 異常系_引数が空文字の場合() {
    try {
      hogeService.execute("");
    } catch (IllegalArgumentException e) {
      assertThat(e.getMessage(), is("argument cannot be empty."));
      throw e;
    }
  }
```

## JUnit5の場合
とはいえテストの中に`try`文があるとギョッとするというか、ゴテゴテしてんなーと思わなくもないし、
`assert`系で済ませられるなら`fail()`もあまり使いたくはない。
条件→実行→検証の順序で綺麗に書きたいから、`@Test`に想定値渡すのもなんかなあ。と思っていましたが、
JUnit5より新たに追加された`assertThrows()`を使うと以下のようにすっきり書けます。

```java
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class HogeServiceTest {

  private HogeService hogeService = new HogeService();

  @Test
  public void 異常系_引数がnullの場合() {
    IllegalArgumentException expected =
        assertThrows(IllegalArgumentException.class, () -> hogeService.execute(null));
    assertThat(expected.getMessage(), is("argument cannot be null."));
  }

  @Test
  public void 異常系_引数が空文字の場合() {
    IllegalArgumentException expected =
        assertThrows(IllegalArgumentException.class, () -> hogeService.execute(""));
    assertThat(expected.getMessage(), is("argument cannot be empty."));
  }
}
```

`assertThrows()`は第一引数に期待する例外のクラス情報、
第二引数に`Executable`というJUnit5が定義する関数型インターフェースを受け取ります。
(さらに第三引数にメッセージを受け取るオーバーロードもあります)

関数型インターフェースの話はそれはそれで一本書けそうなので詳細は割愛しますが、
テスト対象メソッドをラムダ式として渡してあげるだけなので、
早い話がテスト対象メソッドの前に`() -> `を付けてぶちこめばオールオッケー。

例外が発生しなければ`assertThrows`でテスト失敗になるので`fail()`は不要です。
またその後の詳細検証も、`assertThrows`の戻り値で投げられた例外を取得できるので`catch`の必要はなく、
JUnit4時代に比べるとだいぶシンプルかつコンパクトに書けていい感じです。