1、什麼是mock測試?
Mock 測試就是在測試過程中,對於某些不容易構造或者不容易獲取的比較複雜的對象,用一個虛擬的對象(Mock 對象)來創建以便測試的測試方法。
2、為什麼要進行Mock測試?
Mock是為了解決不同的單元之間由於耦合而難於開發、測試的問題。所以,Mock既能出現在單元測試中,也會出現在集成測試、系統測試過程中。
Mock 最大的功能是幫你 把單元測試的耦合分解開, 如果你的代碼對另一個類或者接口有依賴,它能夠幫你模擬這些依賴,並幫你驗證所調用的依賴的行為。
如圖,例如有這樣一段程序,A接口要依賴後面的B、C,當需要測試A的時候,我們需要把整個依賴關係構造出來,比較複雜;
而使用mock的話,可以把結構進行分解,如圖所示
3、Mock對象適用場景
•需要將被測模塊和其他依賴模塊獨立開,構造一個獨立的測試環境,不關注被測單元的依賴對象,只關注被測單元的功能邏輯
•被測單元依賴的模塊尚未開發完成,而 被測單元需要依賴模塊的返回值進行後續處理
•被測單元依賴的對象較難模擬或者構造比較複雜。
4、Mock測試的優勢
•團隊可以並行工作
有了Mock,前後端人員只需要定義好接口文檔就可以開始並行工作,互不影響,只在最後的聯調階段往來密切;後端與後端之間如果有接口耦合,也同樣能被Mock解決;測試過程中如果遇到依賴接口沒有準備好,同樣可以藉助Mock;
不會出現一個團隊等待另一個團隊的情況。這樣的話,開發自測階段就可以及早開展,從而發現缺陷的時機也提前了,有利於整個產品質量以及進度的保證
•可以模擬那些無法訪問的資源
比如牆
•隔離系統
假如我們需要調用一個post請求,為了獲得某個響應,來看當前系統是否能正確處理返回的「響應」,但是這個post請求會造成資料庫中數據的污染,那麼就可以充分利用Mock,構造一個虛擬的post請求,我們給他指定返回就好了。
•測試覆蓋度
假如有一個接口,有100個不同類型的返回,我們需要測試它在不同返回下,系統是否能夠正常響應,但是有些返回在正常情況下基本不會發生
5、Mock測試存在的問題
•測試人員不應該被覆蓋率高的測試所迷惑,覆蓋率高不代表沒有問題。需要去判斷這些地方使用Mock測試是否合理,這些Mock測試是否應該換成真實模塊間的調用和集成。
•當把mock接口換成實際接口後,測試/開發也必須把之前的測試重新做一遍,
建議:mock接口只能與主流程聯調/ 異常返回測試,不要過分依賴mock接口進行測試。
•測試完畢,上線前請一定確保 為了mock而做的相關代碼/配置文件的修改,已經完全恢復了
6、Mock測試方式
一般都是藉助工具來進行mock,常見的比如fiddler。選擇工具時,可以考慮一下幾點
•一是數據要好管理,別讓我管理一堆文件
•二是mock接口最好可以設置成和真實接口完全一致,這樣就只需要切換hosts就可以切換mock接口和真實接口,不需要修改代碼
•三是跨平台,mock接口在win
七、使用mock時,切記的幾點:
1. 測試人員不應該被覆蓋率高的自動化測試所迷惑,覆蓋率高不代表沒有問題。
2. 當把mock接口換成實際接口後,測試/開發也必須把之前的測試重新做一遍。
建議: mock接口只能主流程聯調返回測試,不要過分依賴mock接口進行測試。
3. 測試完畢,上線前,請一定確保為Mock而做的相關代碼/配置文件的修改,已經完全恢復了
建議:上線checklist中條條列出,並上線前review
八、使用Mock做單元測試
Mockito基本使用方法簡介
1)、靜態導入會使代碼更簡潔
import static org.mockito.Mockito.*;
舉例:
//創建mock對象,mock一個List接口
List mockedList = mock(List.class);
//如果不使用靜態導入,則必須使用Mockito調用
List mockList = Mockito.mock(List.class);
2)、驗證某些行為
//你可以mock一個具體的類型,而不僅是接口
LinkedList mockedList = mock(LinkedList.class);
mockedList.add("one");
//驗證
verify(mockedList).add("one");
一旦mock對象被創建了,mock對象會記住所有的交互。然後你就可能選擇性地驗證你感興趣的交互。
3)、如何做一些測試樁
//測試樁
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
當調用mockList.get(0)的時候,返回first
當調用mockList.get(1)的時候,拋出一個運行時異常
4)、其他使用見上面文檔
2、MockMVC基於RESTful風格的測試
對於前後端分離的項目而言,無法直接從前端靜態代碼中測試接口的正確性,因此可以通過MockMVC來模擬HTTP請求。基於RESTful風格的SpringMVC的測試,我們可以測試完整的Spring MVC流程,即從URL請求到控制器處理,再到視圖渲染都可以測試。
2.1、初始化MockMvc對象
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
//在每個測試方法執行之前都初始化MockMvc對象
@BeforeEach
public void setupMockMvc() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
2.2、完成一些接口的測試
1)、嘗試測試一個不存在的請求 /user/1
/**
* @DisplayName 自定義測試方法展示的名稱
* @throws Exception
*/
@DisplayName("測試根據Id獲取User")
@Test
void contextLoads() throws Exception {
//perform,執行一個requestBuilders請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理
mockMvc.perform(MockMvcRequestBuilders
//構造一個get請求
.get("/user/1")
//請求類型 json
.contentType(MediaType.APPLICATION_JSON))
// 期待返回的狀態碼是4XX,因為我們並沒有寫/user/{id}的get接口
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
}
展示結果:
2)、在Controller中完成 /user/{id}
/**
* id:\\d+只匹配數字
* @param id
* @return
*/
@GetMapping("/user/{id:\\d+}")
public User getUserById(@PathVariable Long id) {
return userService.getById(id);
}
修改一下測試類:期待返回的結果是200
@Test
void getUserById() throws Exception {
//perform,執行一個RequestBuilders請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理
mockMvc.perform(MockMvcRequestBuilders
//構造一個get請求
.get("/user/1")
//請求類型 json
.contentType(MediaType.APPLICATION_JSON))
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk());
}
結果展示:
3)、我們可以把結果列印到控制台
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk())
//添加ResultHandler結果處理器,比如調試時 列印結果(print方法)到控制台
.andDo(MockMvcResultHandlers.print());
運行結果:可以看到並沒有返回結果
4)、結合Mockito構建自定義返回結果
這裡就用到了Mockito的應用場景,userService.getById並沒有返回結果,但是我們的測試並不關心userService.getById這個方法是否正常,只是在我們的測試中需要用到這個方法,所以我們可以Mock掉UserService的getById方法,自己定義返回的結果,繼續我們的測試。
@MockBean
private UserService userService;
@Test
void getUserById() throws Exception {
User user = new User();
user.setId(1);
user.setNickname("yunqing");
//Mock一個結果,當userService調用getById的時候,返回user
doReturn(user).when(userService).getById(any());
//perform,執行一個RequestBuilders請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理
mockMvc.perform(MockMvcRequestBuilders
//構造一個get請求
.get("/user/1")
//請求類型 json
.contentType(MediaType.APPLICATION_JSON))
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk())
//添加ResultHandler結果處理器,比如調試時 列印結果(print方法)到控制台
.andDo(MockMvcResultHandlers.print());
}
運行結果
5)、傳參數
@Test
void getUserByUsername() throws Exception {
// perform : 執行請求 ;
mockMvc.perform(MockMvcRequestBuilders
//MockMvcRequestBuilders.get("/url") :構造一個get請求
.get("/user/getUserByName")
//傳參
.param("username","admin")
// 請求type : json
.contentType(MediaType.APPLICATION_JSON))
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk());
}
6)、期望返回結果集有兩個元素
@Test
void getAll() throws Exception {
User user = new User();
user.setNickname("yunqing");
List<User> list = new LinkedList<>();
list.add(user);
list.add(user);
//Mock一個結果,當userService調用list的時候,返回user
when(userService.list()).thenReturn(list);
//perform,執行一個RequestBuilders請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理
mockMvc.perform(MockMvcRequestBuilders
//構造一個get請求
.get("/user/list")
//請求類型 json
.contentType(MediaType.APPLICATION_JSON))
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk())
//期望返回的結果集合有兩個元素
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2))
//添加ResultHandler結果處理器,比如調試時 列印結果(print方法)到控制台
.andDo(MockMvcResultHandlers.print());
}
運行結果:
7)、測試Post請求
@Test
void insert() throws Exception {
User user = new User();
user.setNickname("yunqing");
String jsonResult = JSONObject.toJSONString(user);
//直接自定義save返回true
when(userService.save(any())).thenReturn(true);
// perform : 執行請求 ;
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
//MockMvcRequestBuilders.post("/url") :構造一個post請求
.post("/user/insert")
.accept(MediaType.APPLICATION_JSON)
//傳參,因為後端是@RequestBody所以這裡直接傳json字符串
.content(jsonResult)
// 請求type : json
.contentType(MediaType.APPLICATION_JSON))
// 期望的結果狀態 200
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();//返回結果
int statusCode = mvcResult.getResponse().getStatus();
String result = mvcResult.getResponse().getContentAsString();
//單個斷言
Assertions.assertEquals(200, statusCode);
//多個斷言,即使出錯也會檢查所有斷言
assertAll("斷言",
() -> assertEquals(200, statusCode),
() -> assertTrue("true".equals(result))
);
3、一些常用API總結
常用的期望:
//使用jsonPaht驗證返回的json中code、message欄位的返回值
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("00000"))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("成功"))
//body屬性不為空
.andExpect(MockMvcResultMatchers.jsonPath("$.body").isNotEmpty())
// 期望的返回結果集合有2個元素 , $: 返回結果
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2));
附帶常用API解釋:
RequestBuilder/MockMvcRequestBuilders:
//根據uri模板和uri變量值得到一個GET請求方式的MockHttpServletRequestBuilder;
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)
//同get類似,但是是POST方法;
MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)
//同get類似,但是是PUT方法;
MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)
//同get類似,但是是DELETE方法;
MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables)
//同get類似,但是是OPTIONS方法;
MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)
//提供自己的Http請求方法及uri模板和uri變量,如上API都是委託給這個API;
MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)
//提供文件上傳方式的請求,得到MockMultipartHttpServletRequestBuilder;
MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables)
//創建一個從啟動異步處理的請求的MvcResult進行異步分派的RequestBuilder;
RequestBuilder asyncDispatch(final MvcResult mvcResult)
MockHttpServletRequestBuilder:
//:添加頭信息;
MockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)
//:指定請求的contentType頭信息;
MockHttpServletRequestBuilder contentType(MediaType mediaType)
//:指定請求的Accept頭信息;
MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes)
//:指定請求Body體內容;
MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content)
//:請求傳入參數
MockHttpServletRequestBuilder param(String name,String... values)
//:指定請求的Cookie;
MockHttpServletRequestBuilder cookie(Cookie... cookies)
//:指定請求的Locale;
MockHttpServletRequestBuilder locale(Locale locale)
//:指定請求字符編碼;
MockHttpServletRequestBuilder characterEncoding(String encoding)
//:設置請求屬性數據;
MockHttpServletRequestBuilder requestAttr(String name, Object value)
//:設置請求session屬性數據;
MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes)
//指定請求的flash信息,比如重定向後的屬性信息;
MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes)
//:指定請求的Session;
MockHttpServletRequestBuilder session(MockHttpSession session)
// :指定請求的Principal;
MockHttpServletRequestBuilder principal(Principal principal)
//:指定請求的上下文路徑,必須以「/」開頭,且不能以「/」結尾;
MockHttpServletRequestBuilder contextPath(String contextPath)
//:請求的路徑信息,必須以「/」開頭;
MockHttpServletRequestBuilder pathInfo(String pathInfo)
//:請求是否使用安全通道;
MockHttpServletRequestBuilder secure(boolean secure)
//:請求的後處理器,用於自定義一些請求處理的擴展點;
MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor)
MockMultipartHttpServletRequestBuilder
//:指定要上傳的文件;
MockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file)
ResultActions
//:添加驗證斷言來判斷執行請求後的結果是否是預期的;
ResultActions andExpect(ResultMatcher matcher)
//:添加結果處理器,用於對驗證成功後執行的動作,如輸出下請求/結果信息用於調試;
ResultActions andDo(ResultHandler handler)
//:返回驗證成功後的MvcResult;用於自定義驗證/下一步的異步處理;
MvcResult andReturn()
ResultMatcher/MockMvcResultMatchers
//:請求的Handler驗證器,比如驗證處理器類型/方法名;此處的Handler其實就是處理請求的控制器;
HandlerResultMatchers handler()
//:得到RequestResultMatchers驗證器;
RequestResultMatchers request()
//:得到模型驗證器;
ModelResultMatchers model()
//:得到視圖驗證器;
ViewResultMatchers view()
//:得到Flash屬性驗證;
FlashAttributeResultMatchers flash()
//:得到響應狀態驗證器;
StatusResultMatchers status()
//:得到響應Header驗證器;
HeaderResultMatchers header()
//:得到響應Cookie驗證器;
CookieResultMatchers cookie()
//:得到響應內容驗證器;
ContentResultMatchers content()
//:得到Json表達式驗證器;
JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath(String expression, Matcher matcher)
//:得到Xpath表達式驗證器;
XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args)
//:驗證處理完請求後轉發的url(絕對匹配);
ResultMatcher forwardedUrl(final String expectedUrl)
//:驗證處理完請求後轉發的url(Ant風格模式匹配,@since spring4);
ResultMatcher forwardedUrlPattern(final String urlPattern)
//:驗證處理完請求後重定向的url(絕對匹配);
ResultMatcher redirectedUrl(final String expectedUrl)
//:驗證處理完請求後重定向的url(Ant風格模式匹配,@since spring4);
ResultMatcher redirectedUrlPattern(final String expectedUrl)
希望本文對你有所幫助~~如果對軟體測試、接口測試、自動化測試、面試經驗交流感興趣可以私聊我或關注公眾號「特斯汀軟體測試」。免費領取最新軟體測試大廠面試資料和Python自動化、接口、框架搭建學習資料!技術大牛解惑答疑,同行一起交流。