12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJUnitAdvent Calendar 2022

Day 1

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

Last updated at Posted at 2022-12-01

はじめに

ひとりJUnitアドベントカレンダー1日目の記事です。
初日から遅刻しています。

ユースケース

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

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のアノテーションで想定例外を設定することができます。

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する必要があります。

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を使っていますが、
他の同機能類似メソッドであっても、大抵は失敗理由を引数で受け取って出力できるため、
どういう経緯のテスト失敗かを与えてトレーサビリティ向上を図るのも一案と思います。

    try {
      hogeService.execute(null);
      fail("期待していた例外が発生しませんでした");
    } catch (IllegalArgumentException e) {

もしくは最初に紹介したアノテーションでの期待値設定をしつつ、例外を投げ直す手もあります。
こっちの方が一般的かも。

  @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()を使うと以下のようにすっきり書けます。

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時代に比べるとだいぶシンプルかつコンパクトに書けていい感じです。

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?