MockMVC - 基于RESTful風(fēng)格的SpringMVC的測(cè)試
對(duì)于前后端分離的項(xiàng)目而言,無(wú)法直接從前端靜態(tài)代碼中測(cè)試接口的正確性驯用,因此可以通過(guò)
MockMVC
來(lái)模擬HTTP請(qǐng)求⊥苑基于RESTful風(fēng)格
的SpringMVC
的測(cè)試挖腰,我們可以測(cè)試完整的Spring MVC流程猴仑,即從URL請(qǐng)求到控制器處理宁脊,再到視圖渲染都可以測(cè)試榆苞。
快速入門-這個(gè)方法中包含了大多數(shù)的常見情況:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CourseControllerTest {
@Autowired
private MockMvc mockMvc;
//ObjectMapper是一個(gè)可以重復(fù)使用的對(duì)象
@Autowired
private ObjectMapper mapper;
@Test
public void courseListTest() throws Exception{
mockMvc.perform(MockMvcRequestBuilders.get("/course"))
.andExpect(MockMvcResultMatchers.status().isOk());
}
@Test
public void createTest() throws Exception{
String json = "{\"name\":\"Mock測(cè)試\",\"classHour\":\"4課時(shí)\",\"id\":\"rn:practice:Course:5af27fa5d34f435e581e5bbf\"}";
//將json格式字符串轉(zhuǎn)換成Course對(duì)象里的屬性值
Course course = mapper.readValue(json,Course.class);
//perform,執(zhí)行一個(gè)RequestBuilders請(qǐng)求,會(huì)自動(dòng)執(zhí)行SpringMVC的流程并映射到相應(yīng)的控制器執(zhí)行處理
mockMvc.perform(
//構(gòu)造一個(gè)post請(qǐng)求
MockMvcRequestBuilders.post("/course")
.contentType(MediaType.APPLICATION_JSON_UTF8)
//使用writeValueAsString()方法來(lái)獲取對(duì)象的JSON字符串表示
.content(mapper.writeValueAsString(course)))
//andExpect街夭,添加ResultMathcers驗(yàn)證規(guī)則,驗(yàn)證控制器執(zhí)行完成后結(jié)果是否正確趁尼,【這是一個(gè)斷言】
.andExpect(MockMvcResultMatchers.status().is(200))
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
//假定返回的結(jié)果中砚殿,"name" 值為 "Mock測(cè)試2",如果不是的話似炎,會(huì)拋出異常java.lang.AssertionError羡藐,并給出期望值和實(shí)際值
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Mock測(cè)試2"))
//添加ResultHandler結(jié)果處理器传睹,比如調(diào)試時(shí) 打印結(jié)果(print方法)到控制臺(tái)
.andDo(print())
//返回相應(yīng)的MvcResult
.andReturn();
}
原理:
一 MockMvcBuilder
MockMvcBuilder
是用來(lái)構(gòu)造MockMvc
的構(gòu)造器,其主要有兩個(gè)實(shí)現(xiàn):StandaloneMockMvcBuilder
和DefaultMockMvcBuilder
,分別對(duì)應(yīng)兩種測(cè)試方式店印,即獨(dú)立安裝和集成Web環(huán)境
測(cè)試(此種方式并不會(huì)集成真正的web環(huán)境包券,而是通過(guò)相應(yīng)的Mock API
進(jìn)行模擬測(cè)試溅固,無(wú)須啟動(dòng)服務(wù)器)侍郭。對(duì)于我們來(lái)說(shuō)直接使用靜態(tài)工廠MockMvcBuilders
創(chuàng)建即可。
1.集成Web環(huán)境方式
MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext爆捞,將會(huì)從該上下文獲取相應(yīng)的控制器并得到相應(yīng)的MockMvc煮甥;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:config/IncotermsRestServiceTest-context.xml")
@WebAppConfiguration
public class IncotermsRestServiceTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //構(gòu)造MockMvc
}
...
}
注意:
(1)@WebAppConfiguration:測(cè)試環(huán)境使用,用來(lái)表示測(cè)試環(huán)境使用的ApplicationContext將是WebApplicationContext類型的艇劫;value指定web應(yīng)用的根吼驶;
(2)通過(guò)@Autowired WebApplicationContext wac:注入web環(huán)境的ApplicationContext容器;
(3)然后通過(guò)MockMvcBuilders.webAppContextSetup(wac).build()創(chuàng)建一個(gè)MockMvc進(jìn)行測(cè)試店煞;
2.獨(dú)立測(cè)試方式
MockMvcBuilders.standaloneSetup(Object... controllers):通過(guò)參數(shù)指定一組控制器蟹演,這樣就不需要從上下文獲取了;
public class PricingExportResultsRestServiceTest {
@InjectMocks
private PricingExportResultsRestService pricingExportResultsRestService;
@Mock
private ExportRateScheduleService exportRateScheduleService;
@Mock
private PricingUrlProvider pricingUrlProvider;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(pricingExportResultsRestService).build(); //構(gòu)造MockMvc
}
...
}
主要是兩個(gè)步驟:
(1)首先自己創(chuàng)建相應(yīng)的控制器顷蟀,注入相應(yīng)的依賴
(2)通過(guò)MockMvcBuilders.standaloneSetup模擬一個(gè)Mvc測(cè)試環(huán)境酒请,通過(guò)build得到一個(gè)MockMvc
二 MockMvc
先看一個(gè)測(cè)試?yán)?:
@Test
public void createIncotermSuccess() throws Exception {
IncotermTo createdIncoterm = new IncotermTo();
createdIncoterm.setId(new IncotermId(UUID.fromString("6305ff33-295e-11e5-ae37-54ee7534021a")));
createdIncoterm.setCode("EXW");
createdIncoterm.setDescription("code exw");
createdIncoterm.setLocationQualifier(LocationQualifier.DEPARTURE);
when(inventoryService.create(any(IncotermTo.class))).thenReturn(createdIncoterm); mockMvc.perform(post("/secured/resources/incoterms/create").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON)
.content("{\"code\" : \"EXW\", \"description\" : \"code exw\", \"locationQualifier\" : \"DEPARTURE\"}".getBytes()))
//.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("id.value").exists())
.andExpect(jsonPath("id.value").value("6305ff33-295e-11e5-ae37-54ee7534021a"))
.andExpect(jsonPath("code").value("EXW"));
}
perform:執(zhí)行一個(gè)RequestBuilder請(qǐng)求,會(huì)自動(dòng)執(zhí)行SpringMVC的流程并映射到相應(yīng)的控制器執(zhí)行處理鸣个;
andExpect:添加ResultMatcher驗(yàn)證規(guī)則,驗(yàn)證控制器執(zhí)行完成后結(jié)果是否正確;
andDo:添加ResultHandler結(jié)果處理器,比如調(diào)試時(shí)打印結(jié)果到控制臺(tái);
andReturn:最后返回相應(yīng)的MvcResult;然后進(jìn)行自定義驗(yàn)證/進(jìn)行下一步的異步處理让腹;
看一個(gè)具體的例子2:
@Test
public void testView() throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user/1"))
.andExpect(MockMvcResultMatchers.view().name("user/view"))
.andExpect(MockMvcResultMatchers.model().attributeExists("user"))
.andDo(MockMvcResultHandlers.print())
.andReturn();
Assert.assertNotNull(result.getModelAndView().getModel().get("user"));
}
整個(gè)過(guò)程:
- mockMvc.perform執(zhí)行一個(gè)請(qǐng)求驱犹;
- MockMvcRequestBuilders.get("/user/1")構(gòu)造一個(gè)請(qǐng)求
- ResultActions.andExpect添加執(zhí)行完成后的斷言
- ResultActions.andDo添加一個(gè)結(jié)果處理器俘侠,表示要對(duì)結(jié)果做點(diǎn)什么事情惫东,比如此處使用MockMvcResultHandlers.print()輸出整個(gè)響應(yīng)結(jié)果信息废封。
- ResultActions.andReturn表示執(zhí)行完成后返回相應(yīng)的結(jié)果。
整個(gè)測(cè)試過(guò)程非常有規(guī)律:
- 準(zhǔn)備測(cè)試環(huán)境
- 通過(guò)MockMvc執(zhí)行請(qǐng)求
- 添加驗(yàn)證斷言
3.1 添加結(jié)果處理器
3.2 得到MvcResult進(jìn)行自定義斷言/進(jìn)行下一步的異步請(qǐng)求 - 卸載測(cè)試環(huán)境
三 RequestBuilder/MockMvcRequestBuilders
從名字可以看出拂募,RequestBuilde
r用來(lái)構(gòu)建請(qǐng)求的,其提供了一個(gè)方法buildRequest(ServletContext servletContext)
用于構(gòu)建MockHttpServletRequest
论咏;其主要有兩個(gè)子類MockHttpServletRequestBuilder
和MockMultipartHttpServletRequestBuilder
(如文件上傳使用)糊余,即用來(lái)Mock客戶端
請(qǐng)求需要的所有數(shù)據(jù)。
1.MockMvcRequestBuilders主要API
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根據(jù)uri模板和uri變量值得到一個(gè)GET請(qǐng)求方式的MockHttpServletRequestBuilder洼专;如get(/user/{id}, 1L)雾袱;
MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables):同get類似煎殷,但是是POST方法剑梳;
MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables):同get類似酪刀,但是是PUT方法;
MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) :同get類似赵刑,但是是DELETE方法;
MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables):同get類似瓤摧,但是是OPTIONS方法;
MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables): 提供自己的Http請(qǐng)求方法及uri模板和uri變量皆辽,如上API都是委托給這個(gè)API盆耽;
MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables):提供文件上傳方式的請(qǐng)求匙姜,得到MockMultipartHttpServletRequestBuilder;
RequestBuilder asyncDispatch(final MvcResult mvcResult):創(chuàng)建一個(gè)從啟動(dòng)異步處理的請(qǐng)求的MvcResult進(jìn)行異步分派的RequestBuilder寸癌;
2.MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder API
(1)MockHttpServletRequestBuilder API
MockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders):添加頭信息;
MockHttpServletRequestBuilder contentType(MediaType mediaType):指定請(qǐng)求的contentType頭信息蒿秦;
MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes):指定請(qǐng)求的Accept頭信息炮叶;
MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content):指定請(qǐng)求Body體內(nèi)容;
MockHttpServletRequestBuilder cookie(Cookie... cookies):指定請(qǐng)求的Cookie渡处;
MockHttpServletRequestBuilder locale(Locale locale):指定請(qǐng)求的Locale镜悉;
MockHttpServletRequestBuilder characterEncoding(String encoding):指定請(qǐng)求字符編碼;
MockHttpServletRequestBuilder requestAttr(String name, Object value) :設(shè)置請(qǐng)求屬性數(shù)據(jù)医瘫;
MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes):設(shè)置請(qǐng)求session屬性數(shù)據(jù)侣肄;
MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes):指定請(qǐng)求的flash信息,比如重定向后的屬性信息醇份;
MockHttpServletRequestBuilder session(MockHttpSession session) :指定請(qǐng)求的Session稼锅;
MockHttpServletRequestBuilder principal(Principal principal) :指定請(qǐng)求的Principal;
MockHttpServletRequestBuilder contextPath(String contextPath) :指定請(qǐng)求的上下文路徑僚纷,必須以“/”開頭矩距,且不能以“/”結(jié)尾;
MockHttpServletRequestBuilder pathInfo(String pathInfo) :請(qǐng)求的路徑信息怖竭,必須以“/”開頭锥债;
MockHttpServletRequestBuilder secure(boolean secure):請(qǐng)求是否使用安全通道;
MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor):請(qǐng)求的后處理器,用于自定義一些請(qǐng)求處理的擴(kuò)展點(diǎn)赞弥;
(2)MockMultipartHttpServletRequestBuilder
繼承自MockHttpServletRequestBuilder毅整,又提供了如下API
MockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file):指定要上傳的文件趣兄;
四 ResultActions
調(diào)用MockMvc.perform(RequestBuilder requestBuilder)后將得到ResultActions绽左,通過(guò)ResultActions完成如下三件事:
ResultActions andExpect(ResultMatcher matcher) :添加驗(yàn)證斷言來(lái)判斷執(zhí)行請(qǐng)求后的結(jié)果是否是預(yù)期的;
ResultActions andDo(ResultHandler handler) :添加結(jié)果處理器艇潭,用于對(duì)驗(yàn)證成功后執(zhí)行的動(dòng)作拼窥,如輸出下請(qǐng)求/結(jié)果信息用于調(diào)試;
MvcResult andReturn() :返回驗(yàn)證成功后的MvcResult蹋凝;用于自定義驗(yàn)證/下一步的異步處理鲁纠;
五 ResultMatcher/MockMvcResultMatchers
1.ResultMatcher用來(lái)匹配執(zhí)行完請(qǐng)求后的結(jié)果驗(yàn)證,其就一個(gè)match(MvcResult result)斷言方法鳍寂,如果匹配失敗將拋出相應(yīng)的異常改含;spring mvc測(cè)試框架提供了很多***ResultMatchers來(lái)滿足測(cè)試需求。注意這些***ResultMatchers并不是ResultMatcher的子類迄汛,而是返回ResultMatcher實(shí)例的捍壤。Spring mvc測(cè)試框架為了測(cè)試方便提供了MockMvcResultMatchers靜態(tài)工廠方法方便操作;
2.具體的API如下:
HandlerResultMatchers handler():請(qǐng)求的Handler驗(yàn)證器鞍爱,比如驗(yàn)證處理器類型/方法名鹃觉;此處的Handler其實(shí)就是處理請(qǐng)求的控制器;
RequestResultMatchers request():得到RequestResultMatchers驗(yàn)證器睹逃;
ModelResultMatchers model():得到模型驗(yàn)證器盗扇;
ViewResultMatchers view():得到視圖驗(yàn)證器;
FlashAttributeResultMatchers flash():得到Flash屬性驗(yàn)證沉填;
StatusResultMatchers status():得到響應(yīng)狀態(tài)驗(yàn)證器疗隶;
HeaderResultMatchers header():得到響應(yīng)Header驗(yàn)證器;
CookieResultMatchers cookie():得到響應(yīng)Cookie驗(yàn)證器翼闹;
ContentResultMatchers content():得到響應(yīng)內(nèi)容驗(yàn)證器斑鼻;
JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath (String expression, Matcher matcher):得到Json表達(dá)式驗(yàn)證器;
XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args):得到Xpath表達(dá)式驗(yàn)證器橄碾;
ResultMatcher forwardedUrl(final String expectedUrl):驗(yàn)證處理完請(qǐng)求后轉(zhuǎn)發(fā)的url(絕對(duì)匹配)卵沉;
ResultMatcher forwardedUrlPattern(final String urlPattern):驗(yàn)證處理完請(qǐng)求后轉(zhuǎn)發(fā)的url(Ant風(fēng)格模式匹配,@since spring4)法牲;
ResultMatcher redirectedUrl(final String expectedUrl):驗(yàn)證處理完請(qǐng)求后重定向的url(絕對(duì)匹配)史汗;
ResultMatcher redirectedUrlPattern(final String expectedUrl):驗(yàn)證處理完請(qǐng)求后重定向的url(Ant風(fēng)格模式匹配,@since spring4)
六 一些常用的測(cè)試
1.測(cè)試普通控制器
mockMvc.perform(get("/user/{id}", 1)) //執(zhí)行請(qǐng)求
.andExpect(model().attributeExists("user")) //驗(yàn)證存儲(chǔ)模型數(shù)據(jù)
.andExpect(view().name("user/view")) //驗(yàn)證viewName
.andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp"))//驗(yàn)證視圖渲染時(shí)forward到的jsp
.andExpect(status().isOk())//驗(yàn)證狀態(tài)碼
.andDo(print()); //輸出MvcResult到控制臺(tái)
2.得到MvcResult自定義驗(yàn)證
MvcResult result = mockMvc.perform(get("/user/{id}", 1))//執(zhí)行請(qǐng)求
.andReturn(); //返回MvcResult
Assert.assertNotNull(result.getModelAndView().getModel().get("user")); //自定義斷言
3.驗(yàn)證請(qǐng)求參數(shù)綁定到模型數(shù)據(jù)及Flash屬性
mockMvc.perform(post("/user").param("name", "zhang")) //執(zhí)行傳遞參數(shù)的POST請(qǐng)求(也可以post("/user?name=zhang"))
.andExpect(handler().handlerType(UserController.class)) //驗(yàn)證執(zhí)行的控制器類型
.andExpect(handler().methodName("create")) //驗(yàn)證執(zhí)行的控制器方法名
.andExpect(model().hasNoErrors()) //驗(yàn)證頁(yè)面沒有錯(cuò)誤
.andExpect(flash().attributeExists("success")) //驗(yàn)證存在flash屬性
.andExpect(view().name("redirect:/user")); //驗(yàn)證視圖
4.文件上傳
byte[] bytes = new byte[] {1, 2};
mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //執(zhí)行文件上傳
.andExpect(model().attribute("icon", bytes)) //驗(yàn)證屬性相等性
.andExpect(view().name("success")); //驗(yàn)證視圖
5.JSON請(qǐng)求/響應(yīng)驗(yàn)證
String requestBody = "{\"id\":1, \"name\":\"zhang\"}";
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(requestBody)
.accept(MediaType.APPLICATION_JSON)) //執(zhí)行請(qǐng)求
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) //驗(yàn)證響應(yīng)contentType
.andExpect(jsonPath("$.id").value(1)); //使用Json path驗(yàn)證JSON 請(qǐng)參考http://goessner.net/articles/JsonPath/
String errorBody = "{id:1, name:zhang}";
MvcResult result = mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(errorBody)
.accept(MediaType.APPLICATION_JSON)) //執(zhí)行請(qǐng)求
.andExpect(status().isBadRequest()) //400錯(cuò)誤請(qǐng)求
.andReturn();
Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//錯(cuò)誤的請(qǐng)求內(nèi)容體
6.異步測(cè)試
//Callable
MvcResult result = mockMvc.perform(get("/user/async1?id=1&name=zhang")) //執(zhí)行請(qǐng)求
.andExpect(request().asyncStarted())
.andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) //默認(rèn)會(huì)等10秒超時(shí)
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1));
7.全局配置
mockMvc = webAppContextSetup(wac)
.defaultRequest(get("/user/1").requestAttr("default", true)) //默認(rèn)請(qǐng)求 如果其是Mergeable類型的拒垃,會(huì)自動(dòng)合并的哦mockMvc.perform中的RequestBuilder
.alwaysDo(print()) //默認(rèn)每次執(zhí)行請(qǐng)求后都做的動(dòng)作
.alwaysExpect(request().attribute("default", true)) //默認(rèn)每次執(zhí)行后進(jìn)行驗(yàn)證的斷言
.build();
mockMvc.perform(get("/user/1"))
.andExpect(model().attributeExists("user"));