首先,我先引用大神的一些語言解釋一下mock對單元測試的作用。
參考博客:無所不能的PowerMock,mock私有方法,靜態方法,測試私有方法,final類
1、為什么要用mock
我的一本書的解釋:
(1)創建所需的DB數據可能需要很長時間,如:調用別的接口,模擬很多數據
(2)調用第三方API接口,測試很慢,
(3)編寫滿足所有外部依賴的測試可能很復雜,復雜到不值得編寫,Mock模擬內部或外部依賴可以幫助我們解決這些問題
另一本TDD書的解釋:
(1)對象的結果不確定,如每獲取當前時間,得到的結果都不一樣,無法符合我們的預期;
(2)實現這個接口的對象不存在;
(3)對象速度緩慢
對於TDD還有一個更重要原因:通過模擬可以隔離當前方法使用的的所有依賴,讓我們更加專注於單個單元,忽略其調用的代碼的內部工作原理
一本博客的干貨:
(1)Mock可以用來解除測試對象對外部服務的依賴(比如數據庫,第三方接口等),使得測試用例可以獨立運行。不管是傳統的單體應用,還是現在流行的微服務,這點都特別重要,因為任何外部依賴的存在都會極大的限制測試用例的可遷移性和穩定性。
(2)Mock的第二個好處是替換外部服務調用,提升測試用例的運行速度。任何外部服務調用至少是跨進程級別的消耗,甚至是跨系統、跨網絡的消耗,而Mock可以把消耗降低到進程內。比如原來一次秒級的網絡請求,通過Mock可以降至毫秒級,整整3個數量級的差別。
(3)Mock的第三個好處是提升測試效率。這里說的測試效率有兩層含義。第一層含義是單位時間運行的測試用例數,這是運行速度提升帶來的直接好處。而第二層含義是一個測試人員單位時間創建的測試用例數。
以單體應用為例,隨着業務復雜度的上升,為了運行一個測試用例可能需要准備很多測試數據,與此同時還要盡量保證多個測試用例之間的測試數據互不干擾。為了做到這一點,測試人員往往需要花費大量的時間來維護一套可運行的測試數據。有了Mock之后,由於去除了測試用例之間共享的數據庫依賴,測試人員就可以針對每一個或者每一組測試用例設計一套獨立的測試數據,從而很容易的做到不同測試用例之間的數據隔離性。而對於微服務,由於一個微服務可能級聯依賴很多其他的微服務,運行一個測試用例甚至需要跨系統准備一套測試數據,如果沒有Mock,基本上可以說是不可能的。因此,不管是單體應用還是微服務,有了Mock之后,QE就可以省去大量的准備測試數據的時間,專注於測試用例本身,自然也就提升了單人的測試效率。
現如今比較流行的Mock工具如jMock 、EasyMock 、Mockito等都有一個共同的缺點:不能mock靜態、final、私有方法等。而PowerMock能夠完美的彌補以上三個Mock工具的不足
2、實戰:
在實戰過程中,mock的版本是我花了一天時間才解決的坑,我使用的是阿里雲的maven,有些博客的powermock的依賴依賴不進來,經過多次嘗試,才解決這個問題。下面是代碼。
參考博客:PowerMock+Junit4 Maven配置
<!--mock測試-->
<properties>
<java.version>1.8</java.version>
<powermock-version>2.0.5</powermock-version>
<mockito-version>2.23.4</mockito-version>
</properties>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>${powermock-version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4-rule</artifactId>
<version>${powermock-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
測試類包含了普通的方法調用,靜態方法、私有方法等。
package com.testmock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserController.class)
public class LastMock {
@Mock
private UserServiceImpl serviceImpl;
@InjectMocks
private UserController controller;
/**
* mock service的保存方法
*/
@Test
public void mockSave() {
User user1 = new User();
User user2 = new User();
user1.setId("1");
user2.setId("2");
Mockito.when(serviceImpl.save(user1)).thenReturn(user2); //當調用service的save()時,mock讓他返回user2
User saveUser = controller.saveUser(user1); //調用
Mockito.verify(serviceImpl,Mockito.times(1)).save(user1);//verify驗證mock次數
assertEquals(user2, saveUser);//斷言是否mock返回的是user2
}
/**
* mock spy public方法
* @throws Exception xx
*/
@Test
public void spy_public_method() throws Exception {
UserController spy = PowerMockito.spy(controller); //監視controller的publicCheck方法,讓他返回true
Mockito.when(spy.publicCheck()).thenReturn(true);
String name = spy.getPrivateName("ljw");//執行該方法
assertEquals("public 被mock 了", name);//驗證
}
/**
* mock私有方法
* @throws Exception xx
*/
@Test
public void spy_private_method() throws Exception {
UserController spy = PowerMockito.spy(controller);
PowerMockito.when(spy, "check", any()).thenReturn(true);//私有方法mockito不行了,需要用無所不能的PowerMock監視spy
String name = spy.getPrivateName("ljw");
assertEquals("private 被mock 了", name);
}
/**
* mock 靜態方法
*/
@Test
public void mockStaticMethod() {
PowerMockito.mockStatic(UserController.class);//mock靜態方法
when(UserController.getStaticName(any())).thenReturn("hi");
String staticName = UserController.getStaticName("ljw");//執行
assertEquals("hi", staticName);//驗證
}
@Test
public void mockStaticMethod_2() {
PowerMockito.mockStatic(UserController.class);
when(UserController.getStaticName(any())).thenReturn("hi");
String staticName = controller.returnName();//通過returnName()調用,看能否被mock
assertEquals("hi", staticName);
}
/**
*靜態方法傳入一個HttpServerletRequest參數 + 普通方法
*sA(T.class)檢查參數T的實例instance,表示它為非null。
*same(obj)檢查參數是否與obj相同,從而arg == obj為true。
*eq(obj)根據其equals方法檢查參數是否等於obj。如果您在不使用匹配器的情況下傳遞實數值,這也是行為。
*/
@Test
public void mockStaticMethod_3() {
PowerMockito.mockStatic(UserController.class);
PowerMockito.when(UserController.httpGetStaticName(any(),any())).thenReturn("hi");
String staticName = controller.httpReturnName(eq(any())); //通過returnName()調用,看能否被mock
assertEquals("hi aaa", staticName);
}
/**
* 測試私有方法一
* @throws InvocationTargetException xx
* @throws IllegalAccessException xx
*/
@Test
public void testPrivateMethod() throws InvocationTargetException, IllegalAccessException {
Method method = PowerMockito.method(UserController.class, "say", String.class);
Object say = method.invoke(controller, "hi");
assertEquals("ljw say hi", say);
}
/**
* 測試私有方法二
* @throws Exception xx
*/
@Test
public void testPrivateMethod_2() throws Exception {
Object say = Whitebox.invokeMethod(controller, "say", "hi");
assertEquals("ljw say hi", say);
}
}
在上面的mockStaticMethod_3方法中,使用到了eq(),它的用法在注釋里說明了,這里使用匹配器,我的理解是因為需要和這個方法中的第一個any()匹配。如果用實例參數,結果也是一樣的。那如果你的是普通方法,就直接套用普通方法的測試用例。
UserController
package com.testmock;
import javax.servlet.http.HttpServletRequest;
public final class UserController {
private final UserServiceImpl service;
public UserController(UserServiceImpl service) {
this.service = service;
}
public User saveUser(User user) {
User save = service.save(user);
return save;
}
public String returnName(){
return getStaticName("ljw1") + " aaa";
}
public String httpReturnName(HttpServletRequest request){
return httpGetStaticName("ljw1", request) + " aaa";
}
public static String getStaticName(String name) {
return "A_" + name;
}
public static String httpGetStaticName(String name, HttpServletRequest request) {
String aaa = request.getParameter("aaa");
return aaa + "A_" + name;
}
public String getPrivateName(String name) {
if (publicCheck()){
return "public 被mock 了";
}
if (check(name)){
return "private 被mock 了";
}
return "A_" + name;
}
public boolean publicCheck() {
return false;
}
private boolean check(String name) {
return false;
}
private String say(String content) {
return "ljw say " + content;
}
}
UserServiceImpl
public class UserServiceImpl {
public User save(User user) {
return user;
}
}
User
public class User {
private String id;
//省略set/get
}
使用PowerMock靜態方法獲取Context緩存中的值
有時候,我們會從緩存中去取值,比如登錄后的用戶,這時候,單元測試沒有去啟動web應用,沒有Context的環境。這時候就讓我頭疼了,不知道你有沒有。其實,緩存中取值也是一個靜態方法。
import org.springframework.web.context.ContextLoader;
import javax.servlet.ServletContext;
public class CacheUtil {
public static String getRequestVal(String key){
ServletContext application = ContextLoader.getCurrentWebApplicationContext().getServletContext();
return (String)application.getAttribute(key);
}
}
這就很簡單了吧,直接套用上面靜態方法的測試就可以了。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.junit.Assert.assertEquals;
@RunWith(PowerMockRunner.class)
@PrepareForTest(CacheUtil.class)
public class CacheUtilTest {
@Test
public void getRequestVal(){
PowerMockito.mockStatic(CacheUtil.class);
PowerMockito.when(CacheUtil.getRequestVal("username")).thenReturn("admin");
String username = CacheUtil.getRequestVal("username");
//可以使用實例值,也可以使用匹配器
//PowerMockito.when(CacheUtil.getRequestVal(anyString())).thenReturn("admin");
//String username = CacheUtil.getRequestVal(anyString());
assertEquals("admin", username);
}
}
普通方法
需要測試的方法
public String getVal(String key){
ServletContext application = ContextLoader.getCurrentWebApplicationContext().getServletContext();
return (String)application.getAttribute(key);
}
測試
@RunWith(PowerMockRunner.class)
@PrepareForTest({CacheUtil.class,ContextLoader.class})
public class CacheUtilTest {
@Mock
private CacheUtil cacheUtil;
@Test
public void getVal(){
Mockito.when(cacheUtil.getVal(any())).thenReturn("admin");
String name = cacheUtil.getVal(any());//執行該方法
assertEquals("admin", name);//驗證
}
}