JUnit4での例外テストを楽チンにする!


2018/6/13追記
jUtaime は JUnit5 の登場によりその使命を終えました。
長らくご愛顧ありがとうございました m(_ _)m
みなさま JUnit5 を使いましょう。

What's an issue ?

JUnitでの例外検証コードをもっとスッキリ書きたいっ!
そう思っているのはきっと私だけではないはず・・・

before

例えば Person#setAge(int) なんていうよくあるメソッドを作ったとして、次の4点を検証したい。

  • setAge(-1) は NG → IllegalArgumentException をスロー
  • setAge(0) は OK
  • setAge(200) は OK : 超長寿社会の到来に備えて。
  • setAge(201) は NG → IllegalArgumentException をスロー、例外メッセージあり

通常のJUnit4ではこんな感じになると思います。

JUnit4での通常の例外テスト
public class PersonTest {

    private Person person = new Person("John");

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test(expected = IllegalArgumentException.class)
    public void testSetAge1() {
        person.setAge(-1);
    }

    @Test
    public void testSetAge2() {
        try  {
            person.setAge(0);
            person.setAge(200);  // 200歳まではOKとする。
        } catch(RuntimeException e) {
            fail();
        }
    }

    @Test
    public void testSetAge3() {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Is he a zombie!?");

        person.setAge(201);
    }
}

なーんでたったこれっぽっちの検証で、こんなにもっさりしたコードを書かなきゃいけないんですかね! ぷんぷん

after

これで良いじゃん...

こう書きたい
public class PersonTest {

    @Test
    public void testSetAge() {
        Person p = new Person("John");
        assertThat(of(() -> p.setAge(-1)),  raise(IllegalArgumentException.class));
        assertThat(of(() -> p.setAge(0)),   raiseNothing());
        assertThat(of(() -> p.setAge(200)), raiseNothing());
        assertThat(of(() -> p.setAge(201)), raise(IllegalArgumentException.class, "Is he a zombie!?"));
    }
}

so that

というわけで、afterを実現するライブラリを作りました。
nmby/jetaime · GitHub
nmby/jUtaime · GitHub (2015/7/26 移動しました。)

usage

  • 例外をスローしうる検証対象の処理を、Testee.of() の中に記述します。
  • 期待する例外の型やメッセージ、原因(cause)の型などを、RaiseMatchers.raise() などで指定します。
assertThat(Testee.of(SomeClass::someMethodShouldFail),
        RaiseMatchers.raise(SomeException.class, "expected message"));

TesteeRaiseMatchers は static インポートしておくとよいでしょう。
次のような色々な書き方が可能です。

assertThat(of(() -> Integer.valueOf("abc")), raiseExact(NumberFormatException.class));
assertThat(of(() -> Integer.valueOf("123")), raiseNothing());
assertThat(of(() -> { Object o = null; o.toString(); }), raise(RuntimeException.class));
assertThat(of(obj::dbOperation), rootCause(IOException.class));

また、他の Matcher と組み合わせて使用することもできます。
次の例では、hamcrest.org が提供する anyOfallOfnot と組み合わせて使用しています。

// NullPointerException または IllegalArgumentException がスローされることを検証
assertThat(of(() -> obj.someOperation(null)),
        anyOf(raise(NullPointerException.class), raise(IllegalArgumentException.class)));

// NullPointerException 以外の何らかの実行時例外を原因として上位例外がスローされることを検証
assertThat(of(() -> obj.someOperation(param)),
        allOf(raise(WrappingException.class, "expected message"),
                rootCause(RuntimeException.class),
                not(rootCause(NullPointerException.class))));

allOf() の代わりに、次の連結スタイルで記述することも可能です。

// NullPointerException 以外の何らかの実行時例外を原因として上位例外がスローされることを検証
assertThat(of(() -> obj.someOperation(param)),
        raise(WrappingException.class, "expected message")
                .rootCause(RuntimeException.class)
                .not(rootCause(NullPointerException.class)));

詳しくは javadoc を見てね♪

なお、eclipseで使えばちゃんとレポートも表示されます。

dependencies

  • java 8 以上が必要です。
  • hamcrest-core も必要ですが、JUnit4を使ってるならすでに入ってるはずです。