Mockito+JMockit+TestNG單元測試實踐總結

單元測試實踐背景

  • 測試環境定位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等。以上是個人拙見,如有不對的地方歡迎大家指正。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • 什么是單元測試 在計算機編程中,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設計的最...
    HelloCsl閱讀 10,982評論 1 46
  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小魚閱讀 2,605評論 0 0
  • @Author:彭海波 前言 單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設計的最小...
    海波筆記閱讀 4,988評論 0 52
  • 本文介紹了Android單元測試入門所需了解的內容,包括JUnit、Mockito和PowerMock的使用,怎樣...
    于衛國閱讀 4,593評論 0 5