阿里開源新一代單元測試 Mock 工具!

TestableMock是基於源碼和字節碼加強的Java單元測試輔助工具,包含如下功能:
  • 訪問被測類私有成員:使單元測試能直接調用和訪問被測類的私有成員,解決私有成員初始化和私有方法測試的問題java

  • 快速Mock任意調用:使被測類的任意方法調用快速替換爲Mock方法,實現"指哪換哪",解決傳統Mock工具使用繁瑣的問題git

  • 輔助測試void方法:利用Mock校驗器對方法的內部邏輯進行檢查,解決無返回值方法難以實施單元測試的問題web

訪問私有成員字段和方法

現在關於私有方法是否應該作單元測試的爭論正逐漸消停,開發者的廣泛實踐已經給出事實答案。經過公有方法間接測私有方法在不少狀況下難以進行,開發者們更願意經過修改方法可見性的辦法來讓本來私有的方法在測試用例中變得可測。微信

此外,在單元測試中時常會須要對被測對象進行特定的成員字段初始化,但有時因爲被測類的構造方法限制,使得沒法便捷的對這些字段進行賦值。那麼,可否在不破壞被測類型封裝的狀況下,容許單元測試用例內的代碼直接訪問被測類的私有方法和成員字段呢?TestableMock提供了兩種簡單的解決方案。app

方法一:使用`@EnablePrivateAccess`註解

只需爲測試類添加@EnablePrivateAccess註解,便可在測試用例中得到如下加強能力:框架

  • 調用被測類的私有方法(包括靜態方法)dom

  • 讀取被測類的私有字段(包括靜態字段)編輯器

  • 修改被測類的私有字段(包括靜態字段)工具

  • 修改被測類的常量字段(使用final修飾的字段,包括靜態字段)單元測試

訪問和修改私有、常量成員時,IDE可能會提示語法有誤,但編譯器將可以正常運行測試。(使用編譯期代碼加強,目前僅實現了Java語言的適配)

效果見java-demo示例項目DemoPrivateAccessTest測試類中的用例。

方法二:使用`PrivateAccessor`工具類

若不但願看到IDE的語法錯誤提醒,或是在非Java語言的JVM工程(譬如Kotlin語言)裏,也能夠藉助PrivateAccessor工具類來直接訪問私有成員。

這個類提供了6個靜態方法:

  • PrivateAccessor.get(被測對象, "私有字段名") ➜ 讀取被測類的私有字段

  • PrivateAccessor.set(被測對象, "私有字段名", 新的值) ➜ 修改被測類的私有字段(或常量字段)

  • PrivateAccessor.invoke(被測對象, "私有方法名", 調用參數..) ➜ 調用被測類的私有方法

  • PrivateAccessor.getStatic(被測類型, "私有靜態字段名") ➜ 讀取被測類的靜態私有字段

  • PrivateAccessor.setStatic(被測類型, "私有靜態字段名", 新的值) ➜ 修改被測類的靜態私有字段(或靜態常量字段)

  • PrivateAccessor.invokeStatic(被測類型, "私有靜態方法名", 調用參數..) ➜ 調用被測類的靜態私有方法

快速Mock被測類的任意方法調用

相比以往Mock工具以類爲粒度的Mock方式,TestableMock容許用戶直接定義須要Mock的單個方法,並遵循約定優於配置的原則,按照規則自動在測試運行時替換被測方法中的指定方法調用。

概括起來就兩條:

  • Mock非構造方法,拷貝原方法定義到測試類,增長一個與調用者類型相同的參數,加@MockMethod註解

  • Mock構造方法,拷貝原方法定義到測試類,返回值換成構造的類型,方法名隨意,加@MockContructor註解

具體的Mock方法定義約定以下:

1. 覆寫任意類的方法調用

在測試類裏定義一個有@MockMethod註解的普通方法,使它與需覆寫的方法名稱、參數、返回值類型徹底一致,而後在其參數列表首位再增長一個類型爲該方法本來所屬對象類型的參數。

此時被測類中全部對該需覆寫方法的調用,將在單元測試運行時,將自動被替換爲對上述自定義Mock方法的調用。

注意:當遇到待覆寫方法有重名時,能夠將需覆寫的方法名寫到@MockMethod註解的targetMethod參數裏,這樣Mock方法自身就能夠隨意命名了。

例如,被測類中有一處"anything".substring(1, 2)調用,咱們但願在運行測試的時候將它換成一個固定字符串,則只需在測試類定義以下方法:

// 原方法簽名爲`String substring(int, int)`
// 調用此方法的對象`"anything"`類型爲`String`
// 則Mock方法簽名在其參數列表首位增長一個類型爲`String`的參數(名字隨意)
// 此參數可用於得到當時的實際調用者的值和上下文
@MockMethod
private String substring(String self, int i, int j) {
    return "sub_string";
}

下面這個例子展現了targetMethod參數的用法,其效果與上述示例相同:

// 使用`targetMethod`指定需Mock的方法名
// 此方法自己如今能夠隨意命名,但方法參數依然須要遵循相同的匹配規則
@MockMethod(targetMethod = "substring")
private String use_any_mock_method_name(String self, int i, int j) {
    return "sub_string";
}

