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