倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

続・JUnit 4 のアサーション

前回の記事にいくつか補足。

  • assertEquals() メソッドの引数の順序
  • org.hamcrest.CoreMatchers クラス
  • Assert#assertThat() と Is#is() で Class オブジェクト同士の equals 評価はできない!?

assertEquals() メソッドの引数の順序

これは JUnit 4.x だけに関する話ではないけど、org.junit.Assert#assertEquals() メソッドの引数の順序が分かりにくい、という不満がよく見られます。 JUnit 4.x で assertThat() メソッドを使う理由として挙げられているほど。 例えば Object 同士のアサーションを行う assertEquals() メソッドは

assertEquals( Object expected, Object actural );

となっていて、第1引数に期待される値 (expected) を、第2引数に実際の値 (actual) を渡す仕様になっています。 具体的にコードで書くと、String オブジェクト「str」が "abc" に等しいことのアサーションは

import static org.junit.Assert.assertEquals;

String str = ...;

assertEquals( "abc", str );    // これが正しい
assertEquals( str, "abc" );    // これは逆

となります。 アサーションが通ってしまえばどっちでもいいんですが、通らなかったときにがメッセージがおかしくなるのできちんとした順序で引数をかくべきでしょうね。 個人的にですが、これは「引数の順序が分からない」というよりは、「正しいとされている引数の順序に違和感がある」というのが実際のところではないでしょうか? 日本語でも英語でも「str は "abc" に等しい」、「str is "abc".」と、assertEquals() の引数の順序と逆順にした方が自然に感じますからね。

ではなぜ、assertEquals() メソッドの引数の順序が上記のように定められているのかというと・・・本当の理由は知りませんが、おそらく Object#equals() メソッドを使う際のイディオムから来ているのではないでしょうか? 例えば if-else 文で

String str = ...;

if(str.equals("abc")){
   ...
}else if(str.equals("xyz")){
   ...
}

みたいなコードを書くと str が null だった場合に予期せぬ NullPointerException が投げられてしまいます。 これを避けるために、こういう場合は以下のように書く方がいいとされてます(よね?):

String str = ...;

if("abc".equals(str)){
   ...
}else if("xyz".equals(str)){
   ...
}

これだと equals() メソッドが呼ばれるオブジェクトが null でないことが保証されるので、予期せぬ NullPointerException が投げられなくなります。

assertEquals() メソッドの引数の順序はおそらくこれに従ったものだと思います。 ただし、assertEquals() メソッドの第1引数に null を渡しても NullPointerException は投げられませんけどね。

org.hamcrest.CoreMatchers クラス

Assert#assertThat() メソッドの第2引数に渡す述語に対応する Matcher オブジェクトについて、前回の記事に載せたサンプル・コードでは、org.hamcrest.core パッケージにあるクラス Is や IsEqual のようなクラスの static メソッドを static import して使っていました。 この方法ではほとんど1つの述語に対して1つの static import 文を書かないといけなくて面倒でしたが、(JUnit の JavaDoc には出ていない)org.hamcrest.CoreMatchers クラスにこれらの Matcher オブジェクトを生成する static メソッドがまとめて定義されているので、これを一括して static import すると便利です:

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.*;

String str = ...;
assertThat( str, is(equalTo("abc")) );

まぁ、普通こういう使い方できるようにするよね。

Assert#assertThat() と Is#is() で Class オブジェクト同士の equals 評価はできない!?

Assert#assertThat() に使う述語のうち、最も使うであろう is() メソッド について。 is() メソッドは何気に3つのシグニチャがオーバーロードされています:

  • is(Matcher) ・・・ 引数の Matcher が true を返すならテストが通る
  • is(Object) ・・・ 主語が引数と Object#equals() メソッドで等しいならテストが通る (= is(equalTo(Object)))
  • is(Class) ・・・ 主語が引数の Class クラスのインスタンスならテストが通る (= is(instanceOf(Class)))

このオーバーロードに関して、主語が Class オブジェクトで、述語として何らかの Class オブジェクトと等しいことをテストしたい場合に上手く動作しない、という問題があるようです(達人プログラマーを目指して「実はJUnit4のassertThat()ってしっくりこないんです!(特に、メタプログラミングするレイヤでは)」)。 具体的には

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.*;

Class<?> type = List.class;
assertThat( type, is(List.class) );    // テストは通らない

assertThat( type, is(instanceOf(List.class)) );    // と同じと見なされる

実はJUnit4のassertThat()ってしっくりこないんです!(特に、メタプログラミングするレイヤでは)」に、これを解決するちょっとしたテクニックが書かれてますが、もっと簡単に以下のようにしても OK なようです:

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

Class<?> type = List.class;
assertThat( type, is((Object)List.class) );

まぁ、スマートなような、力ずくのような方法ですがw

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

テスト駆動開発入門

テスト駆動開発入門

  • 作者: ケントベック,Kent Beck,長瀬嘉秀,テクノロジックアート
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2003/09
  • メディア: 単行本
  • 購入: 45人 クリック: 1,058回
  • この商品を含むブログ (161件) を見る