Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Googleのアサーション用ライブラリTruthを試してみた

Guavaのテストコードを読んでいたらTruthというtesting frameworkが使われていることに気づき、最新の個人プロジェクトで使ってみました。まだアルファ版ですし、自分でも使い続けるかどうか微妙なところですが、試用記録として利点をまとめます。

なお著者がアサーションフレームワークに求めるのは、大人数が関わるプロジェクトにおける「開発者の個性(経験、知識、趣味)に限らず、短時間で保守性が高く直感的なコード・エラーメッセージが書ける」ことです。異なる観点からこのプロダクトを見ると、また違った意見があるかと思います。

assertThat()が必要とされた理由

そもそもassertThat()はなぜ必要なのでしょうか。それはassertTrue(), assertFalse() などのメソッドが生むエラーメッセージが直感的でないからです。

Truthのウェブサイトにのっている例が非常にわかりやすいです。直感的なメッセージにするには、結局文字列で状況を説明する必要があります。この方法は開発者の個性に依存してしまいますし、忙しい時などに文字列を書き漏れる恐れもあります。

List <Entity> entityList = service.search(condition);
assertTrue(entityList.isEmpty());
// -> AssertionError

assertTrue("No entity should be found under this condition", entityList.isEmpty());
// -> AssertionError: No entity should be found under this condition

JUnit4はこの問題に対してassertThat()という解決を用意しています。これによって、比較的可読性の高いメッセージを得ることができます。

List <Entity> entityList = service.search(condition);
assertThat(entityList, is(empty()));
// -> AssertionError: Expected: is an empty collection  but: <[unexpected entity]>

assertThat()には確認する値と、期待する状態を表すMatcherとを渡します。Matcherには様々なものが用意されており、ほぼすべての用途をカバーできると考えられます。

なおassertThat()を利用したコードには「英語として読んで意味が通じる」という利点もありますが、Truthでもそこは変わらないためここでは割愛します。

既存手法の持つ問題

ではなぜ代替が必要なのか。私は状況・目的に応じたMatcherを開発者が知らなければならないことが大きな問題と考えます。

例えばCoreMatchersMatchersだけでも、以下の記事で紹介されている通り様々なMatcherがあります。この他にJUnitMatchers(JUnit4.4以降)もあります。これらを知らないと、assertThat(age < 30, is(true)) のようなエラーメッセージ生成に役立たないMatcherの使い方をしてしまいかねません。

他にもassertThat()で生成されたエラーメッセージには「どの値が」問題であったか(上記の例で言うと「entityListが」問題だった)という情報が含まれておらず、結局テストコードを読まなければどんな処理がどのような理由で失敗したのかがわかりません*1IDE単体テストをガンガン回すテスト駆動開発であれば問題になりませんが、テストが落ちた時の対応はコスト高です。

Truthがもたらす解決

Truthはこの問題に対して、メソッドチェーンとnamed()を導入することで解決を図っています。

Truthは期待する状態を表すコード(前述のMatcherに相当)を、引数でなくメソッドチェーンにて記述します。 メソッドチェーンにより、assertThat(...)を書いた時点で、利用可能なメソッドIDEにより一覧表示されます。開発者はすべてのMatcherとその用途を記憶する必要がありません。必要なもののみが、必要なタイミングで、表示されます。

assertThat(someInt).isEqualTo(5);
assertThat(someCollection).contains("a");
assertThat(aMap).containsKey("foo")

また「何が」期待通りでなかったかを補足するためにnamed()メソッドを提供します

// Reports: "hasError()" is unexpectedly false
assertThat(myBooleanResult).named("hasError()").isTrue();

メッセージそのものを指定するwithFailureMessage()も用意されていますが、個人的にはこちらは使わないで良いのではと思います*2

他にもMultimapのようなGuavaが提供するCollectionフレームワークにデフォルトで対応していたり、自分で新しい型に対応させられたりCollectionの要素に対するAssertionを書けたりと、細かいところで気が利いています。

Truthの問題点

以上、良い所も多いプロダクトですが、最近はさほど活発に更新されてはいないようです。アルファ版と言え枯れつつあるからなのか、まだユーザが少ないのかは不明ですが、Mavenリポジトリの情報を見るとまだ利用数が少ないため後者ではないかという印象です。

またアサーションフレームワークには他にも有名なものがいくつかありますが、それらとの違いに関する公式ドキュメントは現時点ではありません。ドキュメント以外では一応、Issueのコメントに拡張性に違いがあると述べられています。Truthが”very alpha”だという点が気になるようであれば、AssertJが良い選択肢になるかもしれません。

以上、なかなか魅力的なプロダクトだと思いますがいかがでしょうか。

*1:1テストメソッド1アサーションを貫けるなら、あるいは……

*2:そもそも私の関心は「技術者の個性に左右されないテストメッセージの生成」にあるので