前言
在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匹配器
這裡提供幾個好用的匹配器,有興趣可以自行運用看看。- Iterable匹配,程式如下。
- hasItems(T... items)
    @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"));
    }- 條件匹配,程式如下。
- is(T value)
    @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));
    }- nullValue()
    @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());
    }
 
沒有留言:
張貼留言