Spring 單元測試-PowerMockRunner和SpringRunner

概述

  我們在開發過程中,爲了代碼的穩定性也好,爲了少給自己以後的開發挖坑也好,多寫單元測試絕對是一件性價比超高的繁瑣事,既然是繁瑣的事情,我想大部分人是不願意寫的,我也不願意寫,但是要做好一個程序員,不僅僅的去做一個低級碼農,那就從最簡單的地方做起,單元測試就是一件特別簡單的事。寫了單元測試的代碼的健壯性和邏輯性絕對要更上一個層次,而且對於開發而言理解回顧代碼邏輯是一件必不可少的事情
  一個 bug 被隱藏的時間越長,修復這個 bug 的代價就越大。前期多去寫一些邊界測試,後期就有時間學習,開發時間和迴歸回顧時間的比例應該是1:1,單元測試是一個方法層面上的測試,也是最細粒度的測試。用於測試一個類的每一個方法都已經滿足了方法的功能要求,可以避免測試點的遺漏,爲了更好的進行測試,可以提高測試效率。在開發中,對於自己開發的模塊,只有在通過單元測試之後,才能提交到 SVN 庫 或者 Git 庫。

工具

  我將在下面介紹下PowerMockRunner和SpringRunner兩個單元測試的運行環境。

SpringRunner

  SpringRunner 繼承了SpringJUnit4ClassRunner,SpringRunner是SpringJUnit4ClassRunner的一個別名,沒有擴展任何功能。下面我們來看下示例

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceNoMockTest {

    @Autowired
    private UserService userService;

    @Test
    public void getUser() throws Exception {
        UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 1st time: result = " + JSON.toJSONString(result));
    }
}

返回顯示:
在這裏插入圖片描述
分析:
首先我們分析下註解:
@RunWith: 用於指定junit運行環境,是junit提供給其他框架測試環境接口擴展,爲了便於使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作爲JUnit測試環境。在JUnit中有很多個Runner,他們負責調用你的測試代碼,每一個Runner都有各自的特殊功能,你要根據需要選擇不同的Runner來運行你的測試代碼。我們此篇文章只探討SpringRunner和PowerMockRunner。
@SpringBootTest: 替代了spring-test中的@ContextConfiguration註解,目的是加載ApplicationContext,啓動spring容器@SpringBootTest註解會自動檢索程序的配置文件,檢索順序是從當前包開始,逐級向上查找被@SpringBootApplication或@SpringBootConfiguration註解的類。

提出問題: 這樣的測試方式會連接數據庫,進行網絡請求等等耗時操作,甚至數據庫中的準備數據是個非常麻煩的事情,各個邏輯分支如果都需要測試,這在大型的項目中工作量是不可想象的,所以有沒有可能對數據庫這種操作直接不處理,這需要mock能力,爲此引入了MockBean,下面來看示例:

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.ArgumentMatchers.any;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;
    @MockBean
    private UserInfoMapper userInfoMapper;

    @Test
    public void getUser() {
        UserInfo result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 1st time: result = " + JSON.toJSONString(result));

        UserInfo userInfo = mockUserInfo();
        Mockito.when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);

        result = userService.getUser("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println("the 2nd time: result = " + JSON.toJSONString(result));
    }

    private UserInfo mockUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId("654321");
        userInfo.setUserName("B");
        userInfo.setPassword("pwB");
        userInfo.setGender("女");
        return userInfo;
    }
}

返回顯示:
在這裏插入圖片描述
分析:
  我們在UserInfoMapper上加了註解@MockBean,這個註解的意思所有的UserInfoMapper的方法都被mock,返回都將變成null,所以第一次返回值爲null;我們在測試方法中藉助Mockito.when將userInfoMapper.selectByUserId的返回值進行自定義,這樣就完成了對數據庫的返回對象的控制,不用怕數據庫的更改導致測試用例的分支沒有跑完成的問題出現。

提出問題: 我們發現這樣的處理雖然符合了上個問題的預期,但是又有了新的問題出現,第一:既然將數據庫和網絡操作全部mock,那好像就沒有必要啓動Spring,@MockBean有個特別大的危害就是導致頻繁的啓動Spring,而spring boot 的啓動時間比較耗時,所以@MockBean,很有可能導致測試運行的很慢;第二:如果有這樣一個方法,是UserService 的私有方法,裏面特別複雜的操作,我並不想在這個測試用例中進行測試,可以不可以mock,或者有一個靜態類,類中的方法有沒有方式mock,還有UserService的變量我又沒有方式可以直接賦值等等,這些SpringRunner也就是SpringJUnit4ClassRunner都是沒有辦法解決的,這需要下面的有個JUNIT工具解決,也就是PowerMockRunner。

PowerMockRunner

  PowerMock基本上cover了所有Mockito不能支持的case(大多數情況也就是靜態方法,但其實也可以支持私有方法和構造函數的調用)。PowerMock使用了字節碼操作,因此它是自帶Junit runner的。在使用PowerMock時,必須使用@PrepareForTest註釋被測類,mock纔會被執行。下面我們來看示例:
