Java のアサーションライブラリ AssertJ の時代が来そうな予感

Javaアサーションライブラリに AssertJ というものがある。
http://joel-costigliola.github.io/assertj/

Fluent にアサーションが書けるとのことなので、我がプロジェクトに導入してみました。

基本

基本的な比較。

Junit, Mockito だとこういう風に書くケースが・・・

assertEquals(expect, value); // JUnit
assertThat(value, is(expect)); // Mockito

こう書けます。

assertThat(value).isEqualsTo(expect); // AssertJ

「何が嬉しいの?」と思うかもしれませんが、とりあえず先に進みましょう。

文字列比較

よくある比較。コードの意図は説明しなくとも伝わると思います。

assertThat("Hello World").startsWith("Hello");
assertThat("Hello World").contains("llo Wor");
assertThat("Hello World").endsWith("World");

それでも、下記のように Mockito + Hamcrest で書いた場合と比べてそれほど嬉しいようには見えません。若干上記の AssertJ のほうが読みやすいような気はしますが、微々たるものです。

// Mockito + Hamcrest
assertThat("Hello World", is(startsWith("Hello")));
assertThat("Hello World", is(containsString("llo Wor")));
assertThat("Hello World", is(endsWith("World")));

真偽値比較

boolean の比較。メソッド名を除けばスタイル的には文字列比較の時とほとんど変わりません。

// AssertJ
assertThat(true).isTrue();
assertThat(false).isFalse();

こちらは Mockito 時代のもの。

// Mockito + Hamcrest
assertThat(true, is(true));
assertThat(false, is(false));

数値系比較

こちらも今までとほぼ同等。

// AssertJ
assertThat(100).isEqualTo(100);
assertThat(100).isGreaterThan(10);
assertThat(100).isLessThanOrEqualTo(200);

Mockito.

// Mockito + Hamcrest
assertThat(100, is(100));
assertThat(100, is(greaterThan(10)));
assertThat(100, is(lessThanOrEqualTo(200)));

ファイル系

ファイル系に関しては AssertJ のほうが優れています。
Mockito のデフォルトのマッチャーには File 関連のマッチャーは存在せず、org.hamcrest:hamcrest-library にも存在しないため、デフォルトで用意されている AssertJ のほうが読みやすさで上回っているように見えます。

// AssertJ
File file = new File("/tmp");
assertThat(file).exists();
assertThat(file).doesNotExist();
assertThat(file).hasExtension("java");

AssertJ のように hasExtension のようなメソッドが存在しないので、自前でパスの末尾が任意の拡張子で終わっているか確認しています。

// Mockito
File file = new File("/tmp");
assertThat(file.exists(), is(true));
assertThat(file.exists(), is(false));
assertThat(file.getAbsolutePath().endsWith(".java"), is(true));

List の比較

そろそろ AssertJ の魅力を知ってもらいましょう。コレクション系の比較がとてもグッドです。

例えば従来のテストでこんなコードがあったとしましょう。

List<String> names = Lists.newArrayList("apple", "orange", "banana");
assertThat(names, contains("apple", "orange", "banana"));

これはほぼ同じようなコードでこう書けます。

// AssertJ
List<String> names = Lists.newArrayList("apple", "orange", "banana");
assertThat(names).containsExactly("apple", "orange", "banana");

あまり変わらないですね。ではこっちはどうでしょう。

サイズを比較後、それぞれのユーザーオブジェクトの名前が期待するものかどうか比較してます。

// Mockito + Hamcrest
List<User> users = ... ;

assertThat(users, hasSize(3));
assertThat(users.get(0).getName(), is("joe"));
assertThat(users.get(1).getName(), is("brown"));
assertThat(users.get(2).getName(), is("ken"));

これは AssertJ の extracting 機能を使って、各オブジェクトのプロパティを抽出し、リストで比較できるのです。コード量がぐっと減りました。

// AssertJ
List<User> users = ... ;

assertThat(users).extracting("name").containsExactly("joe", "brown", "ken");

extracting はもっと評価されるべき。
もう一度言います。
extracting はもっと評価されるべき。

Set の比較

