Hamcrestとは
Javaのテストライブラリです。
テストのassertionの際に利用するMatcherを提供することで、テストの可読性を向上させたり、エラー発生時の原因調査をやりやすくしてくれます。
Matcherとは
テスト対象のオブジェクトが特定の条件を満たしているかどうかを判定するためのインターフェースです。
assertThat
メソッドと組み合わせることで自然文法に近い形でテストを記述できます。
例
@Test
void matcherTest(){
String hoge = "hoge";
assertThat(hoge, is("hoge"));
}
変数hoge
is 文字列のhoge
というような記述で変数hogeを検査することができます。
内部ではequalsメソッドを使って同じ値であることを検査しています。
equalsがtrueになる場合はPASSし、falseになる場合はFAILします。
そのためこの例ではテストはPASSします。
FAILする例
@Test
void matcherTest(){
String hoge = "hoge";
assertThat(hoge, is("fuga"));
}
こちらはequalsがfalseとなるため、テストはFAILします。
この時以下のようなメッセージが出力されます。
java.lang.AssertionError:
Expected: is "fuga"
but: was "hoge"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:6)
・・・(以下スタックトレースの内容が表示されます)
期待値が fuga
だが検査対象の値が hoge
であるというメッセージです。
このように期待値と実際の値がログに出力されることで、テストがFAILした際の調査の手がかりとなってくれます。
Hamcrestではis以外にもたくさんのMatcherがデフォルトで用意されています。
参考
文字列に関するもの、数値に関するもの、Collectionに関するものなどさまざまなMatcherがあります。deepwikiのindexも作成されているので、必要なMatcherが存在するか聞いてみても良いと思います。
Matcherを自作するモチベーション
デフォルトで多くのMatcherが用意されているため、まずはそれらを利用することを検討するべきですが、場合によっては自作のMatcherを作成した方が良いパターンもあると思います。
自作のオブジェクトで複雑な検査を行う場合
テスト対象のメソッドの返り値が自作のオブジェクトの場合も、オブジェクト全体の同値比較の場合などは既存のMatcherが利用できます。(equalsが実装されていればOK)
しかし場合によっては一部のプロパティのみ比較したり、比較する内容がプロパティによって異なる場合もあると思います。そういった場合に用意されているMatcherで比較しようとすると記述が煩雑になってしまうことがあります。検査の出現頻度にもよりますが、自前のMatcherを作ることでテストコードが読みやすくなるのであれば自前のMatcherを作成する価値はあると思います。
FAILした際のエラーの表現力を向上したい場合
個人的にはこちらがかなり大きいと思っていますが、既存のMatcherを利用するよりもFAILした際のエラーの表現力が向上できる場合はMatcherを自作して良いと思います。
例えば動的に構築したSQL文やhtmlなどの大きな文字列を検査する場合です。
(可能であれば避けるべき設計だと思いますが、プロダクトの事情によってはやむを得ない場合もあるでしょう)
大きな文字列の場合は、既存の is
を使った際のエラーメッセージ(期待値と実値が表示されるだけ)ではどこに差異があるのかを見つけづらくなります。
同様に大量のプロパティがあるオブジェクトを検査する場合も、期待値と実値にそれぞれのオブジェクトの toString
の取得結果を出力するだけなので、どのプロパティに差異があるのかを見つけるのが難しくなります。
こういった場合は大きな文字列のどこに差分があるのか、大量のプロパティのどれに差分があるのか、といった内容を出力するとエラーメッセージから原因の箇所を発見しやすくなります。
実装方法
Matcherインターフェースを直接実装しても良いですが、型の決まったオブジェクトに実装する場合は TypeSafeMatcher
を継承すると、nullチェックなど実施してくれて少し楽です。
以下のメソッドを実装すれば簡単に自作のMatcherを作ることができます。
matchesSafely(実装必須)
TypeSafeMatcherに定義されているabstractメソッドです。
protected abstract boolean matchesSafely(T item);
実値のオブジェクトがitemとして渡ってくるので、期待値と比較し、PASSさせたい場合はtrue、FAILさせたい場合はfalseを返します。
describeMismatchSafely
protected void describeMismatchSafely(T item, Description mismatchDescription)
FAILした際のエラーメッセージを作成するメソッドです。
mismatchDescriptionにappendTextすることでエラーメッセージを構築できます。
こちらも実値のオブジェクトがitemとして渡ってくるので、実値の中身をメッセージに利用できます。
TypeSafeMatcherのSuperクラスのBaseMatcherに以下の実装があるため、実装は任意です。
(itemの toString
の結果が実値として返ります)
public void describeMismatch(Object item, Description description) {
description.appendText("was ").appendValue(item);
}
任意のstaticメソッド
assertThatの第2引数がMatcherなので自作のMatcherをnewすることでも利用できますが、インスタンスを返すstaticメソッドを作って利用するのがHamcrestっぽい書き方かなと思います。
describeMismatchを良い感じに実装すると期待結果が相違していた際の調査速度が上がって楽しいので、ぜひやってみてください。
おわり。