對UserService進行單元測試:

package com.hly.unitest.service;

import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.model.UserInfoRequest;
import com.hly.unitest.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
public class UserService {

    private String finalStr = "finalStr";
    @Autowired
    private UserInfoMapper userInfoMapper;

    public void save(UserInfoRequest request) {

        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(request.getUserId());
        userInfo.setUserName(request.getUserName());
        userInfo.setPassword(request.getPassword());
        userInfo.setGender(request.getGender());
        Date currDate = CommonUtil.getMaxDayOfThisMonth(new Date());
        userInfo.setCreateTime(currDate);
        userInfo.setUpdateTime(currDate);
        userInfoMapper.insertSelective(userInfo);
    }

    public UserInfo getUserInfo(String userId) {
        if (CommonUtil.isEmpty(userId)) {
            return null;
        }

        String assetStr = getAssetStr0("a", "b");
        if (!"un".equals(assetStr)){
            System.out.println("assetStr = " + assetStr);
            return null;
        }
        getAssetNull();

        UserInfo userInfo = userInfoMapper.selectByUserId(userId);
        return userInfo;
    }

    public UserInfo getUser(String userId) {
        UserInfo userInfo = userInfoMapper.selectByUserId(userId);
        return userInfo;
    }

    public String testStaticMethod() {

        String randomStr = CommonUtil.generateUUID();
        return randomStr;
    }

    public String testClassVariables() {

        return finalStr;
    }

    private String getAssetStr() {
        System.out.println("getAssetStr");
        return "unit-test";
    }

    private String getAssetStr0(String a, String b) {
        System.out.println("getAssetStr0");
        return "unit-test";
    }

    private void getAssetNull() {
        System.out.println("getAssetNull");
        return;
    }
}

單元測試如下:

package com.hly.unitest.controller;

import com.alibaba.fastjson.JSON;
import com.hly.unitest.dao.UserInfoMapper;
import com.hly.unitest.entity.UserInfo;
import com.hly.unitest.service.UserService;
import com.hly.unitest.util.CommonUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import org.springframework.test.util.ReflectionTestUtils;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class, CommonUtil.class})
public class UserServicePowerRunnerTest {

    @InjectMocks
    private UserService userService;
    @Mock
    private UserInfoMapper userInfoMapper;

    @Before
    public void setUp() {
        // mock私有方法
        userService = PowerMockito.spy(new UserService());
        ReflectionTestUtils.setField(userService, "userInfoMapper", userInfoMapper);
    }

    @Test
    public void getUserInfo() throws Exception {
        UserInfo userInfo = mockUserInfo();
        when(userInfoMapper.selectByUserId(any())).thenReturn(userInfo);

        // 控制私有方法的返回值
        PowerMockito.doReturn("un").when(userService, "getAssetStr0", any(), any());

        UserInfo result = userService.getUserInfo("6f49aa9e-1afc-4439-ad21-25ae90dde566");
        System.out.println(JSON.toJSONString(result));
    }

    @Test
    public void testStaticMethod() throws Exception {
        String result = userService.testStaticMethod();
        System.out.println("the 1st time: result = " + result);

        // mock靜態方法
        PowerMockito.mockStatic(CommonUtil.class);
        String str = "123";
        when(CommonUtil.generateUUID()).thenReturn(str);
        result = userService.testStaticMethod();
        System.out.println("the 2nd time: result = " + result);
    }

    @Test
    public void testClassVariables() throws Exception {
        String result = userService.testClassVariables();
        System.out.println("the 1st time: result = " + result);

        // mock類變量
        Whitebox.setInternalState(userService, "finalStr", "FINAL-STR");
        result = userService.testClassVariables();
        System.out.println("the 2nd time: result = " + result);
    }

    private UserInfo mockUserInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId("654321");
        userInfo.setUserName("B");
        userInfo.setPassword("pwB");
        userInfo.setGender("女");
        return userInfo;
    }
}

測試getUserInfo返回:
在這裏插入圖片描述
測試testStaticMethod返回:
在這裏插入圖片描述
測試testClassVariables返回:
在這裏插入圖片描述
分析:
  可以看到各個測試用例的返回都符合預期,而且沒有啓動Spring,這極大的縮短了執行時間,方便了測試用例的編寫,所以目前來看,我推薦大家使用PowerMockRunner,輕量級的使用,測試覆蓋分支也是極方便的。

提出問題: 我們項目組的覆蓋率插件使用的是jacoco,但是PowerMockRunner和jacoco不兼容,使得我們項目的覆蓋率一直在10%左右徘徊,對於怎麼解決這個問題,一直以來,頭特別的大,在查了很多資料之後找到一個解決方案。

發佈了26 篇原創文章 · 獲贊 6 · 訪問量 6547
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章