単純な文字列や数値の比較ならほぼ同じ。

// Mockito + Hamcrest
Set<String> names = Sets.newHashSet("apple", "orange", "banana");
assertThat(names, containsInAnyOrder("apple", "orange", "banana"));

今回の場合は containsOnly というメソッドが使えます。

// AssertJ
Set<String> names = Sets.newHashSet("apple", "orange", "banana");
assertThat(names).containsOnly("apple", "orange", "banana");

さて、比較したい要素が String や int ではなく、User クラスのようなオブジェクトだとして、各ユーザーの名前を比較したい場合はどうすれば良いのでしょう?Mockito + Hamcrest ではちょっとキツイです。一旦 Set な変数に抜き出して containsInAnyOrder することになり、やりたいことに対して書かなければならないコードが多めな印象です。でも AssertJ ならリストの時とほぼ同様の下記コードで簡単に比較できます。

// AssertJ
Set<User> users = ... ;
assertThat(users).extracting("name").containsOnly("joe", "brown", "ken");

さらに extracting は複数のプロパティを抽出でき、タプルのリストとして扱われます。よって、下記のように tuple メソッドを使って比較することもできます。

// AssertJ
Set<User> users = ... ;
assertThat(users).extracting("name", "age").contains(tuple("ken", 32));

念の為もう一度言っておきましょう。
extracting はもっと評価されるべき。

Map の比較

Map の比較。キーの長さを比較したり、キーの値を比較したり、エントリの比較など。

// Mockito + Hamcrest
Map<String, Object> user = new HashMap<>();
user.put("name", "joe");
user.put("age", 27);

assertThat(user.keySet(), containsInAnyOrder("name", "age"));
assertThat(user.keySet(), hasSize(2));
assertThat(user, hasEntry("name", (Object) "joe"));
assertThat(user, hasEntry("age", (Object) 27));

これが AssertJ だとこう書ける。

Map<String, Object> user = new HashMap<>();
user.put("name", "joe");
user.put("age", 27);

assertThat(user).containsOnlyKeys("name", "age");
assertThat(user).hasSize(2).containsEntry("name", "joe").containsEntry("age", 27);

そう、AssertJ だとアサーションを連続して書けるのです。

AssertJ の良いところ

コレクション系の比較は AssertJ はよさ気です。でも、上に紹介した部分だけでは、「それだけ?」感は否めません。
でもちゃんと AssertJ にも良いところがあるのです。それは、入力補完が効くところです。

たとえば boolean の比較時、

assertThat(true).isTrue();

というコードを書きましたが、下記のようには書けません。

assertThat("String").isTrue(); // compile error!

つまり assertThat(...) の次に続くメソッドは assertThat(...) の ... の型に応じて分岐しているので、IDE と組み合わせればアサーションを書く時が以前よりスムーズになるのです。書いた後のコードは mockito を用いた時のコードとあまり差が無くとも、書く時の快適さが AssertJ は優れているのです。

例えば下記のように、IDE が補完してくれるので、具体的なメソッド名を知らなかったりしててもだいたい何とかなるのです。
f:id:shawshank99:20151014085606p:plain

contains 系を調べたい時も、こんな感じで。
f:id:shawshank99:20151014085602p:plain

AssertJ の良いところ2

ここまで Mockito vs AssertJ のように書いてきましたが、この2つは共存できます。
よって、コレクション系の比較が多い箇所だけ AssertJ を導入する、というような使い方でも大丈夫。理想を言えば依存するライブラリは少ないほうが良いのですが、Mockito にも mock や ArgumentCaptor 等素晴らしい機能があるので、ここはひとつ両者のいいとこ取り、というセンでどうでしょう。弊社のプロジェクトでもそうしており、現状特に問題は起きてません。

まとめ

  1. テストコードでしか用いないライブラリで、
  2. コレクション系の比較が優れているのでテストコード行数の削減につながり、
  3. Mockito やもちろん JUnit とも共存できる

のでさくっと導入して良いのではないでしょうか。

システムテスト自動化 標準ガイド CodeZine BOOKS

システムテスト自動化 標準ガイド CodeZine BOOKS