單元測試實踐背景
-
測試環境定位bug時,需要測試同學協助手動發起相關業務URL請求,開發進行遠程調試
問題:
1、遠程調試影響測試環境數據正常獲取,影響測試同學測試進度
2、遠程調試代碼有時并非最新代碼,與本地不一致增加調試難度,往往需要發最新的包再調試
3、controller層請求參數依賴特定客戶端版本發起,其他版本回歸驗證,增加模擬操作成本 依賴第三方系統,第三方系統請求不穩定或希望第三方接口返回特定數據
為什么需要單測
編寫單元測試代碼并不是一件容易的事情,那為什么還需要去話費時間和精力來編寫單元測試呢?
減少Bug:如今的項目大多都是多人分模塊協同開發,當各個模塊集成時再去發現問題,定位和溝通成本是非常高的,通過單元測試來保證各個模塊的正確性,可以盡早的發現問題,而不時等到集成時再發現
問題。
放心重構:如今持續型的項目越來越多,代碼不斷的在變化和重構,通過單元測試,開發可以放心的修改重構代碼,減少改代碼時心理負擔,提高重構的成功率。
改進設計:越是良好設計的代碼,一般越容易編寫單元測試,多個小的方法的單測一般比大方法(成百上千行代碼)的單測代碼要簡單、
要穩定,一個依賴接口的類一般比依賴具體實現的類容易測試,所以
在編寫單測的過程中,如果發現單測代碼非常難寫,一般表明被測試
的代碼包含了太多的依賴或職責,需要反思代碼的合理性,進而推進
代碼設計的優化,形成正向循環。
個人感受,將controller層請求參數抽取管理后,debug不依賴客戶端與測試環境,能夠迅速在本地執行定位問題;同時,單元測試提供測試數據準備與模擬特定測試數據返回,對業務測試起輔助作用。
單元測試需要理解的幾個概念
被測系統:SUT(System Under Test)
被測系統(System under test,SUT)表示正在被測試的系統,目的是測試系統能否正確操作。這一詞語常用于軟件測試中。軟件系統測
試的一個特例是對應用軟件的測試,稱為被測應用程序(application under test,AUT)。
SUT也表明軟件已經到了成熟期,因為系統測試在測試周期中是集成測試的后一階段。測試替身:Test Double
在單元測試時,使用Test Double減少對被測對象的依賴,使得測試
更加單一。同時,讓測試案例執行的時間更短,運行更加穩定,同時
能對SUT內部的輸入輸出進行驗證,讓測試更加徹底深入。但是,Test Double也不是萬能的,Test Double不能被過度使用,因為實際交付的產品是使用實際對象的,過度使用Test Double會讓測試變得越來越脫離實際。
要理解測試替身,需要了解一下Dummy Objects、Test Stub、Test Spy、Fake Object 這幾個概念,下面我們對這些概念分別進行說明。Dummy Objects
Dummy Objects泛指在測試中必須傳入的對象,而傳入的這些對象
實際上并不會產生任何作用,僅僅是為了能夠調用被測對象而必須傳
入的一個東西。Test Stub
測試樁是用來接受SUT內部的間接輸入(indirect inputs),并返回特定的值給SUT。可以理解Test Stub是在SUT內部打的一個樁,可以按照我們的要求返回特定的內容給SUT,Test Stub的交互完全在SUT內部,因此,它不會返回內容給測試案例,也不會對SUT內部的輸入進行驗證。
Test Spy
Test Spy像一個間諜,安插在了SUT內部,專門負責將SUT內部的間接輸出(indirect outputs)傳到外部。它的特點是將內部的間接輸出返回給測試案例,由測試案例進行驗證,Test Spy只負責獲取內部情報,并把情報發出去,不負責驗證情報的正確性。
Mock Object
Mock Object和Test Spy有類似的地方,它也是安插在SUT內部,獲取到SUT內部的間接輸出(indirect outputs),不同的是,Mock Object還負責對情報(intelligence)進行驗證,總部(外部的測試案例)信任Mock Object的驗證結果。
Fake Object
經常,我們會把Fake Object和Test Stub搞混,因為它們都和外部沒有交互,對內部的輸入輸出也不進行驗證。不同的是,Fake Object并不關注SUT內部的間接輸入(indirect inputs)或間接輸出(indirect outputs),它僅僅是用來替代一個實際的對象,并且擁有幾乎和實際對象一樣的功能,保證SUT能夠正常工作。實際對象過分依賴外部環境,Fake Object可以減少這樣的依賴。
看完Test Double這幾個概念后,是不是一頭霧水?以下通俗解釋,Dummy Objects就不做解釋了。
Test Stub
系統測試需要某一指定數據返回時,開發將獲取數據邏輯代碼替換成指定數據,發包測試完再替換回原來邏輯。替換代碼返回指定數據,這就是測試樁。Test Spy
Test Stub只返回指定內容給SUT,并沒有指定返回測試案例,所以我們引入單元測試,在單元測試用例調用引用該插樁的方法。
這時我們能獲測試樁間接輸出內容,甚至是報錯信息,再也不用到服務器查找錯誤日志了,這就是Test Spy。Mock Object
Mock Object就是在Test Spy的基礎上,加入驗證機制。調用引用該插樁的方法,我們要確保這個插樁正常被執行或指定執行n次,得到的結果是不是我們期望的結果,mock就以此為生。Fake Object
Fake Object相對Test Stub,是一個面向對象概念。我們只希望替換掉一個實際被引用對象里面的一個方法返回值,被替換某個方法返回值的對象就叫Fake Oject,它與實際對象一樣的功能。Mock Object也囊括Fake Object概念,可以看出Test Stub < Fake Object < Mock Object。
Mock框架模型
測試驗證過程,我們不可能每次都修改代碼stub一個方法,發包驗證完后再改回,發布外網回歸驗證階段這種操作根本不被允許。Mock框架應運而生,我們在單元測試用例stub一個方法后,將之注入被測系統SUT,這個注入只會在test spy階段產生影響。
市面上很多mock框架,Jmockit、Mockito、PowerMock、EasyMock等,大體遵循record-replay-verify模型設計,有些地方稱之為expect-run-verify模式(期望--運行--驗證),有些地方稱之(AAA階段)Arrange 、Act、Assert,大體一個意思。很明顯,Mock框架的應用過程,我們先需要指定stub,然后運行被測方法,然后在驗證stub的正確性,這個過程就稱之為mock。
單元測試框架選擇
Testng
TestNG與Junit很相似, 但testng更加靈活,以下為兩者對比。
[圖片上傳失敗...(image-93566-1513052813178)]
參考 JUnit 4 Vs TestNG比較
- Testng支持分組測試
- Testng參數化測試支持復雜類型參數,而junit只支持基本類型
- Testng提供XML靈活配置測試運行套件
- Testng支持依賴測試
- Testng支持并發測試,上面文章未講到的,補充下。如@Test(threadPoolSize=3,invocationCount=6,timeout=500),而Junit的話可以引入JunitPref框架。
Jmockit
Jmockit是一個功能很強大的框架,可以mock靜態方法、final類、抽象類、接口、構造函數等,幾乎無所不能,但編程語言不夠簡潔。
Jmockit的介紹和使用
這里需要補充的點:
注解@Tested,標識的被測對象實例, @Injectable的實例會自動注入到@Tested中,有時候在事件過程中實在無法注入,可以借助spring的反射工具ReflectionTestUtils進行注入。
Expectations:期望,指定的方法必須被調用,且方法默認次數為1。如果指定打樁的方法在test用例不被調用,或者調用次數超過1,則會報錯,建議使用NonStrictExpectations配合Verifications使用。
Expectations(T)/NonStrictExpectations(T),Expectations(.class){}這種方式只會模擬區域中包含的方法,這個類的其它方法將按照正常的業務邏輯運行,T就變成了一個Fake Object。
MockUp(T)中,未mock的函數不受影響,T也是一個Fake Object。通常rpc接口(接口無具體實現方法)、構造函數通過MockUp進行局部方法mock。
以下主要演示一個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格式,目前不支持默認路徑,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靜態方法mock掉ip,防止http請求獲取Ip報錯
new NonStrictExpectations(WebUtils.class, configService) {
{
WebUtils.getClientIp();
result = "172.17.132.66";
}
{
// 后臺控制百分比,返回0則過濾掉類型為27的視頻,返回100則放開下發該視頻“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區別于其他模擬框架的地方允許開發者在沒有建立“預期”時驗證被測系統的行為,編碼設計簡潔優美,使用簡單快捷,成本低。同時Mockito提供@Spy注解實例,這個注解是將實例對象的指定方法返回值給stub掉,而不是將方法內部處理邏輯給跳過。注意,@Spy監視的是一個真實對象。@Spy錄制期望,調用真實的方法,這個對我們測試來說很重要,因為這樣我們才能保證對stub方法輸入的合理性,對stub方法內部調用正確性,Mockito的@Mock注解包括前的JMockit對一個對象的Mock,都是直接跳過調用真實方法而返回錄制期望值,如果沒錄制則返回null,而@Spy對未stub的方法,返回真實的調用邏輯值。
Mockito的缺點是不能stub靜態方法、final類、構造函數、匿名類,所以最好配合Jmockit使用。
學習參考 Mockito 初探
- 允許開發者在沒有建立“預期”時驗證被測系統的行為,如下實例不建立期望,只驗證交互
// 模擬的創建,對接口進行模擬
List mockedList = mock(List.class);
// 使用模擬對象
mockedList.add("one");
mockedList.clear();
// 選擇性地和顯式地驗證
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
相信眼尖的你通過上面的示例發現了MockMvc,參考學習 SpringMVC 測試 mockMVC
為什么使用MockMvc呢?
從學習參考示例看MockMvc URL調用是不是很貼近接口自動化,MockMvc讓我們能測試完整的Spring MVC流程。我們前面的mock示例中直接調用controller層方法要自行構建參數,得到的函數方法結果要經過fastjson進行轉換才是是最終下發給客戶端的結果,這中間其實繞過了spring mvc攔截器和轉換器,通過MockMvc就跟模擬接口請求一樣,請求經過攔截器驗證、參數自行綁定與轉換等。
MockMvc提供諸如MockHttpServletRequest、MockHttpServletResponse、MockHttpSession重量級對象mock,分別對應HttpServletRequest、HttpServletResponse、HttpSession。
下面示例restful結果通過MockHttpServletResponse輸出,即是返回給客戶端的最終結果。
@Test(description = "頭條get.do接口,通過模擬請求鏈接")
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);
}
紙上得來終覺淺,覺知此事要躬行,在實踐過程中總會發現很多跟網上教案沖突的地方,這時候就要多嘗試多思考多驗證。這里只介紹了單元測試的冰山一角,單元測試還有PowerMock、DbUnit等。以上是個人拙見,如有不對的地方歡迎大家指正。