一、讓我們開啓單元測試之旅

前言

思量良久在考慮要不要寫這一篇,是不是直接看門見山將在項目中怎麼進行單元測試。最後想想覺得是不是太猴急了寫那樣,就好比你想和一個姑娘滾牀單,是不是先應該請姑娘吃頓飯、送點禮物什麼的。所以我才決定寫這篇序言,讓我們一起慢慢的揭開單元測試的面紗。

什麼是單元測試

這個在網上有很詳細的解釋。我這就簡單的給出一個概念:
單元測試是開發者編寫一小段代碼,用於檢測被測代碼的一個很小的、很明確的功能是否正確。

單元測試是對軟件基本單元進行的測試,實際應用中是對public 函數進行的測試。
執行單元測試,是爲了驗證某段代碼的行爲確實和開發者所期望的一致。

爲什麼要進行單元測試

理由千千萬,我只寵這三點:

  • 減少調試時間
  • 自動化測試
  • 令設計變得更好

我們或多或少也都聽說過單元測試,只知道用來檢測寫的代碼有沒有問題,這導致之前都沒有寫過測試用例,測試一些重要的方法最多也一個 main方法正常的數據調通了就過了,這樣導致後期出現各種各樣的問題,一遍遍的改代碼,一遍遍的改bug。費時費力還不一定能處理好。我以爲這是軟件開發的詬病,其實不然,是因爲我們不能確認我們寫的那部分代碼沒有問題,所以總花費很長時間找問題上。所以才需要進行單元測試,雖然在剛開始寫單元測試會花費時間,但是我們單元測試全都通過之後,我們對自己寫的代碼更有自信,可以確定沒有代碼沒有問題了,而不是自己認爲沒有問題的那種。這樣後期修復bug,也可以通過單元測試哪些執行成功哪些執行失敗可以快速的定位到問題。我覺得這一點就足以讓我們爲我們寫的代碼編寫相應的單元測試啦,畢竟找問題真是太痛苦,大家應該也深有體會。

單元測試怎麼做

簡單而言,就是對一個 public 方法編寫測試用例,那測試用例又怎麼寫呢?
測試用例說白了也是一個方法,用來驗證目標方法是否符合我們的預期。
那這樣就知道怎麼寫了吧,就是和我們平時寫方法一樣,但是它有一個標準
俗稱 “3A 模式” Arrange-Act-Assert(準備上下文環境–執行被測函數–斷言)。也就是說一個測試用例的方法包含三部分就可以了。

測試用例應該具備的特徵

上面說的測試用例包含這三部分就可以了,那我們的測試用例應該具備怎樣的特徵呢,短小精悍且快準繁

小:一個測試幾行代碼(15)
精準:一個測試之測一個場景
隔離:每個測試都可以獨立、重複運行,無耦合
快:每個測試都應該是毫秒級別的
頻繁:應該頻繁的執行,沒增加、修改、刪除一個測試都要運行一遍

那什麼樣的是好的單元測試呢?
自動化
可重複的
徹底的
獨立的
專業的

好的測試用例

測試用例應該短小精悍且快準狠。這些是對測試用例的函數本身而言的,但在實際項目中出問題往往就是某些情況沒有考慮到導致程序出錯的,我們在自測的時候往往會測試正常數據的情況然而卻忽略的了錯誤情況和邊界值的測試,這些纔是校驗一個項目的健壯性的標準。所以好的測試用例必定是有全面的測試數據。那怎樣獲取全面的測試數據呢?
在這之前需要知道哪些是好的測試數據

  • 最優可能抓住錯誤的
  • 不是重複的,多餘的
  • 一組相似測試用例中最有效的
  • 既不是太簡單,也不是太複雜

那怎樣獲取好的測試數據呢?有等價類劃分法、邊界值法、路徑分析法。

等價類劃分法

等價類劃分法是把所有可能的輸入數據,劃分成若干個子集,然後從每個子集中選取少數的具有代表性的數據作爲測試用例。
該方法是一種重要的、常用的黑盒測試用例設計方法。

有效等價類:對程序的規範說明是合理的,有意義的輸入數據構成的集合。
無效等價類:對程序的規範說明不是合理的或者無意義的輸入數據構成的集合。

我們來看一個例子:計算兩個點距離的函數

public double getDistance(double x1, double y1, double x2, double y2)

在這裏插入圖片描述

邊界值法

邊界值分析法是對輸入或者輸出的邊界值進行測試的一組黑盒測試方法。

通常情況下,邊界值分析法是作爲等價類劃分法的補充,這種情況下,其測試用例來自等價類的邊界。

比如上面一個例子中取邊界值做爲測試用例。

路徑分析法

基本路徑測試是一種白盒測試方法,它在程序控制圖的基礎上,通過分析程序的流程,構造導出基本可執行路徑集合,從而設計測試用例的方法。

設計出的測試用例要保證在測試程序中的每一個可執行語句至少執行一次。
我們來看一個例子
在這裏插入圖片描述
可能的路徑爲:

