阿里開(kāi)源新一代單元測(cè)試 Mock 工具!
TestableMock是基于源碼和字節(jié)碼增強(qiáng)的Java單元測(cè)試輔助工具,包含以下功能:訪問(wèn)被測(cè)類私有成員:使單元測(cè)試能直接調(diào)用和訪問(wèn)被測(cè)類的私有成員,解決私有成員初始化和私有方法測(cè)試的問(wèn)題
快速M(fèi)ock任意調(diào)用:使被測(cè)類的任意方法調(diào)用快速替換為Mock方法,實(shí)現(xiàn)"指哪換哪",解決傳統(tǒng)Mock工具使用繁瑣的問(wèn)題
輔助測(cè)試void方法:利用Mock校驗(yàn)器對(duì)方法的內(nèi)部邏輯進(jìn)行檢查,解決無(wú)返回值方法難以實(shí)施單元測(cè)試的問(wèn)題
訪問(wèn)私有成員字段和方法
如今關(guān)于私有方法是否應(yīng)該做單元測(cè)試的爭(zhēng)論正逐漸消停,開(kāi)發(fā)者的普遍實(shí)踐已經(jīng)給出事實(shí)答案。通過(guò)公有方法間接測(cè)私有方法在很多情況下難以進(jìn)行,開(kāi)發(fā)者們更愿意通過(guò)修改方法可見(jiàn)性的辦法來(lái)讓原本私有的方法在測(cè)試用例中變得可測(cè)。
此外,在單元測(cè)試中時(shí)常會(huì)需要對(duì)被測(cè)對(duì)象進(jìn)行特定的成員字段初始化,但有時(shí)由于被測(cè)類的構(gòu)造方法限制,使得無(wú)法便捷的對(duì)這些字段進(jìn)行賦值。那么,能否在不破壞被測(cè)類型封裝的情況下,允許單元測(cè)試用例內(nèi)的代碼直接訪問(wèn)被測(cè)類的私有方法和成員字段呢?TestableMock提供了兩種簡(jiǎn)單的解決方案。
方法一:使用`@EnablePrivateAccess`注解
只需為測(cè)試類添加@EnablePrivateAccess注解,即可在測(cè)試用例中獲得以下增強(qiáng)能力:
調(diào)用被測(cè)類的私有方法(包括靜態(tài)方法)
讀取被測(cè)類的私有字段(包括靜態(tài)字段)
修改被測(cè)類的私有字段(包括靜態(tài)字段)
修改被測(cè)類的常量字段(使用final修飾的字段,包括靜態(tài)字段)
訪問(wèn)和修改私有、常量成員時(shí),IDE可能會(huì)提示語(yǔ)法有誤,但編譯器將能夠正常運(yùn)行測(cè)試。(使用編譯期代碼增強(qiáng),目前僅實(shí)現(xiàn)了Java語(yǔ)言的適配)
效果見(jiàn)java-demo示例項(xiàng)目DemoPrivateAccessTest測(cè)試類中的用例。
方法二:使用`PrivateAccessor`工具類
若不希望看到IDE的語(yǔ)法錯(cuò)誤提醒,或是在非Java語(yǔ)言的JVM工程(譬如Kotlin語(yǔ)言)里,也可以借助PrivateAccessor工具類來(lái)直接訪問(wèn)私有成員。
這個(gè)類提供了6個(gè)靜態(tài)方法:
PrivateAccessor.get(被測(cè)對(duì)象, "私有字段名")? 讀取被測(cè)類的私有字段PrivateAccessor.set(被測(cè)對(duì)象, "私有字段名", 新的值)? 修改被測(cè)類的私有字段(或常量字段)PrivateAccessor.invoke(被測(cè)對(duì)象, "私有方法名", 調(diào)用參數(shù)..)? 調(diào)用被測(cè)類的私有方法PrivateAccessor.getStatic(被測(cè)類型, "私有靜態(tài)字段名")? 讀取被測(cè)類的靜態(tài)私有字段PrivateAccessor.setStatic(被測(cè)類型, "私有靜態(tài)字段名", 新的值)? 修改被測(cè)類的靜態(tài)私有字段(或靜態(tài)常量字段)PrivateAccessor.invokeStatic(被測(cè)類型, "私有靜態(tài)方法名", 調(diào)用參數(shù)..)? 調(diào)用被測(cè)類的靜態(tài)私有方法
快速M(fèi)ock被測(cè)類的任意方法調(diào)用
相比以往Mock工具以類為粒度的Mock方式,TestableMock允許用戶直接定義需要Mock的單個(gè)方法,并遵循約定優(yōu)于配置的原則,按照規(guī)則自動(dòng)在測(cè)試運(yùn)行時(shí)替換被測(cè)方法中的指定方法調(diào)用。
歸納起來(lái)就兩條:
Mock非構(gòu)造方法,拷貝原方法定義到測(cè)試類,增加一個(gè)與調(diào)用者類型相同的參數(shù),加
@MockMethod注解Mock構(gòu)造方法,拷貝原方法定義到測(cè)試類,返回值換成構(gòu)造的類型,方法名隨意,加
@MockContructor注解
具體的Mock方法定義約定如下:
1. 覆寫(xiě)任意類的方法調(diào)用
在測(cè)試類里定義一個(gè)有@MockMethod注解的普通方法,使它與需覆寫(xiě)的方法名稱、參數(shù)、返回值類型完全一致,然后在其參數(shù)列表首位再增加一個(gè)類型為該方法原本所屬對(duì)象類型的參數(shù)。
此時(shí)被測(cè)類中所有對(duì)該需覆寫(xiě)方法的調(diào)用,將在單元測(cè)試運(yùn)行時(shí),將自動(dòng)被替換為對(duì)上述自定義Mock方法的調(diào)用。
注意:當(dāng)遇到待覆寫(xiě)方法有重名時(shí),可以將需覆寫(xiě)的方法名寫(xiě)到@MockMethod注解的targetMethod參數(shù)里,這樣Mock方法自身就可以隨意命名了。
例如,被測(cè)類中有一處"anything".substring(1, 2)調(diào)用,我們希望在運(yùn)行測(cè)試的時(shí)候?qū)⑺鼡Q成一個(gè)固定字符串,則只需在測(cè)試類定義如下方法:
//?原方法簽名為`String?substring(int,?int)`
//?調(diào)用此方法的對(duì)象`"anything"`類型為`String`
//?則Mock方法簽名在其參數(shù)列表首位增加一個(gè)類型為`String`的參數(shù)(名字隨意)
//?此參數(shù)可用于獲得當(dāng)時(shí)的實(shí)際調(diào)用者的值和上下文
@MockMethod
private?String?substring(String?self,?int?i,?int?j)?{
????return?"sub_string";
}
下面這個(gè)例子展示了targetMethod參數(shù)的用法,其效果與上述示例相同:
//?使用`targetMethod`指定需Mock的方法名
//?此方法本身現(xiàn)在可以隨意命名,但方法參數(shù)依然需要遵循相同的匹配規(guī)則
@MockMethod(targetMethod?=?"substring")
private?String?use_any_mock_method_name(String?self,?int?i,?int?j)?{
????return?"sub_string";
}
完整代碼示例見(jiàn)java-demo和kotlin-demo示例項(xiàng)目中的should_able_to_mock_common_method()測(cè)試用例。(由于Kotlin對(duì)String類型進(jìn)行了魔改,故Kotlin示例中將被測(cè)方法在BlackBox類里加了一層封裝)
2. 覆寫(xiě)被測(cè)類自身的成員方法
有時(shí)候,在對(duì)某些方法進(jìn)行測(cè)試時(shí),希望將被測(cè)類自身的另外一些成員方法Mock掉。
操作方法與前一種情況相同,Mock方法的第一個(gè)參數(shù)類型需與被測(cè)類相同,即可實(shí)現(xiàn)對(duì)被測(cè)類自身(不論是公有或私有)成員方法的覆寫(xiě)。
例如,被測(cè)類中有一個(gè)簽名為String innerFunc(String)的私有方法,我們希望在測(cè)試的時(shí)候?qū)⑺鎿Q掉,則只需在測(cè)試類定義如下方法:
//?被測(cè)類型是`DemoMock`
//?因此在定義Mock方法時(shí),在目標(biāo)方法參數(shù)首位加一個(gè)類型為`DemoMock`的參數(shù)(名字隨意)
@MockMethod
private?String?innerFunc(DemoMock?self,?String?text)?{
????return?"mock_"?+?text;
}3. 覆寫(xiě)任意類的靜態(tài)方法
對(duì)于靜態(tài)方法的Mock與普通方法相同。但需要注意的是,靜態(tài)方法的Mock方法被調(diào)用時(shí),傳入的第一個(gè)參數(shù)實(shí)際值始終是null。
例如,在被測(cè)類中調(diào)用了BlackBox類型中的靜態(tài)方法secretBox(),改方法簽名為BlackBox secretBox(),則Mock方法如下:
//?目標(biāo)靜態(tài)方法定義在`BlackBox`類型中
//?在定義Mock方法時(shí),在目標(biāo)方法參數(shù)首位加一個(gè)類型為`BlackBox`的參數(shù)(名字隨意)
//?此參數(shù)僅用于標(biāo)識(shí)目標(biāo)類型,實(shí)際傳入值將始終為`null`
@MockMethod
private?BlackBox?secretBox(BlackBox?ignore)?{
????return?new?BlackBox("not_secret_box");
}
完整代碼示例見(jiàn)java-demo和kotlin-demo示例項(xiàng)目中的should_able_to_mock_static_method()測(cè)試用例。
測(cè)試無(wú)返回值的方法
如何對(duì)void類型的方法進(jìn)行測(cè)試一直是許多單元測(cè)試框架在悄悄回避的話題,由于以往的單元測(cè)試手段主要是對(duì)被測(cè)單元的返回結(jié)果進(jìn)行校驗(yàn),當(dāng)遇到方法沒(méi)有返回值時(shí)就會(huì)變得無(wú)從下手。
從功能的角度來(lái)說(shuō),雖然void方法不返回任何值,但它的執(zhí)行一定會(huì)對(duì)外界產(chǎn)生某些潛在影響,我們將其稱為方法的"副作用",比如:
初始化某些外部變量(私有成員變量或者全局靜態(tài)變量)
在方法體內(nèi)對(duì)外部對(duì)象實(shí)例進(jìn)行賦值
輸出了日志
調(diào)用了其他外部方法
… …
不返回任何值也不產(chǎn)生任何"副作用"的方法沒(méi)有存在的意義。
這些"副作用"的本質(zhì)歸納來(lái)說(shuō)可分為兩類:修改外部變量和調(diào)用外部方法。
通過(guò)TestableMock的私有字段訪問(wèn)和Mock校驗(yàn)器可以很方便的實(shí)現(xiàn)對(duì)"副作用"的結(jié)果檢查。
1. 修改外部變量的void方法
例如,下面這個(gè)方法會(huì)根據(jù)輸入修改私有成員變量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));
????}
????...?//?其他方法省略
}
若要測(cè)試此方法,可以利用TestableMock直接讀取私有成員變量的值,對(duì)結(jié)果進(jìn)行校驗(yàn):
@EnablePrivateAccess??//?啟用TestableMock的私有成員訪問(wèn)功能
class?DemoTest?{
????private?Demo?demo?=?new?Demo();
????@Test
????public?void?testSaveToCache()?{
????????Integer?firstVal?=?demo.initHash("hello");?//?訪問(wèn)私有方法
????????Integer?nextVal?=?demo.nextHash(firstVal,?"hello");?//?訪問(wèn)私有方法
????????demo.saveToCache("demo",?"hello");
????????assertEquals(firstVal,?demo.hashCache.get("demo::hello"));?//?讀取私有變量
????????demo.saveToCache("demo",?"hello");
????????assertEquals(nextVal,?demo.hashCache.get("demo::hello"));?//?讀取私有變量
????}
}
2. 調(diào)用外部方法的void方法
例如,下面這個(gè)方法會(huì)根據(jù)輸入打印信息到控制臺(tái):
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());
????}
}
若要測(cè)試此方法,可以利用TestableMock快速M(fèi)ock掉System.out.println方法。在Mock方法體里可以繼續(xù)執(zhí)行原調(diào)用(相當(dāng)于并不影響本來(lái)方法功能,僅用于做調(diào)用記錄),也可以直接留空(相當(dāng)于去除了原方法的副作用)。
在執(zhí)行完被測(cè)的void類型方法以后,用InvokeVerifier.verify()校驗(yàn)傳入的打印內(nèi)容是否符合預(yù)期:
class?DemoTest?{
????private?Demo?demo?=?new?Demo();
????//?攔截`System.out.println`調(diào)用
????@MockMethod
????public?void?println(PrintStream?ps,?String?msg)?{
????????//?執(zhí)行原調(diào)用
????????ps.println(msg);
????}
????@Test
????public?void?testRecordAction()?{
????????Action?action?=?new?Action("click",?":download");
????????demo.recordAction();
????????//?驗(yàn)證Mock方法`println`被調(diào)用,且傳入?yún)?shù)符合預(yù)期
????????verify("println").with(matches("\\d{4}-\\d{2}-\\d{2}?\\d{2}:\\d{2}:\\d{2}?\\[click\\]?:download"));
????}
}
項(xiàng)目地址
開(kāi)源地址:https://gitee.com/mirrors/TestableMock
推薦閱讀:
最近面試BAT,整理一份面試資料《Java面試BAT通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?666?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
明天見(jiàn)(??ω??)??
