Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

JDaveの寄り道にhamcrestを試してみる。

JUnit4でも使えるマッチングライブラリ「hamcrest」。

ただ、hamcrestはjarが分かれているから注意する。

  • hamcrest-core-1.1.jar
  • hamcrest-library-1.1.jar

JUnit4.4に同梱されているのはcoreの方だけ。
libraryはsugerの定義が多いけど、機能的な追加もあるっぽい。

libraryを入れていれば、マッチングはすべてMatchersクラスから利用できる。
たとえばequalTo()メソッド。

public class Matchers {
  public static <T> org.hamcrest.Matcher<T> equalTo(T operand) {
    return org.hamcrest.core.IsEqual.equalTo(operand);
  }
}

coreのIsEqualクラスに委譲しているだけ。
BDDもそうだけど、何か最近sugerをたくさん定義しているライブラリは多いね。
これは自然言語に近づけるための工夫だし、いいことだと思う。
オブジェクト指向がメッセージパッシングであるのなら、そのコードが自然言語に近づくのは必然な気がする。
「誰が〜する(してください)」というのがメッセージだから。
(ここで英語と日本語どちらのの文型がよりオブジェクト指向なのかという問題が出てくるけど、よくわからない。)


公式ドキュメントのsugerの項。

Sugar

Hamcrest strives to make your tests as readable as possible. For example, the is matcher is a wrapper that doesn't add any extra behavior to the underlying matcher. The following assertions are all equivalent:

assertThat(theBiscuit, equalTo(myBiscuit));
assertThat(theBiscuit, is(equalTo(myBiscuit)));
assertThat(theBiscuit, is(myBiscuit));

The last form is allowed since is(T value) is overloaded to return is(equalTo(value)).

Tutorial - hamcrest - Hamcrest - library of matchers for building test expressions - Google Project Hosting

equalTo()もis()もis(equalTo())も全部同じ。
Javadocにも書いてある。

    /**
     * Decorates another Matcher, retaining the behavior but allowing tests
     * to be slightly more expressive.
     *
     * eg. assertThat(cheese, equalTo(smelly))
     * vs  assertThat(cheese, is(equalTo(smelly)))
     */
    @Factory
    public static <T> Matcher<T> is(Matcher<T> matcher) {
        return new Is<T>(matcher);
    }

「slightly more expressive」(少しでもより表現力に富むように)。
ちなみに@Factoryアノテーションはhamcrestで定義したアノテーションで、ファクトリメソッドを表す。

/**
 * Marks a Hamcrest static factory method so tools recognise them.
 * A factory method is an equivalent to a named constructor.
 * 
 * @author Joe Walnes
 */
@Retention(RUNTIME)
@Target({METHOD})
public @interface Factory {
}

hamcrestを使ってマッチングをするサンプルを作ってみた

	@Test
	public void testAssertThatWithEqualTo() {

		Assert.assertThat("test", Matchers.equalTo("test"));
		Assert.assertThat("test", Matchers.equalToIgnoringCase("TEST"));
		Assert.assertThat("test", Matchers
				.equalToIgnoringWhiteSpace("    test    "));

		Assert.assertThat(10, Matchers.equalTo(new Integer(10)));
	}

ここで注意。org.junit.Assertを使うこと。junit.framework.Assertじゃないよ。
サンプルはわかりやすくするためにクラスを書いてstaticメソッドを呼び出してるけど、実際はstaticインポートを使うほうがいいね。AssertとかMatchersとか。


で、AssertクラスにJUnit4.4で追加されたassertThat()メソッドを使うとhamcrestのマッチングが利用できる。
ほとんどメソッド名のとおり。
Matchers.equalTo()はObject#equals()メソッドでの検証。equalToIgnoringCase()とequalToIgnoringWhiteSpace()はStringにしか使えなくて、それぞれ大文字小文字や前後のスペースを無視する。

文字列
	@Test
	public void testAssertThatConcerningString() {

		Assert.assertThat("test", Matchers.containsString("es"));
		Assert.assertThat("test", Matchers.startsWith("tes"));
		Assert.assertThat("test", Matchers.endsWith("st"));

	}

containsString()はその文字列を含んでいるかを検証する。
startsWith()、endsWith()はその文字列で始まるか終わるかを検証する。