1-2-3-4-5
1-2-3-4-6
1-2-4-5
1-2-4-6

斷言

我們這裏說的斷言只是Junit斷言,java 本身也有斷言的,但是貌似我們使用的很少以至於我們都忘記了它的存在。
Junit 斷言說是斷言,其實也就是一份方法,沒有什麼語法。我們測試用例中使用斷言,也就是使用這些方法來進行驗證是否達到我們的預期。
方法有很多,大家可以看看源碼,我這裏給出幾個常見的。

函數名 描述
assertEquals 判斷實際產生的值與期望值是否相等
assertNull 判斷對象是否爲null
assertNotNull 判斷對象是否爲非null
assertSame 判斷實際產生的對象與期望對象是否爲同一個對象
assertNotSame 判斷實際產生的對象與期望對象是否爲不同的對象
assertTrue 判斷bool變量是否爲真
assertFalse 判斷bool變量是否爲假
Fail 使測試立即失敗

上面這樣說好像沒有什麼效果,我們先來看其中一個斷言方法的源代碼。我們就看第一個assertEquals 吧
在這裏插入圖片描述
可以看到有很多assertEquals方法。這樣的方法的重載在底層很常見。我們來看下三個參數類似是Object的這個吧。

public static void assertEquals(String message, Object expected, Object actual) {
        if (!equalsRegardingNull(expected, actual)) {
            if (expected instanceof String && actual instanceof String) {
                String cleanMessage = message == null ? "" : message;
                throw new ComparisonFailure(cleanMessage, (String)expected, (String)actual);
            } else {
                failNotEquals(message, expected, actual);
            }
        }
    }

private static boolean equalsRegardingNull(Object expected, Object actual) {
        if (expected == null) {
            return actual == null;
        } else {
            return isEquals(expected, actual);
        }
    }

    private static boolean isEquals(Object expected, Object actual) {
        return expected.equals(actual);
    }
private static void failNotEquals(String message, Object expected, Object actual) {
        fail(format(message, expected, actual));
    }

    static String format(String message, Object expected, Object actual) {
        String formatted = "";
        if (message != null && !message.equals("")) {
            formatted = message + " ";
        }

        String expectedString = String.valueOf(expected);
        String actualString = String.valueOf(actual);
        return expectedString.equals(actualString) ? formatted + "expected: " + formatClassAndValue(expected, expectedString) + " but was: " + formatClassAndValue(actual, actualString) : formatted + "expected:<" + expectedString + "> but was:<" + actualString + ">";
    }

equalsRegardingNull() 函數就是判斷兩個值是否相等,底層還是相當於用的object.equals()。如果兩個值相等就斷言通過,如果不相等就判斷expected和actual是否是string類型,如果是直接將message輸出。如果不是就failNotEquals().failNotEquals方法的源碼我也貼出來了,可以看也很簡單,就是message、expected、actual轉換成string格式輸出出來,並執行fail()使得測試失敗。

從上面看斷言也就不過如此(Junit 斷言)。我們會使用常用的方法就可以寫好測試用例啦,至於其他的方法,我們用到的時候可以直接其源代碼,畢竟也不會很複雜。

簡單案例

目標代碼及功能說明

在這裏插入圖片描述
這段代碼在項目中的作用是對特殊字段的對應的值進行處理並返回。
如果字段是包含time,那將值改成日期格式返回。
如果字段是包含iphone,那將值截取後11位返回。
其他情況,直接返回。

public class DataHandle {
    public static final String REGEX_MOBILE = "^((13[0-9])|(15[0-9])|(17[0-9])|(18[0-9])|(19[0-9])|(14[0-9]))\\d{8}$";

    public String fieldDataHandle(String key,String value){
        //如果是時間類型,將時間戳轉成時間
         if(key.toLowerCase().contains("ipone")){  //如果手機號長於11位,截取後11位
            if(value.length()>11){
                value=value.substring(value.length()-11);
            }
            if(!isMobile(value)){
                return null;
            }
        }else if(key.toLowerCase().contains("time")){
            value=timeStampToDate(value,"yyyy-MM-dd HH:mm:ss");
        }
        return value;
    }
    private static String timeStampToDate(String time,String timeFormat) {
        Long timeLong = Long.parseLong(time);
        SimpleDateFormat sdf = new SimpleDateFormat(timeFormat);//要轉換的時間格式
        Date date;
        try {
            date = sdf.parse(sdf.format(timeLong));
            return sdf.format(date);
        } catch (Exception e) {
            return null;
        }
    }

    private static boolean isMobile(String mobile) {
        return Pattern.matches(REGEX_MOBILE, mobile);
    }

}

單元測試設計

等價類設計

等價類劃分 有效等價類 無效等價類
key 包含time, 包含ipone,包含time和ipone 不包含time 和ipone
value 時間戳,手機號,帶區號的手機號 不是時間戳,也不是手機號

我們根據這個來設計測試用例

