前言
在Testing: Assert類別的使用一文中,我們已經知道Assert類別提供許多簡單、易懂的斷言方法,協助開發者撰寫測試案例時的判斷依據。接著,這篇內容會提到assertThat的使用,讓撰寫測試時的斷言可讀性更高。
AssertThat與Hamcrest匹配器(Matcher)
assertThat語法是另一種斷言方式,需要和Hamcrest工具一起使用。而所謂的Hamcrest是一組API和匹配器的實作,在Android Studio就可以直接使用它,用來將物件與各種期望進行匹配,再結合assertThat語法,讓測試程式更好讀。這樣說明好像有點抽象,先前的斷言語法,我們大概了解怎麼使用,但有時候測試的判斷會更為複雜,我們這時就可以利用assertThat與Hamcrest匹配器。寫個程式,讓我們可以觀察使用assertEquals和assertThat之間的差異。假設有個名為AppendWordAndContent的類別,有個appendWordAndContent方法,程式碼如下。
public class AppendWordAndContent {
public String appendWordAndContent(String appendingWord, String content) {
StringBuilder sb = new StringBuilder();
sb.append(appendingWord).append(" ").append(content);
return sb.toString();
}
}
在AppendWordAndContent方法,提供一個將word和content兩個字串合併的方法,測試的方式如下。
@Test
public void testOutputHasLineNumbersCase1WithAssertTrue() throws Exception {
// Arrange
String content = "這是assertThat的測試案例。";
// Act
String output = this.appendWordAndContent.appendWordAndContent("1st", content);
// Assert
assertTrue(output.indexOf("1st") != -1);
}
我們使用了assertTrue(output.indexOf("1st") != -1); 其實在assert裡面使用判斷不是很好,加上也不夠直覺,可讀性滿差的,可以改用AssertThat(actual, matcher)這種斷言方式來聲明,程式碼如下。
@Test
public void testOutputHasLineNumbersCase1WithAssertThat() throws Exception {
// Arrange
String content = "這是assertThat的測試案例。";
int NO_RESULT = -1;
// Act
String output = this.appendWordAndContent.appendWordAndContent("1st", content);
// Assert
assertThat(output.indexOf("1st"), is(not(NO_RESULT)));
}
在改寫後的程式中,做了兩件事情。首先將-1這樣的魔術數字把它改成一個具有意義的的變數”NO_RESULT”,當結果等於NO_RESULT時視為找不到結果,再來,!=的地方改成了is(not(NO_RESULT)),最後就變成了assertThat(output.indexOf("1st"), is(not(NO_RESULT))),這樣就變得好讀許多。
Ham crest匹配器
這裡提供幾個好用的匹配器,有興趣可以自行運用看看。
@Test
public void testHasItems() throws Exception {
List<Integer> testIntArray = Arrays.asList(1, 2, 3, 5);
assertThat(testIntArray, hasItems(5, 3, 2));
}
- 字串匹配,程式如下。
- startsWith(java.lang.String prefix)
@Test
public void testStartsWith() throws Exception {
String word = "preview";
assertThat(word, startsWith("pre"));
}
- endsWith(java.lang.String suffix)
@Test
public void testEndsWith() throws Exception {
String fileName = "word.exe";
assertThat(fileName, endsWith("exe"));
}
- containsString(java.lang.String substring)
@Test
public void testContainsString() throws Exception {
String content = "1st" + " " + "Unit Test Sample!";
assertThat(content, containsString("1st"));
}
@Test
public void testIs() throws Exception {
String content = "1st" + " " + "Unit Test Sample!";
assertThat(content.charAt(2), is('t'));
}
- anyOf(java.lang.Class<T> type)
@Test
public void testAnyOf() throws Exception {
String content = "1st" + " " + "Unit Test Sample!";
assertThat(content, anyOf(startsWith("1st"), startsWith("1")));
}
- instanceOf(java.lang.Class<?> type)
@Test
public void testInstanceOf() throws Exception {
Exception e1 = new IllegalArgumentException();
assertThat(e1, instanceOf(IllegalArgumentException.class));
}
@Test
public void testNullValue() throws Exception {
Object object = null;
assertThat(object, nullValue());
}
另外,你也可以匯入Hamcres-All的函式庫,讓你的程式引用完整的匹配器,gradle設定如下。更多的使用方式,大家可以自行查詢
官方文件,以下僅舉一個範例。
testCompile 'org.hamcrest:hamcrest-all:1.3'
@Test
public void testHasSize() throws Exception {
List<Integer> testIntArray = Arrays.asList(1, 2, 3, 5);
assertThat(testIntArray, hasSize(4));
assertThat(testIntArray, contains(1, 2, 3, 5));
assertThat(testIntArray, containsInAnyOrder(2, 3, 5, 1));
}
客製化匹配器
如果上述提供給您的匹配器不敷使用,想要客製化也是可以的,我們可以來實作看看。實作一個匹配器需要繼承BaseMatcher這個抽象類別,並且實作matches和describeTo這兩個方法,實作方式如下。
首先我們先建立一個Role的資料物件,做為之後匹配器的測試,程式碼如下。
public class Role {
private String id;
public Role(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
再來建立一個名為RoleOf的匹配器,並且繼承BaseMatcher<Role>。matches方法的參數item是當你使用assertThat時傳進來的物件,我們就要依據這個物件實作你要的邏輯,這邊就判斷員工編號(id)是否以”a”開頭;在describeTo方法內,我們則是可以在測試失敗時,欲顯示的訊息,程式碼如下。
public class RoleOf extends BaseMatcher<Role> {
@Override
public boolean matches(Object item) {
if (!(item instanceof Role)) {
return false;
}
Role role = (Role) item;
if (role.getId() == null || role.getId().length() == 0) {
return false;
}
return role.getId().toLowerCase().startsWith("a") ? true : false;
}
@Override
public void describeTo(Description description) {
description.appendText("管理人員的編號應以a為首!");
}
}
執行測試,程式碼與結果如下。
@Test
public void testRoleOf() throws Exception {
Role role = new Role("b123456789");
assertThat(role, new RoleOf());
}
java.lang.AssertionError:
Expected: 管理人員的編號應以a為首!
but: was <com.xy.unittestsample.sample3.Role@593634ad>
Expected :管理人員的編號應以a為首!
Actual :<com.xy.unittestsample.sample3.Role@593634ad>
我們可以看到測試失敗了,原因在於測試內容並非為a開頭的字串,錯誤訊息也正確顯示出來,但這邊卻只顯示Actual :<com.xy.unittestsample.sample3.Role@593634ad>,這樣我們無法正確得知是什麼錯誤,所以我們可以用下面的方式改進,覆寫describeMismatch這個方法即可,程式如下。
public class RoleOf extends BaseMatcher<Role> {
@Override
public boolean matches(Object item) {
if (!(item instanceof Role)) {
return false;
}
Role role = (Role) item;
if (role.getId() == null || role.getId().length() == 0) {
return false;
}
return role.getId().toLowerCase().startsWith("a") ? true : false;
}
@Override
public void describeTo(Description description) {
description.appendText("管理人員的編號應以a為首!");
}
@Override
public void describeMismatch(Object item, Description description) {
super.describeMismatch(item, description);
if (item == null) {
description.appendText("物件是空的!");
}
Role role = (Role) item;
description.appendText("該角色為一般人員!").appendText("員工編號為: ").appendText(role.getId());
}
}
java.lang.AssertionError:
Expected: 管理人員的編號應以a為首!
but: was <com.xy.unittestsample.sample3.Role@593634ad>該角色為一般人員!員工編號為: b123456789
Expected :管理人員的編號應以a為首!
Actual :<com.xy.unittestsample.sample3.Role@593634ad>該角色為一般人員!員工編號為: b123456789
看到了?這邊將實際的員工編號顯示出來,這樣才方便我們去重構錯誤的程式!最後我們就使用一個以”a"開頭的員工編號,讓這個測試通過吧!程式如下。
@Test
public void testRoleOfIsCorrect() throws Exception {
Role role = new Role("a123456789");
assertThat(role, new RoleOf());
}
但看看上面的測試,你可能覺得這樣的寫法怎麼不像Hamcrest匹配器那樣,當然你可以用一個靜態工廠方法來達到同樣的目的,程式碼與測試如下。
public class RoleMatcher {
public static Matcher<Role> isAdmin() {
return new RoleOf();
}
}
@Test
public void testRoleOfIsCorrectWithFactoryMethod() throws Exception {
Role role = new Role("a123456789");
assertThat(role, isAdmin());
}
小結
以上我們知道該如何利用assertThat與Macher讓我們的測試程式的可讀性更高,相信多練習,我們一定可以將測試程式寫得更好,希望大家能多多練習囉!
Source Code
Github:
https://github.com/xavier0507/UnitTestSample.git