実はJUnit4のassertThat()ってしっくりこないんです!(特に、メタプログラミングするレイヤでは)

私のように昔からJUnitのコードを書くことが習慣となっていると、値の検証はassertEquals(期待値, 実際値)メソッドで行うというというのがずっと常識となっていました。しかし、4年ほど前にリリースされたJUnit4.4以降では、長年親しんできたassertEquals()に加えて、新しくassertThat()と呼ばれる別の検証メソッドが追加されています。この新しい検証メソッドについては、既にいくつかのサイトで説明されています。
http://journal.mycom.co.jp/articles/2007/07/20/junit1/index.html
林檎生活100: JUnit 4.4 の機能,assertThat と Assumptions,Theories について.
hamcrestのMatcherメモ - 都元ダイスケ IT-PRESS
hamcrest の CoreMatchers 詳細 - A Memorandum
いくつかの例を引用してみると以下のとおりです。

    assertThat(n, is(nullValue())); // assert that n is null value.
    assertThat(s, is(notNullValue())); // assert that s is not null value.
    assertThat(s, is("foo")); // asser that s is "foo".
    assertThat(s, is(not(s3))); // assert that s is not f3.
    assertThat(num, is(greaterThan(0.5)));	// 1.0 > 0.5
    assertThat(num, is(greaterThanOrEqualTo(1.0))); // 1.0 >= 1.0

なるほど、確かにこの新しい方式を使うことで流れる英文のような表現でテストの検証条件を記述でき、テストの可読性も向上しそうです。
遅ればせながら、私も今後はこの新しい方式を使ってテストを記述するようにしようとちょっと試しに使ってみたのですが、まだ、初心者のためか、結構はまる箇所がいくつかいろいろと見つかり、現時点ではまだ試行錯誤の段階ですね。
最初にはまったのが、Classクラス同士の厳密な比較を行うことができなかったことです。
たとえば、先日紹介したSpringのTypeDescriptorの動作確認試験で、昔の書き方だと

    assertEquals(List.class, typeDesc.getType());

のようにして結果がListクラスのインスタンスを返してくることを検証可能なのですが、これを単純にassertThat()形式に書き換えて、以下のようにすると

    assertThat(typeDesc.getType(), is(List.class));

以下のとおり例外となりアサーションが通りません。

java.lang.AssertionError: 
Expected: is an instance of java.util.List
     got: <interface java.util.List>

	at org.junit.Assert.assertThat(Assert.java:778)
	at org.junit.Assert.assertThat(Assert.java:736)
	at com.github.ryoasai.TypeDescriptorTest.testResolveFieldType(TypeDescriptorTest.java:39)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:49)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

こうなる原因は、JUnitJava DOCを見ればわかるのですが、is()メソッドでClassクラスをパラメーターにとるメソッドがオーバーロードされているのが原因です。
http://kentbeck.github.com/junit/javadoc/latest/org/hamcrest/core/Is.html
ちなみに、以下のようにすると今度はコンパイルが通りません。

    assertThat(typeDesc.getType(), is(equalTo(List.class)));
   // 型 Assert のメソッド assertThat(T, Matcher<T>) は引数 (Class<capture#3-of ?>, Matcher<Class<List>>) に適用できません

このとき出たつぶやきが以下のとおり。

そうしたら、@kompiroさんとid:ouobpoさんから以下のコメントをいただきました。
それ以降のやり取りは以下にトゥギャています。
JUnit4のassertThat()って便利なの? - Togetter
確かに、Class同士の比較は業務ロジックを書くアプリケーションやドメインのレイヤでは稀なのかもしれません。でも、以下のようなケースはどうでしょうか。

    BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(person);		
    assertEquals("test", beanWrapper.getPropertyValue("name"));

これはSpringのbeanWrapperを用いてBeanのプロパティに動的にリクレクションでアクセスする例です。しかし、これを単純に以下のように書き換えてもコンパイルが通りません。

    BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(person);		
    assertThat(beanWrapper.getPropertyValue("name"), is("test")); // コンパイルエラー
  // 型 Assert のメソッド assertThat(T, Matcher<T>) は引数 (Object, Matcher<String>) に適用できません