key value 預期值
字段包含time 時間戳 返回日期格式的的字符串
字段包含time 不是時間戳 null
字段包含ipone 不是手機號 null
字段包含ipone 是11位的手機號 返回11位手機號字符串
字段包含ipone 是手機號,但位數大於11位 返回11位手機號字符串
字段包含time,ipone 時間戳 返回日期格式的的字符串
字段包含time,ipone 不是手機號,也不是時間戳 null
字段包含time,ipone 手機號 null
字段不包含time 和ipone 時間戳 時間戳字符串
字段不包含time 和ipone 11位手機號 手機號字符串
字段不包含time 和ipone 大於11位手機號 返回值字符串
字段不包含time 和ipone 不是手機號,也不是時間戳 值對應字符串

編寫測試用例

public class DataHandleTest {

    DataHandle dataHandle = null;

    @Before
    public void setup()
    {
        dataHandle = new DataHandle();
    }

    @After
    public void tearDown()
    {
        dataHandle = null;
    }


    @Test
    public void testFieldDataHandle_包含time是時間戳_返回日期字符串(){
        assertEquals("2019-09-10 19:02:30", dataHandle.fieldDataHandle("atime","1568113350000"));
    }

    @Test
    public void testFieldDataHandle_包含time不是時間戳_返回NULL(){
        assertNull(dataHandle.fieldDataHandle("atime","1568113350aaa"));
    }

    @Test
    public void testFieldDataHandle_包含ipone不是手機號_返回NULL(){
        assertNull(dataHandle.fieldDataHandle("bipone","aaa"));
    }

    @Test
    public void testFieldDataHandle_包含ipone是11位手機號_返回手機號字符串(){
        assertEquals("13265459362",dataHandle.fieldDataHandle("bipone","13265459362"));
    }

    @Test
    public void testFieldDataHandle_包含ipone是大於11位手機號_返回手機號字符串(){
        assertEquals("13265459362",dataHandle.fieldDataHandle("bipone","+8613265459362"));
    }

    @Test
    public void testFieldDataHandle_包含time和ipone是時間戳_返回NULL(){
        assertNull(dataHandle.fieldDataHandle("atimebipone","1568168656000"));
    }

    @Test
    public void testFieldDataHandle_包含time和ipone是手機號_返回手機號字符串(){
        assertEquals("13265459362",dataHandle.fieldDataHandle("atimebipone","13265459362"));
    }

    @Test
    public void testFieldDataHandle_包含time和ipone不是時間戳手機號_返回NULL(){
        assertNull(dataHandle.fieldDataHandle("atimebipone","aaabbb"));
    }

    @Test
    public void testFieldDataHandle_不包含time和ipone是時間戳_返回時間戳字符串(){
        assertEquals("1568114439",dataHandle.fieldDataHandle("ccc","1568114439"));
    }

    @Test
    public void testFieldDataHandle_不包含time和ipone是11位手機號_返回時間手機號字符串(){
        assertEquals("13112341234",dataHandle.fieldDataHandle("ccc","13112341234"));
    }

    @Test
    public void testFieldDataHandle_不包含time和ipone是大於11位手機號_返回值字符串(){
        assertEquals("+8613412341234",dataHandle.fieldDataHandle("ccc","+8613412341234"));
    }

    @Test
    public void testFieldDataHandle_不包含time和ipone不是時間戳手機號_返回值字符串(){
        assertEquals("abcdefg",dataHandle.fieldDataHandle("ccc","abcdefg"));
    }


} 

然後我們執行一下測試用例;
在這裏插入圖片描述
可以看到有一個地方的測試用例是不通過的,那就說明有問題,我們看一下。

 @Test
    public void testFieldDataHandle_包含time不是時間戳_返回NULL(){
        assertNull(dataHandle.fieldDataHandle("atime","1568113350aaa"));
    }

這個是拋異常了,因爲日期格式轉換錯誤,但是我們在日期轉換的時候已經捕獲了呀,並且返回爲null 。
在這裏插入圖片描述
那爲什麼測試用例沒有通過呢,而是直接拋異常出來了,調試發現這個方法沒有捕獲到異常,而是直接拋出給Junit了。所以這裏提示代碼不能這麼寫。一般異常了不建議返回null.而是打印出異常把信息拋出。這裏我們就不改了。我們將測試用例改一下,在測試用例中捕獲一下異常。
改成如下:

 @Test(expected = NumberFormatException.class)
    public void testFieldDataHandle_包含time不是時間戳_throwsException(){
        dataHandle.fieldDataHandle("atime","1568113350aaa");
    }

再全部執行一下
在這裏插入圖片描述
這樣就不抱錯了。
好啦這個就是一個簡單的測試用例啦。

總結

最後總結一下吧,我覺得應該知道以下幾點

  • 認識到單元測試的必要性
  • 好的測試用例是關鍵
  • 測試用例中斷言必不可少
  • 編寫測試用例的規範要遵循

看到這啦的小夥伴,如果覺得喜歡就點個贊吧嘿嘿。如果有什麼意見,歡迎給我提。嘿嘿。後續想寫一下測試用例的規範,喜歡的可以持續關注❤

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章