因爲最近工做須要, 在項目中要作單元測試, 以達到指定的測試用例覆蓋率指標。項目中咱們引入的powermockito來編寫測試用例, JaCoCo來監控單元測試覆蓋率。關於框架的選擇, 網上討論mockito和powermockito孰優孰劣的文章衆多, 這裏就很少作闡述, 讀者若有興趣可自行了解。html
<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.*; 來簡化代碼, 提升可閱讀性。web
另外因爲被測試類在測試方法中被mock掉, 且被@PrepareForTest註解標記時, JaCoCo工具統計測試覆蓋率將忽略該測試類。可經過在基類BaseTest中添加 @Rule public ExpectedException thrown = ExpectedException.none(); , 並去掉 @RunWith(PowerMockRunner.class) 和 @SuppressStaticInitializationFor 來使統計測試覆蓋率生效。但這裏又有一個新問題產生, 基類修改以後, 發現測試用例沒法進入debug調試, 所以建議先用修改前的基類來編寫單元測試, 便於調試, 待測試用例完成後, 再修改基類令Jacoco統計覆蓋率生效。redis
關於PowerMockito的實踐, 博主目前在項目中的使用主要就涉及到了這些, 因此此次作了回標題黨"XXX深刻實踐" ^_^, 後面若有接觸新的相關知識點, 會陸續更新到本篇文章中。若有錯誤, 歡迎指正, 謝謝你^_^數據庫