例外を捕捉するテストの書き方

例外が送出されることを期待する Javaユニットテストの書き方は、テストメソッドの中に例外を捕捉するコードを書いて、期待する例外を捕捉したら return し、メソッドの末尾では fail を呼び出すような書き方をするのが常道のよう。

public void test001() {
  try {
    testee.dont_pass_me_null(null);
  } catch (IllegalArgumentException e) {
    return;
  }
  fail();
}

public void test002() {
  try {
    testee.dont_pass_me_zero(0);
  } catch (IllegalArgumentException e) {
    return;
  }
  fail();
}

public void test003() {
  try {
    testee.open_file("");
  } catch (IOException e) {
    return;
  }
  fail();
}
...

ただ、書いているうちにこの try catch がすごくつらくなってくる。むっちゃ繰り返しやん!

こんな風に書けたらいいなあ…:

public void test001() {
  assertTrue(catch_<IllegalArgumentException>(testee.dont_pass_me_null(null)));
}

public void test002() {
  assertTrue(catch_<IllegalArgumentException>(testee.dont_pass_me_zero(0)));
}

public void test003() {
  assertTrue(catch_<IOException>(testee.open_file("")));
}
...

とおもったけれど、 Java にはテンプレート関数の概念はなく、また Java のテンプレートの仕組みである Generics では Throwable なオブジェクトは指定できないのだとか。

It is a compile-time error if a generic class is a direct or indirect subclass of Throwable.

Java SE Specifications

ところで、 C++ でも template bool catch_(U v); のような関数では意味がない。

というのは引数が積極評価されるため、さきに testee.xxx() の例外送出関数が実行されて例外が発生し、テンプレート関数の catch_ の実行にまでまわらないから。

つまり引数で指定したテスト対象の関数実行を catch_ 関数のなかで評価するよう遅延させないといけない。そうなると簡単なのはマクロ。あるいは lambda でくるむか。 CppUnit ではたしか、マクロで例外送出を assert できた。

lambda におもいいたったところで Java に戻って、こんな具合には書けるけれど…:

boolean catch_IllegalArgumentException(Runnable method) {
  try {
    method.run();
    return false;
  } catch (IllegalArgumentException e) {
    return true;
  }
}

boolean catch_IOException(Runnable method) {
  try {
    method.run();
    return false;
  } catch (IOException e) {
    return true;
  }
}
...

public void test001() {
  assertTrue(catch_IllegalArgumentException(new Runnable() {
    public void run() {
      testee.dont_pass_me_null(null);
    }
  }));
}

public void test002() {
  assertTrue(catch_IllegalArgumentException(new Runnable() {
    public void run() {
      testee.dont_pass_me_zero(0);
    }
  }));
}

public void test003() {
  assertTrue(catch_IOException(new Runnable() {
    public void run() {
      testee.open_file("");
    }
  }));
}
...

いちいち無名クラスを実体化せねばならないあたりでダメだね。残念。

Test アノテーション

と、ここまで書いたところで @koic さんからコメントをいただいた。多謝!

JUnit 4 だとアノテーション使ってできませんでした?@Test(expected=例外クラス名.class) な感じです。

https://twitter.com/#!/koic/status/71415957058359296

わたしの作業対象プラットフォームで使われているのが JUnit3 だったため試せていないのだけれど、 Test アノテーションを使うことで例外を捕捉するテストが書けるようになっているそう。こんなかんじに:

@Test(expected=ArithmeticException.class) 
  public void divideByZero() {
    int n = 2 / 0;
}
An early look at JUnit 4

すばらしく簡潔。メソッド名が test ではじまらなくてもよくなるというのもグッドポイント。