一、简介
为什么使用MockMvc ?
- 只对service层进行测试,测试面就覆盖不到controller层,无法做到模拟前端的请求,也无法使用到一些例如@NotNull这样的参数校验。
- 如果借助其他工具如postman发送http请问,需要先启动项目再发送请求,要分两部进行,步骤繁琐;不方面以后其他人员重复运行测试用例;结果校验需要人工比对数据。
MockMvc是什么?
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
二、MockMvc测试的运行逻辑
看一段代码:
@Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } @Test public void testDeleteJob() { JobIdReq jobId = new JobIdReq(); jobId.setJobId( "20200217000000201853" ); String httpUrl = "/label/job/delete/" ; MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders.post(httpUrl) .contentType(MediaType.APPLICATION_JSON) .content(JsonUtil.transfer2JsonString(jobId)).param( "username" , "zhangsan" ).param( "password" , "123456" ); try { ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder); resultActions.andExpect(status().isOk()) // 对返回值进行断言 .andDo(print()) // 打印输出发出请求的详细信息 .andReturn().getResponse().getContentAsString(); // 获取方法的返回值 MvcResult mvcResult = resultActions.andReturn(); } catch (Exception e) { e.printStackTrace(); } } |
结合代码分析MockMvc的运行逻辑:
- MockMvcBuilders构造MockMvc的构造器
- MockMvcRequestBuilders构造RequestBuilder请求 -→ MockHttpServletRequestBuilder
- mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务层处理逻辑
- perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式。
- 使用StatusResultMatchers对结果进行验证 → status().isOk()
- 使用ContentResultMatchers对请求返回的内容进行验证 →
- 使用ResultHandler对请求结果进行处理 → print()
- 获取方法的返回值MvcResult → andReturn()
三、MockMvcBuilders
MockMvc是spring测试下的一个非常好用的类,他们的初始化需要在setUp中进行。
MockMvcBuilders用来构造MockMvc, 而MockMvcBuilders的工作就是将构造任务委托给DefaultMockMvcBuilder或StandaloneMockMvcBuilder来完成。
① MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并构造相应的MockMvc;
② MockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了,比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build(); (没有实验过)
四、MockMvcRequestBuilders
从名字上看,MockMvcRequestBuilders是用来构造请求的,其主要有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(如文件上传使用),即用来Mock客户端请求需要的所有数据。
MockMvcRequestBuilders的主要API:
MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder,如果在controller的方法中method选择的是RequestMethod.GET,那在controllerTest中对应就要使用MockMvcRequestBuilders.get
MockHttpServletRequestBuilder get(URI uri) : 根据URI 得到一个GET请求方式的MockHttpServletRequestBuilder
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方法;
MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... urlVariables) : 得到一个MockMultipartHttpServletRequestBuilder
RequestBuilder asyncDispatch(MvcResult mvcResult):得到一个RequestBuilder
五、MockHttpServletRequestBuilder
从上面代码中可以看出,mockMvc的perform对象就是MockHttpServletRequestBuilder。我们在MockMvc的测试中,最重要的就是构造请求对象。那么如何正确的模拟http请求呢?
MockHttpServletRequestBuilder的主要API:
MockHttpServletRequestBuilder contentType(String contentType):设置请求中的媒体类型
MockHttpServletRequestBuilder contentType(MediaType contentType)
MockHttpServletRequestBuilder contentType(String contentType) : 指定客户端可以接收的媒体类型
MockHttpServletRequestBuilder accept(MediaType... mediaTypes)
MockHttpServletRequestBuilder content(String content) : 设置请求实体的内容(请求实体的json串)
MockHttpServletRequestBuilder content(byte[] content)
MockHttpServletRequestBuilder param(String name, String... values):设置请求参数,name对应congroller方法中指定的参数名称
MockHttpServletRequestBuilder characterEncoding(String encoding):指定编码方式
MockHttpServletRequestBuilder header(String name, Object... values): 设置header
MockHttpServletRequest buildRequest(ServletContext servletContext):
MockHttpServletRequest postProcessRequest(MockHttpServletRequest request)
MockHttpServletRequestBuilder cookie(Cookie... cookies):设置cookie
MockHttpServletRequestBuilder sessionAttr(String name, Object value) : 设置session
六、MockMultipartHttpServletRequestBuilder
它继承了MockHttpServletRequestBuilder
如果是文件上传,需要用到这个builder, MockMultipartHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart(httpUrl) .
除了继承到的方法,它还有的主要API:
MockMultipartHttpServletRequestBuilder file(MockMultipartFile file)
MockMultipartHttpServletRequestBuilder file(String name, byte[] content)
Requests & Responses 的标识及解释
六、ResultActions
调用MockMvc.perform(RequestBuilder requestBuilder)后将得到ResultActions,对ResultActions有以下三种处理:
- ResultActions.andExpect:添加执行完成后的断言。添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确。
- ResultActions.andDo:添加一个结果处理器,比如此处使用.andDo(MockMvcResultHandlers.print())输出整个响应结果信息,可以在调试的时候使用。
- ResultActions.andReturn:表示执行完成后返回相应的结果
七、MvcResult
MvcResult mvcResult = resultActions.andReturn();
其主要API:
MockHttpServletResponse getResponse()
MockHttpServletRequest getRequest()
Object getAsyncResult()
Object getAsyncResult(long timeToWait)
ModelAndView getModelAndView()
八、MockMultipartFile与MultiPartFile
MultiPartFile:用来表示文件上传请求中的文件(A representation of an uploaded file received in a multipart request.)
MultiPartFile作为一个接口,继承InputStreamSource,有4个实现类:
ByteArrayMultipartFile:文件数据作为字节数组保存在内存中(the file data is held as a byte array in memory. )
CommonsMultipartFile:
MockMultipartFile:测试带有上传文件的controller接口时,使用它Mock要上传的文件。
StandardMultipartFile:为了适配Servelt3.0 (Spring MultipartFile adapter, wrapping a Servlet 3.0 Part object.)。
如果应用部署到Servlet 3.0的容器中,那么会有MultipartFile的一个替代方案,Spring MVC也能接 受javax.servlet.http.Part作为控制器方法的参数。
StandardMultipartFile的构造方法
public StandardMultipartFile(Part part, String filename) {
this.part = part;
this.filename = filename;
}
MockMultipartFile的几种构造方式:
/**
* Create a new MockMultipartFile with the given content.
* @param name the name of the file
* @param originalFilename the original filename (as on the client's machine)
* @param contentType the content type (if known)
* @param content the content of the file
*/
1)MockMultipartFile( String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content)
2)MockMultipartFile(String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream)
3)MockMultipartFile(String name, @Nullable byte[] content) {this(name, "", null, content);}
4)MockMultipartFile(String name, InputStream contentStream)
九、使用示例:
1、Get 请求
controller: @GetMapping (value = "/doc/contentlabel" ) @ApiOperation (value = "文档内容查询接口 " ) public GenericResponse<DocContentAndLabelResponse> getContentAndLabel( @RequestParam (name = "docId" ) final String docId) {....} ------------------------------------------------------- @Test public void getContentlabel() { String httpUrl = "/label/doc/contentlabel" ; MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders.get(httpUrl); mockHttpServletRequestBuilder.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON) .param( "docId" , "20200116000078270841" ); MvcResult result = null ; try { result = mockMvc.perform(mockHttpServletRequestBuilder) .andExpect(status().isOk()) .andDo(print()) .andReturn(); } catch (Exception e) { e.printStackTrace(); } } |
注意:
1)mockHttpServletRequestBuilder.accept(), 如果类型设置的不正确,报406(后台的返回结果前台无法解析)
2)mockHttpServletRequestBuilder.contentType()可以不用设置
3) 如果mockHttpServletRequestBuilder.param("docId", "20200116000078270841")改成content("20200116000078270841"), 很明显controller接收到的参数就是空
4)如果controller层的接收参数是个实体,使用.content()的方式传递参数,还需要设置contentType。( 之前一直觉得get请求都用.param(), Post请求都用.content() )
5)思考:如果controller参数有单个参数,也有实体,针对post和get请求,参数该怎么传递?
2、Post请求
Controller: @RequestMapping (value = "delete" , method = RequestMethod.POST) @ResponseStatus (HttpStatus.OK) public GenericResponse deleteJob( @ApiParam (value = "任务 id" , name = "jobId" , required = true ) @RequestBody @Valid JobIdReq jobId) { procy essJobService.deleteJob(jobId.getJobId()); return new GenericResponse(); } ------------------------------------------------------- @Test public void testDeleteJob() { JobIdReq jobId = new JobIdReq(); jobId.setJobId( "20200217000000201853" ); String httpUrl = "/label/job/delete/" ; MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders.post(httpUrl) .contentType(MediaType.APPLICATION_JSON) .content(JsonUtil.transfer2JsonString(jobId)) .accept(MediaType.APPLICATION_JSON); try { ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder); resultActions.andExpect(status().isOk()) // 对返回值进行断言 .andExpect(content().contentType( "application/json;charset=UTF-8" )) //验证响应contentType == application/json;charset=UTF-8 .andDo(print()) // 打印输出发出请求的详细信息 .andReturn().getResponse().getContentAsString(); // 获取方法的返回值 } catch (Exception e) { e.printStackTrace(); } } |
注意:
1)controller接收的参数是个实体JobIdReq, 所以是mockHttpServletRequestBuilder.content(....)
2) 如果不设置contentType(MediaType.APPLICATION_JSON)会报错,因为传参是个json串,对应不到controller层的参数,报org.springframework.web.HttpMediaTypeNotSupportedException错误。
3) 思考:如何controller层的接收参数是个实体,可以使用.param参数设置实体中的属性值吗? 答案是不行的。(之所以这样思考是因为文件上传的测试种,尽管接收参数是个实体,还是可以通过.param()设置值 )
4)如果controller接收的参数是单个值,不是实体,使用.param()的方式设置参数。( 之前一直觉得get请求都用.param(), Post请求都用.content() )
3、文件上传 (Controller层接收参数包括上传文件、单个属性值)
Controller层: @RequestMapping (value = "/fileUpload" , method = {RequestMethod.POST}) @ResponseBody public GenericResponse fileUpload( @NotNull List<MultipartFile> multipartFileList, @NotNull String jobName, @NotNull String jobType, @NotNull String bizDefine) ------------------------------------------------------- @Test public void testFileUpload() { List<MockMultipartFile> fileList = generateFiles(Arrays.asList( "multipartFileList" , "multipartFileList" )); MockMultipartFile baseFile = fileList.get( 0 ); MockMultipartFile compareFile = fileList.get( 1 ); String jobName = "fileUploadtest0001" ; String jobType = "DIFF" ; String bizDefine = "docVerify" ; String httpUrl = "/label/fileUpload" ; MockHttpServletRequestBuilder mockHttpServletRequestBuilder = (MockMvcRequestBuilders.multipart(httpUrl) .file(baseFile).file(compareFile) .param( "jobName" , jobName) .param( "jobType" , jobType) .param( "bizDefine" , bizDefine)); try { ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder); resultActions.andExpect(status().isOk()) // 对返回值进行断言 .andDo(print()) // 打印输出发出请求的详细信息 .andReturn().getResponse().getContentAsString(); // 获取方法的返回值 } catch (Exception e) { e.printStackTrace(); } } public List<MockMultipartFile> generateFiles(List<String> mockMultipartFileNames) { File baseFile = new File( "/Users/qinwenjing/Documents/financeDoc/利润公告.pdf" ); File compareFile = new File( "/Users/qinwenjing/Documents/financeDoc/利润表_table.pdf" ); List<MockMultipartFile> fileList = new ArrayList<>(); FileInputStream basefFileInputStream = null ; FileInputStream comparefFileInputStream = null ; try { basefFileInputStream = new FileInputStream(baseFile); MockMultipartFile baseMultipartFile = new MockMultipartFile(mockMultipartFileNames.get( 0 ), baseFile.getName(), ContentType.APPLICATION_OCTET_STREAM.toString(), basefFileInputStream); comparefFileInputStream = new FileInputStream(compareFile); MockMultipartFile compareMultipartFile = new MockMultipartFile(mockMultipartFileNames.get( 1 ), compareFile.getName(), ContentType.APPLICATION_OCTET_STREAM.toString(), comparefFileInputStream); fileList.add(baseMultipartFile); fileList.add(compareMultipartFile); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (basefFileInputStream != null ) { try { basefFileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (comparefFileInputStream != null ) { try { comparefFileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return fileList; } |
注意:
1)MockMvcRequestBuilders.multipart(httpUrl)相当于是个post请求,如果controller只配置Get接收方式会有问题
2)contentType默认为 Headers = [Content-Type:"multipart/form-data"]
3)主意MockMultipartFile 对象的创建
MockMultipartFile baseMultipartFile = new MockMultipartFile(mockMultipartFileNames.get(0), baseFile.getName(),
ContentType.APPLICATION_OCTET_STREAM.toString(), basefFileInputStream);
第一个参数对应controller方法指定的文件接收对象的值,controller中文件接收对象是List<MultipartFile> multipartFileList,构造的所有MockMultipartFile的第一个参数值都是"multipartFileList"。
如果controller中方法只接收一个文件,而不是List,则构造的MockMultipartFile第一个参数就为controller中指定的文件参数名。
4、文件上传 (Controller层接收参数包括上传文件、单个属性值、实体)
注意:一般不会选择这个接收参数的形式,这里纯粹为了测试
Controller层: @RequestMapping (value = "/fileUpload" , method = {RequestMethod.POST}) @ResponseBody public GenericResponse fileUpload( @NotNull List<MultipartFile> multipartFileList, @NotNull String jobName, @Valid ExtractFileUpload fileUploadObject) ------------------------------------------------------- @Test public void testFileUpload() { List<MockMultipartFile> fileList = generateFiles(Arrays.asList( "multipartFileList" , "multipartFileList" )); MockMultipartFile baseFile = fileList.get( 0 ); MockMultipartFile compareFile = fileList.get( 1 ); String jobName = "fileUploadtest0001" ; String jobType = "DIFF" ; String bizDefine = "docVerify" ; String httpUrl = "/label/fileUpload" ; ExtractFileUpload fileUploadObject = new ExtractFileUpload(); fileUploadObject.setJobType(jobType); fileUploadObject.setBizDefine(bizDefine); MockHttpServletRequestBuilder mockHttpServletRequestBuilder = (MockMvcRequestBuilders.multipart(httpUrl) .file(baseFile).file(compareFile) .param( "jobName" , jobName) .content(JsonUtil.transfer2JsonString(fileUploadObject)) .contentType(MediaType.APPLICATION_JSON)); try { ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder); resultActions.andExpect(status().isOk()) // 对返回值进行断言 .andDo(print()) // 打印输出发出请求的详细信息 .andReturn().getResponse().getContentAsString(); // 获取方法的返回值 } catch (Exception e) { e.printStackTrace(); } } -------------------------------------------------------------- @Data public class ExtractFileUpload { private String jobName; /** 可为空 */ @ApiModelProperty (value = "schemaId or docCode" ) private String schemaId; /** * 上传文档 */ @NotNull private List<MultipartFile> multipartFileList; @ApiModelProperty (value = "短文本抽取页传 shortDoc、三表勾稽页传 docReview、首页新建任务传 combined" ) private String bizDefine; @ApiModelProperty (value = "短文本抽取页传EXTRACT、三表勾稽页传AUDIT、首页新建任务传EXTRACT" ) private String jobType; } |
注意:
1)controller层ExtractFileUpload fileUploadObject参数中的值设置并不能从content(JsonUtil.transfer2JsonString(fileUploadObject)中拿到,反而是从param中参数中拿到。
查看controller层接收到的参数会发下,fileUploadObject实体中jobName和multipartFileList有值,而bizDefine和jobType没有值。
2)如果controller层的方法如下,接收参数只有个实体,对应的测试方式和 『文件上传 (Controller层接收参数包括上传文件、单个属性值)』中的测试方法一样, 使用 .param设置参数
Controller层: @RequestMapping (value = "/extract/job/create" , method = {RequestMethod.POST}) @ResponseBody public GenericResponse extractFileUpload( @Valid ExtractFileUpload extractFileUpload) { logger.info( "ExtractJobInfoController#extractJobCreate extractJobForm={}" , extractFileUpload); extractProcessJobService.extractFileUpload(extractFileUpload); return new GenericResponse<>(CommonConstants.SUCCESS); } -------------------------------------------------------------- |
十、常用结果验证
上面示例中的代码只是简单的进行了状态码的验证,没有对执行结果的细节进行验证,下面详细的对执行结果进行验证。
1、使用andExpect进行验证
mockMvc.perform(get( "/label/doc/contentlabel" )) //执行请求 .andExpect(model().hasNoErrors()) //验证页面没有错误 .andExpect(model().attributeExists( "user" )) //验证存储模型数据, model代表的ModelAndView .andExpect(view().name( "user/view" )) //验证viewName .andExpect(flash().attributeExists( "success" )) //验证存在flash属性 .andExpect(jsonPath( "$.id" ).value( 1 )); //使用Json path验证JSON 请参考http://goessner.net/articles/JsonPath/ .andExpect(handler().handlerType(LabelDocController. class )) //验证执行的控制器类型 .andExpect(handler().methodName( "getContentAndLabel" )) //验证执行的控制器方法名 .andExpect(forwardedUrl( "/WEB-INF/jsp/user/view.jsp" )) //验证视图渲染时forward到的jsp .andExpect(status().isOk()) //验证状态码 .andDo(print()); //输出MvcResult到控制台 |
2、得到MvcResult自定义验证
ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder); mvcResult = resultActions .andExpect(status().isOk()) .andDo(print()) .andReturn(); MockHttpServletResponse response = mvcResult.getResponse(); MockHttpServletRequest request = mvcResult.getRequest(); // ModelAndView : Holder for both Model and View in the web MVC framework ModelAndView modelAndView = mvcResult.getModelAndView(); // Get the result of async execution. Object ayncResult = mvcResult.getAsyncResult(); // 对response进行验证,主要是对返回结果内容的验证, 返回的是 @ResponseBody json字符串 String contentAsString = mvcResult.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(contentAsString); Assert.assertEquals( "20191111123243345" , jsonObject.getJSONObject( "content" ).getString( "docId" )); // 对modelAndView验证 Assert.assertNotNull(mvcResult.getModelAndView().getModel().get( "user" )); |
说明:
我主要是对返回结果的内容进行验证,可以讲返回的结果转换为JSONObject对象进行验证。可以使用断言的方式验证返回结果参数的正确性。
3、异步验证
MvcResult result = mockMvc.perform(get( "/user/async1?id=1&name=zhang" )) //执行请求 .andExpect(request().asyncStarted()) .andExpect(request().asyncResult(CoreMatchers.instanceOf(User. class ))) //默认会等10秒超时 .andReturn(); mockMvc.perform(asyncDispatch(result)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath( "$.id" ).value( 1 )); |