hamcrestのMatcherメモ
技術ネタじゃないところで盛り上げてしまった。技術ネタいこう、技術ネタ。
さて、JUnitを使う際、hamcrestライブラリを使って、英語として読めるようなassertionを書く、なんてのは流行ってたり流行っていなかったり?
JUnit4限定だけれど、assertionの際、assertEqualsとか色々assertionのメソッドはあるけど、全てassertThatで書くことができるはず。assertThatメソッドの第一引数にテスト対象、第二引数にhamcrestのMatcherインターフェイスの実装を与えます。なんのこっちゃですが。
Jiemamyでは、なるべくassertThat以外のassertionメソッドを使わないようにテストを書いています。(もしかしたらもう一つも残ってないかも。)
まぁ、以下のように書くと、英語っぽいのが書けますよ、と。
assertThat(aaaa, is(not(equalTo(bbbb))));
カッコとカンマを全部取り除くと assert that aaaa is not equal to bbbb. となる。英語として読めるよね。aaaaはbbbbとは異なる、という表明です。こうすることによって、まさにコード自身がコメントのような役割を果たします。
その他色々なパターンがあるので、片っ端からまとめてみました。ちなみに、以下のassertionは、全て通過するように書いてあります。それぞれの解説はしませんが、上記のように読んでみると、何をassertしたいのか読み取れると思います。staticインポートを上手く使っているのにも注目ですね。
import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace; import static org.hamcrest.Matchers.eventFrom; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.hasXPath; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.typeCompatibleWith; import static org.junit.Assert.assertThat; import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EventObject; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.custommonkey.xmlunit.DetailedDiff; import org.custommonkey.xmlunit.Diff; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.Test; import org.w3c.dom.Document; public class HamcrestTest { @Test public void testHamcrestBasic() { String s = "foo"; String s2 = s; String s3 = "foobarbaz"; String s4 = "foo bar baz"; String n = null; Class<?> c = String.class; // --- 基本形 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(s, is(equalTo(s2))); // assert that s is equal to f2. [equals()による比較] assertThat(s, is(not(equalTo(s3)))); assertThat(s, is(sameInstance(s2))); // assert that s is same instance of f2. [== による比較] assertThat(s, is(not(sameInstance(s3)))); assertThat(s, is(instanceOf(String.class))); // assert that s is instance of String. assertThat(c, is(typeCompatibleWith(CharSequence.class))); // assert that c is type (which is) compatible with CharSequence class assertThat(c, is(not(typeCompatibleWith(Number.class)))); // --- String系 assertThat(s, is(equalToIgnoringCase("FOO"))); assertThat(s4, is(equalToIgnoringWhiteSpace("foo bar baz"))); assertThat(s3, startsWith("foo")); assertThat(s3, endsWith("baz")); assertThat(s3, containsString("bar")); // --- 数値系 double num = 1.0; assertThat(num, is(greaterThan(0.5))); // 1.0 > 0.5 assertThat(num, is(greaterThanOrEqualTo(1.0))); // 1.0 >= 1.0 assertThat(num, is(lessThan(1.1))); // 1.0 < 1.1 assertThat(num, is(lessThanOrEqualTo(1.0))); // 1.0 <= 1.0 assertThat(num, is(closeTo(0.95, 0.1))); // 1.0 = 0.95±0.1 assertThat(num, is(not(closeTo(0.95, 0.01)))); // 1.0 != 0.95±0.01 } @Test public void testHamcrestCollection() { String f = "foo"; String[] a = { "foo", "foobar", "foobarbaz" }; Collection<String> c = Arrays.asList("foo", "bar", "baz"); Map<String,String> map = new HashMap<String, String>(); map.put("A", "a"); map.put("B", "b"); // --- コレクション系 assertThat(c, hasItems("bar", "baz")); assertThat(f, isIn(c)); assertThat(f, isOneOf(a)); assertThat(map, hasEntry("A", "a")); assertThat(map, not(hasEntry("A", "b"))); assertThat(map, not(hasEntry("Z", "z"))); assertThat(map, Matchers.<String, String>hasKey("A")); assertThat(map, not(Matchers.<String, String>hasKey("Z"))); assertThat(map, Matchers.<String, String>hasValue("a")); assertThat(map, not(Matchers.<String, String>hasValue("z"))); } @Test public void testHamcrestEvent() throws Exception { Object o = new Object(); EventObject ev = new EventObject(o); Object o2 = new Object(); EventObject ev2 = new EventObject(o2); assertThat(ev, is(eventFrom(o))); assertThat(ev, is(not(eventFrom(o2)))); assertThat(ev2, is(eventFrom(o2))); assertThat(ev2, is(not(eventFrom(o)))); } @Test public void testHamcrestXml() throws Exception { final String XML1 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +"<foo hoge=\"hoge\" fuga=\"fuga\">\n" +" <bar>baz</bar>\n" +"</foo>\n"; final String XML2 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +"<foo fuga=\"fuga\" hoge=\"hoge\">\n" +" <bar>baz</bar>\n" +"</foo>\n"; DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document document = documentBuilder.parse( new ByteArrayInputStream(XML1.getBytes("UTF-8"))); assertThat(document, hasXPath("/foo/bar", is("baz"))); // hamcrestじゃなくて、xmlunitをご紹介。便利だったのでついでに。 DetailedDiff diff = new DetailedDiff(new Diff(XML1, XML2)); assertThat(diff.getAllDifferences().toString(), diff.similar(), is(true)); } @Test public void testHamcrestBean() throws Exception { Hoge hoge = new Hoge(); assertThat(hoge, hasProperty("fuga")); assertThat(hoge, not(hasProperty("ponyo"))); hoge.setFuga("wooo"); assertThat(hoge, hasToString(equalTo("Hoge[wooo]"))); // ↑は↓と同じ assertThat(hoge.toString(), is(equalTo("Hoge[wooo]"))); } @Test public void testHamcrestLogical() { String f1 = "foobarbaz"; String f2 = "foo bar baz"; Collection<Matcher<? extends String>> matchers = new ArrayList<Matcher<? extends String>>(); matchers.add(containsString("foo")); matchers.add(containsString("oob")); matchers.add(containsString("arb")); // f1はすべてを満たす assertThat(f1, is(allOf(matchers))); // f2は何れか一つを満たす assertThat(f2, is(anyOf(matchers))); } private class Hoge { private String fuga; public String getFuga() { return fuga; } public void setFuga(String fuga) { this.fuga = fuga; } @Override public String toString() { return "Hoge[" + fuga + "]"; } } }
追記
Matcher の static import は * でいいと思うんだな。テストコードだし。
はてなブックマーク - Yamashiro0217のブックマーク / 2009年7月10日
っていうツッコミがあったけど、これには理由がある。といっても、正確な理由は忘れてしまったw
いやね、Jiemamyではstatic importについても*を禁止しているのは、確か、mockitoのstatic importと併用した時に識別子名カブりが起こるんだったと思う。まぁ、テストだから*でいい、ってのはおおむねOKな考え方だとは思うけど、複数の仕組みを組み合わせる時にこういう問題が起こるかもしれないよ、ってことで。