單元測試實(shí)踐背景
-
測試環(huán)境定位bug時(shí)通今,需要測試同學(xué)協(xié)助手動(dòng)發(fā)起相關(guān)業(yè)務(wù)URL請(qǐng)求辫塌,開發(fā)進(jìn)行遠(yuǎn)程調(diào)試
問題:
1掺喻、遠(yuǎn)程調(diào)試影響測試環(huán)境數(shù)據(jù)正常獲取,影響測試同學(xué)測試進(jìn)度
2持隧、遠(yuǎn)程調(diào)試代碼有時(shí)并非最新代碼抑月,與本地不一致增加調(diào)試難度,往往需要發(fā)最新的包再調(diào)試
3舆蝴、controller層請(qǐng)求參數(shù)依賴特定客戶端版本發(fā)起,其他版本回歸驗(yàn)證题诵,增加模擬操作成本 依賴第三方系統(tǒng)洁仗,第三方系統(tǒng)請(qǐng)求不穩(wěn)定或希望第三方接口返回特定數(shù)據(jù)
為什么需要單測
編寫單元測試代碼并不是一件容易的事情,那為什么還需要去話費(fèi)時(shí)間和精力來編寫單元測試呢性锭?
減少Bug:如今的項(xiàng)目大多都是多人分模塊協(xié)同開發(fā)赠潦,當(dāng)各個(gè)模塊集成時(shí)再去發(fā)現(xiàn)問題,定位和溝通成本是非常高的草冈,通過單元測試來保證各個(gè)模塊的正確性拳恋,可以盡早的發(fā)現(xiàn)問題,而不時(shí)等到集成時(shí)再發(fā)現(xiàn)
問題轰驳。
放心重構(gòu):如今持續(xù)型的項(xiàng)目越來越多,代碼不斷的在變化和重構(gòu),通過單元測試,開發(fā)可以放心的修改重構(gòu)代碼袋狞,減少改代碼時(shí)心理負(fù)擔(dān)湾蔓,提高重構(gòu)的成功率桃序。
改進(jìn)設(shè)計(jì):越是良好設(shè)計(jì)的代碼泛释,一般越容易編寫單元測試魂贬,多個(gè)小的方法的單測一般比大方法(成百上千行代碼)的單測代碼要簡單愈犹、
要穩(wěn)定饭玲,一個(gè)依賴接口的類一般比依賴具體實(shí)現(xiàn)的類容易測試次哈,所以
在編寫單測的過程中滋恬,如果發(fā)現(xiàn)單測代碼非常難寫,一般表明被測試
的代碼包含了太多的依賴或職責(zé)苦银,需要反思代碼的合理性烙博,進(jìn)而推進(jìn)
代碼設(shè)計(jì)的優(yōu)化,形成正向循環(huán)予颤。
個(gè)人感受,將controller層請(qǐng)求參數(shù)抽取管理后政供,debug不依賴客戶端與測試環(huán)境,能夠迅速在本地執(zhí)行定位問題沦寂;同時(shí),單元測試提供測試數(shù)據(jù)準(zhǔn)備與模擬特定測試數(shù)據(jù)返回济榨,對(duì)業(yè)務(wù)測試起輔助作用榄棵。
單元測試需要理解的幾個(gè)概念
被測系統(tǒng):SUT(System Under Test)
被測系統(tǒng)(System under test,SUT)表示正在被測試的系統(tǒng)疹味,目的是測試系統(tǒng)能否正確操作。這一詞語常用于軟件測試中。軟件系統(tǒng)測
試的一個(gè)特例是對(duì)應(yīng)用軟件的測試边臼,稱為被測應(yīng)用程序(application under test臼予,AUT)追驴。
SUT也表明軟件已經(jīng)到了成熟期,因?yàn)橄到y(tǒng)測試在測試周期中是集成測試的后一階段备禀。測試替身:Test Double
在單元測試時(shí),使用Test Double減少對(duì)被測對(duì)象的依賴奈揍,使得測試
更加單一曲尸。同時(shí),讓測試案例執(zhí)行的時(shí)間更短男翰,運(yùn)行更加穩(wěn)定另患,同時(shí)
能對(duì)SUT內(nèi)部的輸入輸出進(jìn)行驗(yàn)證,讓測試更加徹底深入蛾绎。但是昆箕,Test Double也不是萬能的,Test Double不能被過度使用租冠,因?yàn)閷?shí)際交付的產(chǎn)品是使用實(shí)際對(duì)象的鹏倘,過度使用Test Double會(huì)讓測試變得越來越脫離實(shí)際。
要理解測試替身顽爹,需要了解一下Dummy Objects纤泵、Test Stub、Test Spy镜粤、Fake Object 這幾個(gè)概念捏题,下面我們對(duì)這些概念分別進(jìn)行說明。Dummy Objects
Dummy Objects泛指在測試中必須傳入的對(duì)象繁仁,而傳入的這些對(duì)象
實(shí)際上并不會(huì)產(chǎn)生任何作用涉馅,僅僅是為了能夠調(diào)用被測對(duì)象而必須傳
入的一個(gè)東西归园。Test Stub
測試樁是用來接受SUT內(nèi)部的間接輸入(indirect inputs)黄虱,并返回特定的值給SUT∮褂眨可以理解Test Stub是在SUT內(nèi)部打的一個(gè)樁捻浦,可以按照我們的要求返回特定的內(nèi)容給SUT晤揣,Test Stub的交互完全在SUT內(nèi)部,因此朱灿,它不會(huì)返回內(nèi)容給測試案例昧识,也不會(huì)對(duì)SUT內(nèi)部的輸入進(jìn)行驗(yàn)證。
Test Spy
Test Spy像一個(gè)間諜盗扒,安插在了SUT內(nèi)部跪楞,專門負(fù)責(zé)將SUT內(nèi)部的間接輸出(indirect outputs)傳到外部。它的特點(diǎn)是將內(nèi)部的間接輸出返回給測試案例侣灶,由測試案例進(jìn)行驗(yàn)證甸祭,Test Spy只負(fù)責(zé)獲取內(nèi)部情報(bào),并把情報(bào)發(fā)出去褥影,不負(fù)責(zé)驗(yàn)證情報(bào)的正確性池户。
Mock Object
Mock Object和Test Spy有類似的地方,它也是安插在SUT內(nèi)部凡怎,獲取到SUT內(nèi)部的間接輸出(indirect outputs)校焦,不同的是,Mock Object還負(fù)責(zé)對(duì)情報(bào)(intelligence)進(jìn)行驗(yàn)證统倒,總部(外部的測試案例)信任Mock Object的驗(yàn)證結(jié)果寨典。
Fake Object
經(jīng)常,我們會(huì)把Fake Object和Test Stub搞混房匆,因?yàn)樗鼈兌己屯獠繘]有交互凝赛,對(duì)內(nèi)部的輸入輸出也不進(jìn)行驗(yàn)證。不同的是坛缕,F(xiàn)ake Object并不關(guān)注SUT內(nèi)部的間接輸入(indirect inputs)或間接輸出(indirect outputs)墓猎,它僅僅是用來替代一個(gè)實(shí)際的對(duì)象,并且擁有幾乎和實(shí)際對(duì)象一樣的功能赚楚,保證SUT能夠正常工作毙沾。實(shí)際對(duì)象過分依賴外部環(huán)境,F(xiàn)ake Object可以減少這樣的依賴宠页。
看完Test Double這幾個(gè)概念后左胞,是不是一頭霧水?以下通俗解釋举户,Dummy Objects就不做解釋了烤宙。
Test Stub
系統(tǒng)測試需要某一指定數(shù)據(jù)返回時(shí),開發(fā)將獲取數(shù)據(jù)邏輯代碼替換成指定數(shù)據(jù)俭嘁,發(fā)包測試完再替換回原來邏輯境蜕。替換代碼返回指定數(shù)據(jù),這就是測試樁彬犯。Test Spy
Test Stub只返回指定內(nèi)容給SUT,并沒有指定返回測試案例罢猪,所以我們引入單元測試,在單元測試用例調(diào)用引用該插樁的方法叉瘩。
這時(shí)我們能獲測試樁間接輸出內(nèi)容膳帕,甚至是報(bào)錯(cuò)信息,再也不用到服務(wù)器查找錯(cuò)誤日志了薇缅,這就是Test Spy危彩。Mock Object
Mock Object就是在Test Spy的基礎(chǔ)上,加入驗(yàn)證機(jī)制泳桦。調(diào)用引用該插樁的方法恬砂,我們要確保這個(gè)插樁正常被執(zhí)行或指定執(zhí)行n次,得到的結(jié)果是不是我們期望的結(jié)果蓬痒,mock就以此為生泻骤。Fake Object
Fake Object相對(duì)Test Stub,是一個(gè)面向?qū)ο蟾拍钗嗌荨N覀冎幌M鎿Q掉一個(gè)實(shí)際被引用對(duì)象里面的一個(gè)方法返回值狱掂,被替換某個(gè)方法返回值的對(duì)象就叫Fake Oject,它與實(shí)際對(duì)象一樣的功能亲轨。Mock Object也囊括Fake Object概念趋惨,可以看出Test Stub < Fake Object < Mock Object。
Mock框架模型
測試驗(yàn)證過程惦蚊,我們不可能每次都修改代碼stub一個(gè)方法器虾,發(fā)包驗(yàn)證完后再改回,發(fā)布外網(wǎng)回歸驗(yàn)證階段這種操作根本不被允許蹦锋。Mock框架應(yīng)運(yùn)而生兆沙,我們?cè)趩卧獪y試用例stub一個(gè)方法后,將之注入被測系統(tǒng)SUT莉掂,這個(gè)注入只會(huì)在test spy階段產(chǎn)生影響葛圃。
市面上很多mock框架,Jmockit憎妙、Mockito库正、PowerMock、EasyMock等厘唾,大體遵循record-replay-verify模型設(shè)計(jì)褥符,有些地方稱之為expect-run-verify模式(期望--運(yùn)行--驗(yàn)證),有些地方稱之(AAA階段)Arrange 抚垃、Act喷楣、Assert趟大,大體一個(gè)意思。很明顯抡蛙,Mock框架的應(yīng)用過程,我們先需要指定stub魂迄,然后運(yùn)行被測方法粗截,然后在驗(yàn)證stub的正確性,這個(gè)過程就稱之為mock捣炬。
單元測試框架選擇
Testng
TestNG與Junit很相似熊昌, 但testng更加靈活,以下為兩者對(duì)比湿酸。
[圖片上傳失敗...(image-93566-1513052813178)]
參考 JUnit 4 Vs TestNG比較
- Testng支持分組測試
- Testng參數(shù)化測試支持復(fù)雜類型參數(shù)婿屹,而junit只支持基本類型
- Testng提供XML靈活配置測試運(yùn)行套件
- Testng支持依賴測試
- Testng支持并發(fā)測試,上面文章未講到的推溃,補(bǔ)充下昂利。如@Test(threadPoolSize=3,invocationCount=6,timeout=500),而Junit的話可以引入JunitPref框架。
Jmockit
Jmockit是一個(gè)功能很強(qiáng)大的框架铁坎,可以mock靜態(tài)方法蜂奸、final類、抽象類硬萍、接口扩所、構(gòu)造函數(shù)等,幾乎無所不能朴乖,但編程語言不夠簡潔祖屏。
Jmockit的介紹和使用
這里需要補(bǔ)充的點(diǎn):
注解@Tested,標(biāo)識(shí)的被測對(duì)象實(shí)例, @Injectable的實(shí)例會(huì)自動(dòng)注入到@Tested中,有時(shí)候在事件過程中實(shí)在無法注入,可以借助spring的反射工具ReflectionTestUtils進(jìn)行注入买羞。
Expectations:期望袁勺,指定的方法必須被調(diào)用,且方法默認(rèn)次數(shù)為1畜普。如果指定打樁的方法在test用例不被調(diào)用魁兼,或者調(diào)用次數(shù)超過1,則會(huì)報(bào)錯(cuò)漠嵌,建議使用NonStrictExpectations配合Verifications使用咐汞。
Expectations(T)/NonStrictExpectations(T),Expectations(.class){}這種方式只會(huì)模擬區(qū)域中包含的方法,這個(gè)類的其它方法將按照正常的業(yè)務(wù)邏輯運(yùn)行儒鹿,T就變成了一個(gè)Fake Object化撕。
MockUp(T)中,未mock的函數(shù)不受影響,T也是一個(gè)Fake Object约炎。通常rpc接口(接口無具體實(shí)現(xiàn)方法)植阴、構(gòu)造函數(shù)通過MockUp進(jìn)行局部方法mock蟹瘾。
以下主要演示一個(gè)rpc接口的mock。
public class ColumnArticlesControllerTest2 extends BaseContorllerMockTest {
private MockMvc mockMvc;
@Autowired
private ConfigService configService;
@Autowired
private ICpDataKievHandler cpDataKievHandler;
@Autowired
private IndexArticlesDaoCacheImpl indexArticlesDao;
@Autowired
private ColumnArticlesController columnArticlesController;
@BeforeMethod()
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(columnArticlesController).build();
}
// CSV最好使用gbk格式掠手,目前不支持默認(rèn)路徑憾朴,CSV文件位于到dataprovider目錄下
@Test(description = "測試list.do接口", dataProvider = "genData", dataProviderClass = CommonDataProvider.class)
@Csv("/dataprovider/ColumnArticlesControllerTest/testGetColumnArticleList.csv")
public void testGetColumnArticleList(String cpChannelId, long columnId, String ucParam, Integer v, String flymeuid,
String nt, String vn, String deviceinfo, String deviceType, String os, Integer supportSDK, Integer cpType)
throws Exception {
String imei = deviceinfo.substring(deviceinfo.indexOf("imei="), deviceinfo.indexOf("&"));
ArticleView params = new ArticleView();
params.setCpChannelId(cpChannelId);
params.setColumnId(columnId);
params.setUcparam(ucParam);
params.setClientReqId(System.currentTimeMillis() + imei);
CommonParams commonParams = new CommonParams();
commonParams.setV(v);
commonParams.setFlymeuid(flymeuid);
commonParams.setNt(nt);
commonParams.setVn(vn);
commonParams.setDeviceinfo(DeviceUtil.deviceToEncrypt(deviceinfo));
commonParams.setDeviceType(deviceType);
commonParams.setOs(os);
System.out.println(configService.getConfigValue(ConfigKeyEnum.UC_VIDEO_PER));
// jmock靜態(tài)方法mock掉ip,防止http請(qǐng)求獲取Ip報(bào)錯(cuò)
new NonStrictExpectations(WebUtils.class, configService) {
{
WebUtils.getClientIp();
result = "172.17.132.66";
}
{
// 后臺(tái)控制百分比,返回0則過濾掉類型為27的視頻喷鸽,返回100則放開下發(fā)該視頻“XXX鍵盤”
configService.getConfigValue(ConfigKeyEnum.UC_VIDEO_PER);
result = "100";
}
};
final ICpDataKievHandler cpDataKievHandler2 = cpDataKievHandler;
try {
String video27Articles = FileUtils
.getFileText(FileUtils.getCurrentProjectPath() + "/src/test/resources/afdata/video27Articles.json");
final CpDataResult value = JSON.parseObject(video27Articles, CpDataResult.class);
cpDataKievHandler = new MockUp<ICpDataKievHandler>() {
@mockit.Mock
CpDataResult getUCArticleList(String imei, long channelId, String method, String recoid, long ftime,
String cityCode, String cityName, int pageSize) {
return value;
}
}.getMockInstance();
ReflectionTestUtils.setField(indexArticlesDao, "cpDataKievHandler", cpDataKievHandler);
System.out.println(JSON
.toJSON(columnArticlesController.getColumnArticleList(params, supportSDK, cpType, commonParams)));
} finally {
//mock完還原接口方法取值众雷,避免影響其他用例
ReflectionTestUtils.setField(indexArticlesDao, "cpDataKievHandler", cpDataKievHandler2);
}
}
Mockito
Mockito區(qū)別于其他模擬框架的地方允許開發(fā)者在沒有建立“預(yù)期”時(shí)驗(yàn)證被測系統(tǒng)的行為,編碼設(shè)計(jì)簡潔優(yōu)美做祝,使用簡單快捷砾省,成本低。同時(shí)Mockito提供@Spy注解實(shí)例混槐,這個(gè)注解是將實(shí)例對(duì)象的指定方法返回值給stub掉编兄,而不是將方法內(nèi)部處理邏輯給跳過。注意声登,@Spy監(jiān)視的是一個(gè)真實(shí)對(duì)象狠鸳。@Spy錄制期望,調(diào)用真實(shí)的方法悯嗓,這個(gè)對(duì)我們測試來說很重要碰煌,因?yàn)檫@樣我們才能保證對(duì)stub方法輸入的合理性,對(duì)stub方法內(nèi)部調(diào)用正確性绅作,Mockito的@Mock注解包括前的JMockit對(duì)一個(gè)對(duì)象的Mock芦圾,都是直接跳過調(diào)用真實(shí)方法而返回錄制期望值,如果沒錄制則返回null俄认,而@Spy對(duì)未stub的方法个少,返回真實(shí)的調(diào)用邏輯值。
Mockito的缺點(diǎn)是不能stub靜態(tài)方法眯杏、final類夜焦、構(gòu)造函數(shù)、匿名類岂贩,所以最好配合Jmockit使用茫经。
學(xué)習(xí)參考 Mockito 初探
- 允許開發(fā)者在沒有建立“預(yù)期”時(shí)驗(yàn)證被測系統(tǒng)的行為,如下實(shí)例不建立期望萎津,只驗(yàn)證交互
// 模擬的創(chuàng)建卸伞,對(duì)接口進(jìn)行模擬
List mockedList = mock(List.class);
// 使用模擬對(duì)象
mockedList.add("one");
mockedList.clear();
// 選擇性地和顯式地驗(yàn)證
verify(mockedList).add("one");
verify(mockedList).clear();
- 與spring組合的簡單示例:
public class SearchControllerTest extends BaseContorllerMockTest {
private MockMvc mockMvc;
private static final Logger ILOG = LoggerFactory.getLogger(SearchControllerTest.class);
@Autowired
private IRedisClient redisClient;
@Spy
@Autowired
private SearchService searchService;
@InjectMocks
@Autowired
private SearchController searchController;
@BeforeMethod()
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(searchController).build();
MockitoAnnotations.initMocks(this);
}
@Test
public void testGetHotWords(){
Mockito.when(searchService.getHotWords(Mockito.anyInt(), Mockito.anyInt())).thenReturn(Arrays.asList("周杰倫","林俊杰"));
System.out.println(JSON.toJSON(searchController.getHotWords(0, 30)));//輸出{"value":{"words":["周杰倫","林俊杰"]},"message":"","redirect":"","code":200}
}
}
MockMvc
相信眼尖的你通過上面的示例發(fā)現(xiàn)了MockMvc,參考學(xué)習(xí) SpringMVC 測試 mockMVC
為什么使用MockMvc呢?
從學(xué)習(xí)參考示例看MockMvc URL調(diào)用是不是很貼近接口自動(dòng)化锉屈,MockMvc讓我們能測試完整的Spring MVC流程荤傲。我們前面的mock示例中直接調(diào)用controller層方法要自行構(gòu)建參數(shù),得到的函數(shù)方法結(jié)果要經(jīng)過fastjson進(jìn)行轉(zhuǎn)換才是是最終下發(fā)給客戶端的結(jié)果颈渊,這中間其實(shí)繞過了spring mvc攔截器和轉(zhuǎn)換器遂黍,通過MockMvc就跟模擬接口請(qǐng)求一樣终佛,請(qǐng)求經(jīng)過攔截器驗(yàn)證、參數(shù)自行綁定與轉(zhuǎn)換等雾家。
MockMvc提供諸如MockHttpServletRequest铃彰、MockHttpServletResponse、MockHttpSession重量級(jí)對(duì)象mock芯咧,分別對(duì)應(yīng)HttpServletRequest牙捉、HttpServletResponse、HttpSession唬党。
下面示例restful結(jié)果通過MockHttpServletResponse輸出鹃共,即是返回給客戶端的最終結(jié)果鬼佣。
@Test(description = "頭條get.do接口,通過模擬請(qǐng)求鏈接")
public void testGetMethodThroughMockRequestUrl() throws Exception {
MvcResult result = mockMvc
.perform(get("/android/unauth/settings/get.do").param("v", "3021000").param("flymeuid", "113516747")
.param("nt", "wifi").param("deviceType", "mx5").param("os", "5.1-1505319080_stable")
.param("vn", "3.21.0").param("deviceinfo",
"v6FBm9zBUDEtahUN942%2Fyg9SrkQPmTvaFwvgfujjfk%2BxjcNQL0fr1Knx9TMeqzZVAQVBqkdzfe9b9ZM8P2p%2BucjGohlhGn0MvEKrSJ1XbUYOEBTUJG%2Bjvvf1c2v0qXhfqkx37mT%2Ffii1KgiQ6zGNhOLjjN9QxC1Lsx2D6jDPqcQ%3D"))
.andReturn();
MockHttpServletResponse mockHttpServletResponse = result.getResponse();
String s = mockHttpServletResponse.getContentAsString();
System.out.println(s);
}
紙上得來終覺淺驶拱,覺知此事要躬行,在實(shí)踐過程中總會(huì)發(fā)現(xiàn)很多跟網(wǎng)上教案沖突的地方晶衷,這時(shí)候就要多嘗試多思考多驗(yàn)證蓝纲。這里只介紹了單元測試的冰山一角,單元測試還有PowerMock晌纫、DbUnit等税迷。以上是個(gè)人拙見,如有不對(duì)的地方歡迎大家指正锹漱。