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的實踐, 博主目前在項目中的使用主要就涉及到了這些, 所以這次做了回標題黨"XXX深入實踐" ^_^, 后面如有接觸新的相關知識點, 會陸續更新到本篇文章中。如有錯誤, 歡迎指正, 謝謝你^_^

參考資料

非web下的PowerMockito單元測試


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM