假 設你有一個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"));
}
}
...
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"));
}
}
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);
...
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)));
...
List<Integer> numbers = some.generate(5);
assertThat(numbers , everyItem(lessThan(5)));
...
在這邊,你使用的是org.junit.matchers.JUnitMatchers的everyItem()與org.hamcrest.Matchers的lessThan()。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傳入Matcher的matches()方法,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"));
}
}
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。