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な考え方だとは思うけど、複数の仕組みを組み合わせる時にこういう問題が起こるかもしれないよ、ってことで。