概述
由於最近工作需要, 在項目中要做單元測試, 以達到指定的測試用例覆蓋率指標。項目中我們引入的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深入實踐" ^_^, 后面如有接觸新的相關知識點, 會陸續更新到本篇文章中。如有錯誤, 歡迎指正, 謝謝你^_^
