1. 程式人生 > >powermockito單元測試之深入實踐

powermockito單元測試之深入實踐

概述

由於最近工作需要, 在專案中要做單元測試, 以達到指定的測試用例覆蓋率指標。專案中我們引入的powermockito來編寫測試用例, JaCoCo來監控單元測試覆蓋率。關於框架的選擇, 網上討論mockito和powermockito孰優孰劣的文章眾多, 這裡就不多做闡述, 讀者如有興趣可自行了解。

依賴引入

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4-rule-agent</artifactId>
    <version>1.6.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.6.6</version>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.6.6</version>
</dependency>

被測試類

public class PowerMockitoDemo {

    @Autowired
    private StudentDao studentDao;

    @Autowired
    private TeacherService teacherService;

    public void study() {
        //doSomething
    }

    private void play(Map<String, Object> project, Person person, int hours) {
        
//doSomething } private boolean updateStudentName(String newName) { //doSomething } public ServiceResult grantRights(List<String> usernames, String rights, String orderid, String id) { String value1 = PropertiesUtil.get("key1"); String value2 = PropertiesUtil.get("key2"); studentDao.saveRecord(usernames);
//返回值型別void Student student = studentDao.getStudentById(id); boolean result = this.verifyParams(usernames); teacherService.syncDB2Redis(usernames, orderid); this.updateOperation(rights); } private boolean verifyParams(List<String> usernames) { //doSomething } private void updateOperation(String rights) { //doSomething } }

測試用例基類

//@PrepareForTest註解和@RunWith註解需結合使用,單獨使用將不起作用
@RunWith(PowerMockRunner.class)
@PrepareForTest({RedisUtils.class})
@SuppressStaticInitializationFor({"com.test.util.RedisUtils", "com.test.util.HttpUtils"})//用於阻止類中的靜態程式碼塊執行
public abstract class BaseTest {

    public RedisUtils redisUtils;

    @Rule
    public ExpectedException thrown = ExpectedException.none();//斷言要丟擲的異常

    public void setUp() {
        initMocks(this);

        PowerMockito.suppress(PowerMockito.constructor(ShardedJedisClientImpl.class, String.class));
        redisUtils = PowerMockito.mock(RedisUtils.class);
        Whitebox.setInternalState(RedisUtils.class, "redisUtil", redisUtils);//給類或例項物件的成員變數設定模擬值,這裡是給RedisUtils類中的欄位redisUtil設定模擬值

        PowerMockito.suppress(PowerMockito.constructor(HttpUtils.class));
        PowerMockito.mockStatic(HttpUtils.class);//mock類中所有靜態方法
    }

    /**
     * @param instance 真實物件
     * @param methodName 方法名
     * @param args 形參列表
     */
    public Object callPrivateMethod(Object instance, String methodName, Object... args) throws Exception {
        return Whitebox.invokeMethod(instance, methodName, args);//呼叫私有方法
    }

}

測試用例

@PrepareForTest({PowerMockitoDemo.class, PropertiesUtil.class})//此處PowerMockitoDemo被測試類新增到@PrepareForTest註解中, 用於mock其靜態、final修飾及私有方法;另外,PropertiesUtil工具類由於不通用,不適合抽取到基類BaseTest中, 可在子類mock
@SuppressStaticInitializationFor("com.test.util.PropertiesUtil")//用於阻止類中的靜態程式碼塊執行
public class PowerMockitoDemoTest extends BaseTest{

    @org.powermock.core.classloader.annotations.Mock
    private StudentDao studentDao;

    @org.powermock.core.classloader.annotations.Mock
    private TeacherService teacherService;

    @org.powermock.core.classloader.annotations.Mock
    @InjectMocks
    private PowerMockitoDemo powerMockitoDemo;

    @Override
    @Before
    public void setUp() {
        super.setUp();
        PowerMockito.suppress(PowerMockito.constructor(PropertiesUtil.class));
        PowerMockito.mockStatic(PropertiesUtil.class);//mock類中所有靜態方法
    }

    @Test
    public void studyWhenCallSuccessfully() {
        PowerMockito.doCallRealMethod().when(powerMockitoDemo).study();
        //doSomething

        powerMockitoDemo.study();
    }

    @Test
    public void playWhenCallSuccessfully() {
        Map<String, Object> project = new HashMap<String, Object>();
        Person person = new Person();
        int hours = 8;

        PowerMockito.doCallRealMethod().when(powerMockitoDemo, "play", Matchers.anyMapOf(String.class, Object.class), Matchers.any(Person.class), Matchers.anyInt());
        //doSomething

        this.callPrivateMethod(powerMockitoDemo, "play", project, person, hours);
    }

    @Test
    public void updateStudentNameWhenCallSuccessfully() throws Exception {
        String id = "9527";

        PowerMockito.when(powerMockitoDemo, "updateStudentName", Matchers.anyString()).thenCallRealMethod();
        //doSomething

        boolean actualResult = this.callPrivateMethod(powerMockitoDemo, "updateStudentName", id);
        Assert.assertTrue(actualResult == true);
    }

    @Test
    public void getStudentByIdWhenCallSuccessfully() throws Exception {
        List<String> usernames = new ArrayList<String>();
        String rights = "萬葉飛花流";
        String orderid = "orderid";
        String id = "id";

        //呼叫真實方法
        PowerMockito.when(powerMockitoDemo.grantRights(Matchers.anyListOf(String.class), Matchers.anyString(), Matchers.anyString(), Matchers.anyString())).thenCallRealMethod();
        //當方法內重複呼叫同一個方法時, 可通過Matchers.eq()方法來指定實際入參來加以區分
        PowerMockito.when(PropertiesUtil.get(Matchers.eq("key1"))).thenReturn("value1");
        PowerMockito.when(PropertiesUtil.get(Matchers.eq("key2"))).thenReturn("value2");
        //返回值型別為void,不做任何事情
        PowerMockito.doNothing().when(studentDao).saveRecord(Matchers.anyListOf(String.class));
        //類似需要呼叫資料庫、redis、遠端服務的,可直接模擬返回值,不做方法的真實呼叫
        PowerMockito.when(studentDao.getStudentById(Matchers.anyString())).thenReturn(new Student());
        //呼叫真實的私有方法
        PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenCallRealMethod();
        //模擬私有方法返回值
        PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenReturn(true);
        //模擬方法呼叫丟擲異常。當被呼叫的方法頭沒有顯式宣告異常時, 則mock只支援unchecked exception,比如這裡syncDB2Redis()方法簽名沒有宣告任何異常,則thenThrow()模擬異常時只支援模擬執行時異常,使用非執行時異常將編譯不通過
        PowerMockito.when(teacherService.syncDB2Redis(Matchers.anyListOf(String.class), Matchers.anyString())).thenThrow(new RuntimeException("Failed to call remote service"));
        PowerMockito.doNothing().when(powerMockitoDemo, "updateOperation", Matchers.anyString());

        ServiceResult expectedResult = new ServiceResult();
        ServiceResult actualResult = powerMockitoDemo.grantRights(usernames, rights, orderid, id);
        //斷言實際呼叫結果是否符合預期值
        Assert.assertEquals(JSON.toJSONString(expectedResult), JSON.toJSONString(actualResult));
    }
}

如上, 具體的闡釋在程式碼註釋中都已經標註, 抽取基類是為了提高程式碼可重用性。博主這裡是為了演示, 所以程式碼看起來會有點臃腫, 在實際專案使用中, 可以通過靜態引入 import static org.mockito.Mockito.when; 和 import static org.mockito.Matchers.*; 來簡化程式碼, 提高可閱讀性。

另外由於被測試類在測試方法中被mock掉, 且被@PrepareForTest註解標記時, JaCoCo工具統計測試覆蓋率將忽略該測試類。可通過在基類BaseTest中新增 @Rule public ExpectedException thrown = ExpectedException.none(); , 並去掉 @RunWith(PowerMockRunner.class) 和 @SuppressStaticInitializationFor 來使統計測試覆蓋率生效。但這裡又有一個新問題產生, 基類修改之後, 發現測試用例無法進入debug除錯, 因此建議先用修改前的基類來編寫單元測試, 便於除錯, 待測試用例完成後, 再修改基類令Jacoco統計覆蓋率生效。

關於PowerMockito的實踐, 博主目前在專案中的使用主要就涉及到了這些, 後面如有接觸新的相關知識點, 會陸續更新到本篇文章中。如有錯誤, 歡迎指正, 謝謝^_^