数値
	@Test
	public void testAssertThatConcerningNumber() {

		// clothTo()は誤差範囲にあるか検証する
		Assert.assertThat(1.01d, Matchers.closeTo(1.001d, 0.01d));
		Assert.assertThat(0.991d, Matchers.closeTo(1.001d, 0.01d));

		// 1.01 は 1.001 +- 0.005 の範囲にないのでfailureとなる
		// Assert.assertThat(1.01d, Matchers.closeTo(1.001d, 0.005d));

		Assert.assertThat(11, Matchers.greaterThan(10));
		Assert.assertThat(11, Matchers.greaterThanOrEqualTo(11));
		// lessThan(), lessThanOrEqualTo()もある

	}

closeTo()がけっこうおもしろくて、doubleの検証に使う。第1引数に基準となる数値、第2引数に誤差の範囲を渡す。
基準 +- 誤差の範囲にあればtrueってこと。

あと、数値の比較にgreaterThan()、lessThan()がある。より大きい、より小さいってやつ。基準を含む場合はgreaterThanOrEqualTo()、lessThanOrEqualTo()。

コレクション
	@Test
	public void testAssertThatConcerningCollection() {
		
		List<Integer> list = new ArrayList<Integer>();
		list.add(1);
		list.add(2);
		list.add(3);
		
		Assert.assertThat(list, Matchers.hasItem(2));
		// hasItems()メソッドの引数は可変長引数である
		Assert.assertThat(list, Matchers.hasItems(2));
		Assert.assertThat(list, Matchers.hasItems(1, 2, 3));
		
		Integer[] integers = {new Integer(1), new Integer(2)};
		Assert.assertThat(integers, Matchers.hasItemInArray(1));
		
		Map<Integer, String> map = new HashMap<Integer,String>();
		map.put(1, "one");
		map.put(2, "two");
		map.put(3, "three");
		
		Assert.assertThat(map, Matchers.hasEntry(1, "one"));
		// MapのkeyとvalueそれぞれにMatcherを適用できる
		Assert.assertThat(map, Matchers.hasEntry(Matchers.equalTo(2), Matchers.equalTo("two")));

	}

その要素があるかを検証するのがhasItem()。可変長引数を使ってhasItems()を呼び出せば、複数の要素でも検証できる。
コレクションじゃなくて配列に対して検証するときはhasItemInArray()。
MapはhasEntry(key, value)で。キーだけならhasKey(key)、バリューだけならhasValue(value)を使う。

Bean
	@Test
	public void testAssertThatConcerningBeans() {
		
		Item item = new Item();
		item.setName("test");
		
		Assert.assertThat(item, Matchers.hasProperty("name"));
		Assert.assertThat(item, Matchers.hasProperty("name", Matchers.equalTo("test")));
	}

Beanに指定したプロパティがあるかを検証するhasProperty()。プロパティとその値を検証するときは引数が2つのものを使う。

Object
	@Test
	public void testAssertThatConcerningObject() {
		
		Item item = new Item();
		item.setName("test");
		
		// 第1引数オブジェクトのtoString()を検証する
		Assert.assertThat(item, Matchers.hasToString(Matchers.equalTo(item.toString())));
		
		// Class#isAssignableFrom()で検証する
		Assert.assertThat(item.getClass(), Matchers.typeCompatibleWith(Serializable.class));
		
		Assert.assertThat(item.getClass(), Matchers.instanceOf(Serializable.class));
		
		Item nullItem = null;
		Assert.assertThat(nullItem, Matchers.nullValue());
		Assert.assertThat(item, Matchers.notNullValue());
		
		Assert.assertThat(item, Matchers.sameInstance(item));

	}

hasToString()はtoString()を検証。
型はtypeCompatibleWith()やinstanceOf()を使う。
nullはnullValue()で。
sameInstance()はJUnitのassertSame()だね。

ロジック
	@Test
	public void testAssertThatConcerningLogic() {
		
		// Matchers.anything()は常にtrueになる
		Assert.assertThat("test", Matchers.anything());
	
		// すべてのMatcherがtrueになる必要がある
		Assert.assertThat("test", Matchers.allOf(Matchers.equalTo("test"), Matchers.containsString("es")));
		
		// どれか1つのMatcherがtrueになればよい
		Assert.assertThat("test", Matchers.anyOf(Matchers.equalTo("xxxx"), Matchers.containsString("es")));
		
	}

anything()はとにかくいつもtrue。assertTrue(true)だね。
複数のMatcherを適用するときにはallOf()やanyOf()を使う。allOf()が「&&」、anyOf()が「||」ってこと。


使うパターンが思いつかなかったけど、Matchersにある各マッチングメソッドの引数にさらにMatcherを渡すこともできるから、複雑な条件も指定できるかも。