完整代碼示例見java-demokotlin-demo示例項目中的should_able_to_mock_common_method()測試用例。(因爲Kotlin對String類型進行了魔改,故Kotlin示例中將被測方法在BlackBox類里加了一層封裝)

2. 覆寫被測類自身的成員方法

有時候,在對某些方法進行測試時,但願將被測類自身的另一些成員方法Mock掉。

操做方法與前一種狀況相同,Mock方法的第一個參數類型需與被測類相同,便可實現對被測類自身(不管是公有或私有)成員方法的覆寫。

例如,被測類中有一個簽名爲String innerFunc(String)的私有方法,咱們但願在測試的時候將它替換掉,則只需在測試類定義以下方法:

// 被測類型是`DemoMock`
// 所以在定義Mock方法時,在目標方法參數首位加一個類型爲`DemoMock`的參數(名字隨意)
@MockMethod
private String innerFunc(DemoMock self, String text) {
    return "mock_" + text;
}

3. 覆寫任意類的靜態方法

對於靜態方法的Mock與普通方法相同。但須要注意的是,靜態方法的Mock方法被調用時,傳入的第一個參數實際值始終是null

例如,在被測類中調用了BlackBox類型中的靜態方法secretBox(),改方法簽名爲BlackBox secretBox(),則Mock方法以下:

// 目標靜態方法定義在`BlackBox`類型中
// 在定義Mock方法時,在目標方法參數首位加一個類型爲`BlackBox`的參數(名字隨意)
// 此參數僅用於標識目標類型,實際傳入值將始終爲`null`
@MockMethod
private BlackBox secretBox(BlackBox ignore) {
    return new BlackBox("not_secret_box");
}

完整代碼示例見java-demokotlin-demo示例項目中的should_able_to_mock_static_method()測試用例。

測試無返回值的方法

如何對void類型的方法進行測試一直是許多單元測試框架在悄悄迴避的話題,因爲以往的單元測試手段主要是對被測單元的返回結果進行校驗,當遇到方法沒有返回值時就會變得無從下手。

從功能的角度來講,雖然void方法不返回任何值,但它的執行必定會對外界產生某些潛在影響,咱們將其稱爲方法的"反作用",好比:

  1. 初始化某些外部變量(私有成員變量或者全局靜態變量)

  2. 在方法體內對外部對象實例進行賦值

  3. 輸出了日誌

  4. 調用了其餘外部方法

  5. … …

不返回任何值也不產生任何"反作用"的方法沒有存在的意義。

這些"反作用"的本質概括來講可分爲兩類:修改外部變量調用外部方法

經過TestableMock的私有字段訪問和Mock校驗器能夠很方便的實現對"反作用"的結果檢查。

1. 修改外部變量的void方法

例如,下面這個方法會根據輸入修改私有成員變量hashCache

class Demo {
    private Map<String, Integer> hashCache = mapOf();

    public void updateCache(String domain, String key) {
        String cacheKey = domain + "::" + key;
        Integer num = hashCache.get(cacheKey);
        hashCache.put(cacheKey, count == null ? initHash(key) : nextHash(num, key));
    }

    ... // 其餘方法省略
}

若要測試此方法,能夠利用TestableMock直接讀取私有成員變量的值,對結果進行校驗:

@EnablePrivateAccess  // 啓用TestableMock的私有成員訪問功能
class DemoTest {
    private Demo demo = new Demo();

    @Test
    public void testSaveToCache(
{
        Integer firstVal = demo.initHash("hello"); // 訪問私有方法
        Integer nextVal = demo.nextHash(firstVal, "hello"); // 訪問私有方法
        demo.saveToCache("demo""hello");
        assertEquals(firstVal, demo.hashCache.get("demo::hello")); // 讀取私有變量
        demo.saveToCache("demo""hello");
        assertEquals(nextVal, demo.hashCache.get("demo::hello")); // 讀取私有變量
    }
}

2. 調用外部方法的void方法

例如,下面這個方法會根據輸入打印信息到控制檯:

class Demo {
    public void recordAction(Action action{
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
        String timeStamp = df.format(new Date());
        System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
    }
}

若要測試此方法,能夠利用TestableMock快速Mock掉System.out.println方法。在Mock方法體裏能夠繼續執行原調用(至關於並不影響原本方法功能,僅用於作調用記錄),也能夠直接留空(至關於去除了原方法的反作用)。

在執行完被測的void類型方法之後,用InvokeVerifier.verify()校驗傳入的打印內容是否符合預期:

class DemoTest {
    private Demo demo = new Demo();

    // 攔截`System.out.println`調用
    @MockMethod
    public void println(PrintStream ps, String msg) {
        // 執行原調用
        ps.println(msg);
    }

    @Test
    public void testRecordAction() {
        Action action = new Action("click"":download");
        demo.recordAction();
        // 驗證Mock方法`println`被調用,且傳入參數符合預期
        verify("println").with(matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\[click\\] :download"));
    }
}

項目地址

開源地址:https://gitee.com/mirrors/TestableMock

關注肥朝公衆號回覆"mock",查看該mock工具介紹使用文檔



本文分享自微信公衆號 - 肥朝(feichao_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索