JUnitで自作Matcherを作ってみた
JUnitで自作Matcherを作ってみた
JUnitでは、ListやHash自体のチェック(xxxを含んでいる、リストが空)はできるが、
"返り値xxxxは次の配列の要素のいずれか"というチェックができないので、自作Matcherを作ってみることにした。
案外さっくりできたけど、例外チェック甘いので手裏剣(not マサカリ)を投げてくれればありがたいです。
環境
どうやら4.11から4.12にかけてhamcrest周りでいろいろ変更があったらしく、4.12で動かすには追加でライブラリが必要になります。
org.hamcrest.hamcrest-core 1.3
要件
- メソッドを呼び出した返り値がenumで、定義されているenumのうち数種類のいずれかの可能性がある。
- 文脈に沿ったassertThatを使用したい
- 既に用意されている方法でやろうとすると
assertThat([A, B, C], hasItem(returnVal))
となり、expectedとactualが逆になるので文脈的におかしい。 assertThat(returnVal, is(includedIn([A, B, C])))
と書けるようなカスタムMatcherを用意する。
- 既に用意されている方法でやろうとすると
実装
こちらがMatcher宣言クラス。
import文は省略する。
package local.aibou.junit.matcher; public class IsIncludedIn extends BaseMatcher<Status> { private final List<Status> statuses; private Object actual; public IsIncludedIn(List<Status> statuses) { this.statuses = statuses; } @Override public boolean matches(Object actual) { this.actual = actual; if(!(actual instanceof Status)) { return false; } for(Status status : this.statuses) { if(status == actual) { return true; } } return false; } @Override public void describeTo(Description description) { if(this.actual != null) { description.appendValue(this.actual); description.appendText(" is not included in "); } description.appendValueList("[ ", ", ", " ]", statuses); } public static Matcher<Status> includedIn(List<Status> statuses) { return new IsIncludedIn(statuses); } }
要は、一番最後のstaticメソッドincludedIn
で、matches
とdescribeTo
を実装したMatcher<T>
クラスを返すようなメソッドを定義してやればいい。
なお、このTクラスはactualのクラスと同じクラス(と子クラスも可能?)である必要がある。
上記クラスを使用したテストコードが以下
@Test public void isIncludedInテスト(){ List<Status> list = Arrays.asList(Status.Hoge, Status.Fuga, Status.Piyo); assertThat(Status.Hoge, is(includedIn(list))); assertThat(Status.Other, is(not(includedIn(list)))); }
Java 8だとラムダ式が使えそうな気がするので、より完結なコードになる可能性もある。
実行結果
テスト成功時はなにも表示されないので、テスト失敗時にどういう出力がされるか、出力結果をコピペする。
(上記最終行のis(not())
をis()
に変更して実行した。)
java.lang.AssertionError: Expected: is <Status.Other> is not included in [ <Status.Hoge>, <Status.Fuga>, <Status.Piyo> ] got: <Status.Other> <Click to see difference> at org.junit.Assert.assertThat(Assert.java:780) at org.junit.Assert.assertThat(Assert.java:738) at local.aibou.AppTest.isIncludedInテスト(AppTest.java:23) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at org.junit.runner.JUnitCore.run(JUnitCore.java:157) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:202) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:65) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
is <Status.Other> is not included in [ <Status.Hoge>, <Status.Fuga>, <Status.Piyo> ]
ってその通りじゃん、
という感じだが、descriptionTo
のメソッド内容が出力されてしまう。
ここは非常に難しい問題だと思う。後述参照
ちなみに、describeTo
メソッドが空の場合はこんな感じの出力になる。
java.lang.AssertionError: Expected: is got: <Status.Other> <Click to see difference> at org.junit.Assert.assertThat(Assert.java:780) at org.junit.Assert.assertThat(Assert.java:738) at local.aibou.AppTest.isIncludedInテスト(AppTest.java:23) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222) at org.junit.runners.ParentRunner.run(ParentRunner.java:300) at org.junit.runner.JUnitCore.run(JUnitCore.java:157) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:202) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:65) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Expected節が is しか表示されなくなるので、こちらも非常に気持ち悪い。
考察
パッケージ名をどうするか
Projectのルートとなるパッケージの配下でjunit
というパッケージを切ればよさそう。
例)Project名がinner-apiとかなら、jp.ameba.kiban.innerapi.junit
とか
カスタムMatcher自体のテストはどうするか
もちろんするべき。
actual はObject型で受け取るので、期待とは違うクラスやNULLなどの例外チェックはしておいたほうがいいと思われる。
パッケージ名は上記参照。
JUnitのバージョンについて
上記でも述べたが、4.11から4.12にかけて破壊的な変更が加えられているので、 hamcrest-coreパッケージ追加するだけで動作するかの保証は正直できない。 各プロジェクトにあわせて設定しよう。
共通JUnitライブラリを作るべきか
事業部等で使いまわす用の共通JUnitカスタムMatcherライブラリを用意すべきかという話。
便利なものがあればいいが、正直なところ微妙。
パッと考える限り、事業部共通で使える=OSSとして公開できる代物であると考えられるし、
(今回の実装例にもある通り、)プロジェクト内にとどまるようなカスタムMatcherになると考えられる。
つまり、いちプロジェクトものか、万人が使えるものかという二極になると思う。
自分の担当プロジェクトはよくxxx-api(xxxはプロジェクト名)やxxx-coreというリポジトリで区切られているので、xxx-coreに追加すればよさそう。
Descriptionはどう書くべきか
正直なとこと、よくわかっていない。
出力形式がかなり制限されているので、テスト失敗時のメッセージが非常に見難いものになる可能性が高い。
これはカスタムMatcherの宿命な気もするので、今後に期待といったところか。
結論
Rubyで開発したいです