基于Mock測(cè)試Spring MVC接口過(guò)程解析
1. 前言
在Java開發(fā)中接觸的開發(fā)者大多數(shù)不太注重對(duì)接口的測(cè)試,結(jié)果在聯(lián)調(diào)對(duì)接中出現(xiàn)各種問(wèn)題。也有的使用Postman等工具進(jìn)行測(cè)試,雖然在使用上沒有什么問(wèn)題,如果接口增加了權(quán)限測(cè)試起來(lái)就比較惡心了。所以建議在單元測(cè)試中測(cè)試接口,保證在交付前先自測(cè)接口的健壯性。今天就來(lái)分享一下胖哥在開發(fā)中是如何對(duì)Spring MVC接口進(jìn)行測(cè)試的。
在開始前請(qǐng)務(wù)必確認(rèn)添加了Spring Boot Test相關(guān)的組件,在最新的版本中應(yīng)該包含以下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
本文是在Spring Boot 2.3.4.RELEASE下進(jìn)行的。
2. 單獨(dú)測(cè)試控制層
如果我們只需要對(duì)控制層接口(Controller)進(jìn)行測(cè)試,且該接口不依賴@Service、@Component等注解聲明的Spring Bean時(shí),可以借助@WebMvcTest來(lái)啟用只針對(duì)Web控制層的測(cè)試,例如
@WebMvcTest
class CustomSpringInjectApplicationTests {
@Autowired
MockMvc mockMvc;
@SneakyThrows
@Test
void contextLoads() {
mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
.andExpect(ResultMatcher.matchAll(status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.test", Is.is("hello"))))
.andDo(MockMvcResultHandlers.print());
}
}
這種方式要快的多,它只加載了應(yīng)用程序的一小部分。但是如果你涉及到服務(wù)層這種方式是不湊效的,我們就需要另一種方式了。
3. 整體測(cè)試
大多數(shù)Spring Boot下的接口測(cè)試是整體而又全面的測(cè)試,涉及到控制層、服務(wù)層、持久層等方方面面,所以需要加載比較完整的Spring Boot上下文。這時(shí)我們可以這樣做,聲明一個(gè)抽象的測(cè)試基類:
package cn.felord.custom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
/**
* 測(cè)試基類,
* @author felord.cn
*/
@SpringBootTest
@AutoConfigureMockMvc
abstract class CustomSpringInjectApplicationTests {
/**
* The Mock mvc.
*/
@Autowired
MockMvc mockMvc;
// 其它公共依賴和處理方法
}
只有當(dāng)@AutoConfigureMockMvc存在時(shí)MockMvc才會(huì)被注入Spring IoC。
然后針對(duì)具體的控制層進(jìn)行如下測(cè)試代碼的編寫:
package cn.felord.custom;
import lombok.SneakyThrows;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 測(cè)試FooController.
*
* @author felord.cn
*/
public class FooTests extends CustomSpringInjectApplicationTests {
/**
* /foo/map接口測(cè)試.
*/
@SneakyThrows
@Test
void contextLoads() {
mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
.andExpect(ResultMatcher.matchAll(status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.test", Is.is("bar"))))
.andDo(MockMvcResultHandlers.print());
}
}
4. MockMvc測(cè)試
集成測(cè)試時(shí),希望能夠通過(guò)輸入U(xiǎn)RL對(duì)Controller進(jìn)行測(cè)試,如果通過(guò)啟動(dòng)服務(wù)器,建立http client進(jìn)行測(cè)試,這樣會(huì)使得測(cè)試變得很麻煩,比如,啟動(dòng)速度慢,測(cè)試驗(yàn)證不方便,依賴網(wǎng)絡(luò)環(huán)境等,為了可以對(duì)Controller進(jìn)行測(cè)試就引入了MockMvc。
MockMvc實(shí)現(xiàn)了對(duì)Http請(qǐng)求的模擬,能夠直接使用網(wǎng)絡(luò)的形式,轉(zhuǎn)換到Controller的調(diào)用,這樣可以使得測(cè)試速度快、不依賴網(wǎng)絡(luò)環(huán)境,而且提供了一套驗(yàn)證的工具,這樣可以使得請(qǐng)求的驗(yàn)證統(tǒng)一而且很方便。接下來(lái)我們來(lái)一步步構(gòu)造一個(gè)測(cè)試的模擬請(qǐng)求,假設(shè)我們存在一個(gè)下面這樣的接口:
@RestController
@RequestMapping("/foo")
public class FooController {
@Autowired
private MyBean myBean;
@GetMapping("/user")
public Map<String, String> bar(@RequestHeader("Api-Version") String apiVersion, User user) {
Map<String, String> map = new HashMap<>();
map.put("test", myBean.bar());
map.put("version", apiVersion);
map.put("username", user.getName());
//todo your business
return map;
}
}
參數(shù)設(shè)定為name=felord.cn&age=18,那么對(duì)應(yīng)的HTTP報(bào)文是這樣的:
GET /foo/user?name=felord.cn&age=18 HTTP/1.1
Host: localhost:8888
Api-Version: v1
可以預(yù)見的返回值為:
{
"test": "bar",
"version": "v1",
"username": "felord.cn"
}
事實(shí)上對(duì)接口的測(cè)試可以分為以下幾步。
構(gòu)建請(qǐng)求
構(gòu)建請(qǐng)求由MockMvcRequestBuilders負(fù)責(zé),他提供了請(qǐng)求方法(Method),請(qǐng)求頭(Header),請(qǐng)求體(Body),參數(shù)(Parameters),會(huì)話(Session)等所有請(qǐng)求的屬性構(gòu)建。/foo/user接口的請(qǐng)求可以轉(zhuǎn)換為:
MockMvcRequestBuilders.get("/foo/user")
.param("name", "felord.cn")
.param("age", "18")
.header("Api-Version", "v1")
執(zhí)行Mock請(qǐng)求
然后由MockMvc執(zhí)行Mock請(qǐng)求:
mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
.param("name", "felord.cn")
.param("age", "18")
.header("Api-Version", "v1"))
對(duì)結(jié)果進(jìn)行處理
請(qǐng)求結(jié)果被封裝到ResultActions對(duì)象中,它封裝了多種讓我們對(duì)Mock請(qǐng)求結(jié)果進(jìn)行處理的方法。
對(duì)結(jié)果進(jìn)行預(yù)期期望
ResultActions#andExpect(ResultMatcher matcher)方法負(fù)責(zé)對(duì)響應(yīng)的結(jié)果的進(jìn)行預(yù)期期望,看看是否符合測(cè)試的期望值。參數(shù)ResultMatcher負(fù)責(zé)從響應(yīng)對(duì)象中提取我們需要期望的部位進(jìn)行預(yù)期比對(duì)。
假如我們期望接口/foo/user返回的是JSON,并且HTTP狀態(tài)為200,同時(shí)響應(yīng)體包含了version=v1的值,我們應(yīng)該這么聲明:
ResultMatcher.matchAll(MockMvcResultMatchers.status().isOk(),
MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON),
MockMvcResultMatchers.jsonPath("$.version", Is.is("v1")));
JsonPath是一個(gè)強(qiáng)大的JSON解析類庫(kù),請(qǐng)通過(guò)其項(xiàng)目倉(cāng)庫(kù)https://github.com/json-path/JsonPath了解。
對(duì)響應(yīng)進(jìn)行處理
ResultActions#andDo(ResultHandler handler)方法負(fù)責(zé)對(duì)整個(gè)請(qǐng)求/響應(yīng)進(jìn)行打印或者log輸出、流輸出,由MockMvcResultHandlers工具類提供這些方法。我們可以通過(guò)以上三種途徑來(lái)查看請(qǐng)求響應(yīng)的細(xì)節(jié)。
例如/foo/user接口:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /foo/user
Parameters = {name=[felord.cn], age=[18]}
Headers = [Api-Version:"v1"]
Body = null
Session Attrs = {}
Handler:
Type = cn.felord.xbean.config.FooController
Method = cn.felord.xbean.config.FooController#urlEncode(String, Params)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"test":"bar","version":"v1","username":"felord.cn"}
Forwarded URL = null
Redirected URL = null
Cookies = []
獲取返回結(jié)果
如果你希望進(jìn)一步處理響應(yīng)的結(jié)果,也可以通過(guò)ResultActions#andReturn()拿到MvcResult類型的結(jié)果進(jìn)行進(jìn)一步的處理。
完整的測(cè)試過(guò)程
通常andExpect是我們必然會(huì)選擇的,而andDo和andReturn在某些場(chǎng)景下會(huì)有用,它們兩個(gè)是可選的。我們把上面的連在一起。
@Autowired
MockMvc mockMvc;
@SneakyThrows
@Test
void contextLoads() {
mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
.param("name", "felord.cn")
.param("age", "18")
.header("Api-Version", "v1"))
.andExpect(ResultMatcher.matchAll(status().isOk(),
content().contentType(MediaType.APPLICATION_JSON),
jsonPath("$.version", Is.is("v1"))))
.andDo(MockMvcResultHandlers.print());
}
這種流式的接口單元測(cè)試從語(yǔ)義上看也是比較好理解的,你可以使用各種斷言、正例、反例測(cè)試你的接口,最終讓你的接口更加健壯。
5. 總結(jié)
一旦你熟練了這種方式,你編寫的接口將更加具有權(quán)威性而不會(huì)再漏洞百出,甚至有時(shí)候你也可以使用Mock來(lái)設(shè)計(jì)接口,使之更加貼合業(yè)務(wù)。所以CRUD不是完全沒有技術(shù)含量,高質(zhì)量高效率的CRUD往往需要這種工程化的單元測(cè)試來(lái)支撐。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 基于postman實(shí)現(xiàn)http接口測(cè)試過(guò)程解析
- Python+unittest+requests 接口自動(dòng)化測(cè)試框架搭建教程
- 基于Fiddler實(shí)現(xiàn)修改接口返回?cái)?shù)據(jù)進(jìn)行測(cè)試
- Jmeter對(duì)接口測(cè)試入?yún)?shí)現(xiàn)MD5加密
- Python接口測(cè)試文件上傳實(shí)例解析
- Python接口測(cè)試數(shù)據(jù)庫(kù)封裝實(shí)現(xiàn)原理
- Python3+Requests+Excel完整接口自動(dòng)化測(cè)試框架的實(shí)現(xiàn)
- python接口調(diào)用已訓(xùn)練好的caffe模型測(cè)試分類方法
- Xmeter API接口測(cè)試工具使用方法解析
相關(guān)文章
springboot?vue測(cè)試列表遞歸查詢子節(jié)點(diǎn)下的接口功能實(shí)現(xiàn)
這篇文章主要為大家介紹了springboot?vue測(cè)試列表遞歸查詢子節(jié)點(diǎn)下的接口功能實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
mybatis打印SQL,并顯示參數(shù)的實(shí)例
這篇文章主要介紹了mybatis打印SQL,并顯示參數(shù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
Java調(diào)用Oss JDk實(shí)現(xiàn)刪除指定目錄下的所有文件
這篇文章主要為大家詳細(xì)介紹了Java如何調(diào)用Oss JDk實(shí)現(xiàn)刪除指定目錄下的所有文件功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2025-03-03
Java中可以實(shí)現(xiàn)負(fù)載均衡的算法詳解
這篇文章主要介紹了Java中可以實(shí)現(xiàn)負(fù)載均衡的算法詳解,在Java中,有多種算法可以實(shí)現(xiàn)負(fù)載均衡,下面是兩個(gè)常見的算法示例,隨機(jī)算法和輪詢算法,需要的朋友可以參考下2023-08-08
詳談jvm--Java中init和clinit的區(qū)別
下面小編就為大家?guī)?lái)一篇詳談jvm--Java中init和clinit的區(qū)別。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10
SpringBoot@Componet注解注入失敗的問(wèn)題
這篇文章主要介紹了SpringBoot@Componet注解注入失敗的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
Java多線程通信wait()和notify()代碼實(shí)例
這篇文章主要介紹了Java多線程通信wait()和notify()代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
淺談Java中的atomic包實(shí)現(xiàn)原理及應(yīng)用
這篇文章主要介紹了淺談Java中的atomic包實(shí)現(xiàn)原理及應(yīng)用,涉及Atomic在硬件上的支持,Atomic包簡(jiǎn)介及源碼分析等相關(guān)內(nèi)容,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12
java實(shí)現(xiàn)文件上傳的詳細(xì)步驟
文件上傳是用戶將本地文件通過(guò)Web頁(yè)面提交到服務(wù)器的過(guò)程,涉及客戶端、服務(wù)器端、上傳表單等組件,在SpringBoot中,通過(guò)MultipartFile接口處理上傳文件,并將其保存在服務(wù)器,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-10-10
自主配置數(shù)據(jù)源,mybatis/plus不打印sql日志問(wèn)題
這篇文章主要介紹了自主配置數(shù)據(jù)源,mybatis/plus不打印sql日志問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12