コンパイルが通らない原因は第2引数で渡したMatcherが型Tで型パラメーター化されているからで、この場合第一引数にString型を渡す必要があるからです。正しくコンパイルするためには以下のように第1引数に対して明示的なキャストが必要となります。

    BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(person);		
    assertThat((String)beanWrapper.getPropertyValue("name"), is("test"));

ただし、これだともともとの目的だった英文のような可読性は一気に下がってしまいあまり美しくありません。
今のところの感想としては

  • Javaだとメソッドの括弧が省略できないのが面倒
  • よほどの英語脳でないと可読性が高まったとは思えない
  • クラス同士の比較が困難(後述するように、Matcherの独自拡張で可能)
  • greaterThan()やnot()などは普通に式で表現した方が簡潔でわかりやすい
  • 半端に型付けされていて柔軟性がない

などの問題があり、どうもしっくりこないんです*1というのが率直なところです。もちろん、習熟していけば、いろいろな解決のテクニックが工夫できるとは思いますが、特に、Classクラスやリクレクションを多用するようなインフラ層のフレームワークメタプログラミング的にコーディングするような場合には、無駄に不便さが目立つような気がします。Javaの半端な型推論の問題もありますが、静的な型付けによるチェックがテストフレームワークとしてはいまいち相性が悪いような気がするのですね。*2
JUnitのassertThat()はパラメーターの順序が従来のassertEquals()と逆順になるため、混在させることも可読性の意味でいまいちなのですよね。assertThat()はアプリケーション層専用にし、インフラ層はあえてassertEquals()に統一するというのも現実的にはあるのかと思いました。あるいはテストケースは動的言語のGroovyで書き直すといったのも、生産性の問題を考えるとありなのではないかと思います。
(追記)
その後、早速id:shuji_w6eさんとid:koichikさんより以下のアドバイスをいただきました。

JUnit4では、実質的にassertEquals()はレガシーな部分として認識すべきであり、互換性の問題を除いて使用は避けるべきということだと思います。この場合、必要に応じて独自のMatcherクラスを作成することが重要なポイントのようですね。確かにその発想が欠けていました。CDIでも独自のアノテーションをアプリケーションのプログラマーが普通に作ったりすることを求められますが、今後プログラマーとしてはこのように独自にDSLを拡張するようなスキルも重要になるのだと思います。
(さらに追記)

ということで、JUnit3の方式をレガシーとして捨てさるのも簡単にはできないというか、なかなか複雑なところがあります。現在は昔と違ってJUnitだけでなくTDDやBDDなどをサポートする他のフレームワークが存在します*3し、言語も対象がJavaであっても必ずしもJavaでテストを書く必然性もないのですから、目的に応じていろいろなやり方を研究して取り入れることが大切なのだと思いました。
ちなみに、当初の目的であるClassクラスの比較をするのが目的であればid:koichikさんのアドバイスをヒントに、以下のようなメソッドを定義して、

    public static Matcher<Class<?>> isSameClassAs(Class<?> clazz) {
        @SuppressWarnings("unchecked") //ダブルキャストイディオム
        Matcher<Class<?>> result = (Matcher<Class<?>>)(Matcher<?>)is((Object)clazz);
        
        return result;
    }

static importすれば、以下のように記述できますね。やり方がわかってしまえば、特に何のことは無いというあっけない感じですが。

    assertThat(beanWrapper.getPropertyType("name"), isSameClassAs(String.class));

*1:もちろん、元ネタは以下実はオブジェクト指向ってしっくりこないんです!:気分はstatic!:エンジニアライフ

*2:このように書くと老害と呼ばれて若い人からいじめられそうですが。

*3:たとえば、GroovyならSpockというライブラリーが便利らしい。http://d.hatena.ne.jp/backpaper0/20110421/1303399381