結合 Hamcrest


假 設你有一個Guest類別, generate()方法用來產生訪客名單,以List<String>傳回,無論訪客名單內容為何,當中一定要有"Justin"、 "Momor"與"Hamimi"三位訪客,為此,你可能撰寫以下的測試:
package test.cc.openhome;
...
import cc.openhome.Guest;
public class GuestTest {
    @Test
    public void testGenerate() {
        Guest guest = new Guest();
        List<String> guests = guest.generate();
        assertTrue(guests.contains("Justin") &&
                   guests.contains("Momor") &&
                   guests.contains("Hamimi"));
    }
}

在斷言測試的部份,使用assertTrue()來判斷&&的結果最後是否為true,如果要確定的訪客數很多,這樣的斷言方式,將會產 生較多的程式碼而降低可讀性。

JUnit 4在4.4版之後引進入 Hamcrest 的支援,其目的在於改進斷言測試時的可讀性,直接來看看結合Hamcrest後的測試程式如何撰寫:
package test.cc.openhome;
import static org.junit.Assert.*;
import static org.junit.matchers.JUnitMatchers.hasItems;
import org.junit.Test;
import cc.openhome.Guest;
public class GuestTest {
    @Test
    public void testGenerate() {
        Guest guest = new Guest();
       
List<String> guests = guest.generate();
        assertThat(guests, hasItems("Justin", "Momor", "Hamimi"));
    }
}

可以看到,結合Hamcrest後,閱讀assertThat()的斷言,比較接近閱讀自然語言, guests, hasItems("Justin", "Momor", "Hamimi")讀來就像是:「訪客名單中有"Justin", "Momor", "Hamimi"。」

再假設有個例子,必須產生
不大於指定數字的整數陣列,你本來可能如下撰寫 斷言:
...
List<Integer> numbers = some.generate(5);
assertTrue(numbers.get(0) < 5 &&
           numbers.get(1) < 5 &&
           numbers.get(2) < 5 &&
           numbers.get(3) < 5 &&
           numbers.get(4) < 5);
...

如果結合Hamcrest,則可以改進如下:
...
List<Integer> numbers = some.generate(5);
assertThat(numbers , everyItem(lessThan(5)));
...

在這邊,你使用的是org.junit.matchers.JUnitMatcherseveryItem()org.hamcrest.MatcherslessThan()。JUnitMatchers提供了基本的幾個靜態方法:
  • both
  • containsString
  • either
  • everyItem
  • hasItem
  • hasItems

Hamcrest則提供更多的靜態方法,區分為核心(Core)、邏輯(Logic)、物件(Object)、Beans、群 集(Collections)、數字(Number)與文字(Text)等幾個大類,你可以在 The Hamcrest Tutorial 找到相關說明。

assertThat ()方法的簽署之一為:
assertThat(T, org.hamcrest.Matcher<T>)

T是待測結果, assertThat()會將T傳入Matchermatches()方法,matches()方法必須傳回true或false 的結果,表示斷言成功或失敗。

舉個例子來說,如果你想自定一個FirstOddItems, 提供一個靜態方法firstOddItems(),表示要斷言的List必須符合指定的前奇數個元素,也就是像這樣的用法:
package test.cc.openhome;

import static cc.openhome.FirstOddItems.firstOddItems;
import static org.junit.Assert.*;
...

public class GuestTest {
    @Test
    public void testGenerate() {
        Guest guest = new Guest();
        List<String> guests = guest.generate();
        assertThat(guests, firstOddItems("Justin", "Momor", "Bush"));
    }
}


則你可以如下定義Matcher:

package cc.openhome;

import java.util.Arrays;
import java.util.List;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;

public class FirstOddItems<I> extends BaseMatcher<I> {
private I items;

public FirstOddItems(I items) {
this.items = items;
}

public boolean matches(Object obj) {
List result = (List) obj;
int i = 0;
for(Object item : (List) items) {
if(!item.equals(result.get(i))) {
return false;
}
i += 2;
}
return true;
}

public void describeTo(Description desc) {
desc.appendText("前奇數個不符");
}

@Factory
public static <T> Matcher<List<T>> firstOddItems(T... items) {
return new FirstOddItems<List<T>>(Arrays.asList(items));
}
}

BaseMatcher實作了Matcher,你可以繼承它來自定義 Matcher,describeTo()會在matcher()結 果為false時呼叫,傳入的Description可以用來提供錯誤訊息。@Factory主要的目的是給工具使用,就目前這個例子而言,可標可不標。

在更複雜的例子中,例如:
assertThat(numbers, everyItem(lessThan(5)));

這表示org.junit.matchers.JUnitMatchers的everyItem()方法接受org.hamcrest.Matchers 的lessThan()傳回值,也就是一個Matcher<T>,也就是everyItem()的方法簽署中參數部份為:
everyItem(Matcher<T>)

在everyItem()中會建立另一個Matcher<T>,這個Matcher<T>的matches()方法會結合 lessThan()傳入的Matcher<T>的matches()來判斷要傳回true或false。