一、简介
为什么使用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)
七、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的几种构造方式:
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();
}
}
注意:
-
mockHttpServletRequestBuilder.accept(), 如果类型设置的不正确,报406(后台的返回结果前台无法解析)
-
mockHttpServletRequestBuilder.contentType()可以不用设置
-
如果mockHttpServletRequestBuilder.param(“docId”, “20200116000078270841”)改成content(“20200116000078270841”), 很明显controller接收到的参数就是空
-
如果controller层的接收参数是个实体,使用.content()的方式传递参数,还需要设置contentType。( 之前一直觉得get请求都用.param(), Post请求都用.content() )
-
思考:如果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"))
.andDo(print())
.andReturn().getResponse().getContentAsString();
} catch (Exception e) {
e.printStackTrace();
}
}
注意:
1)controller接收的参数是个实体JobIdReq, 所以是mockHttpServletRequestBuilder.content(…)
-
如果不设置contentType(MediaType.APPLICATION_JSON)会报错,因为传参是个json串,对应不到controller层的参数,报org.springframework.web.HttpMediaTypeNotSupportedException错误。
-
思考:如何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 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"))
.andExpect(view().name("user/view"))
.andExpect(flash().attributeExists("success"))
.andExpect(jsonPath("$.id").value(1));
.andExpect(handler().handlerType(LabelDocController.class))
.andExpect(handler().methodName("getContentAndLabel"))
.andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp"))
.andExpect(status().isOk())
.andDo(print());
2、得到MvcResult自定义验证
ResultActions resultActions = mockMvc.perform(mockHttpServletRequestBuilder);
mvcResult = resultActions
.andExpect(status().isOk())
.andDo(print())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
MockHttpServletRequest request = mvcResult.getRequest();
ModelAndView modelAndView = mvcResult.getModelAndView();
Object ayncResult = mvcResult.getAsyncResult();
String contentAsString = mvcResult.getResponse().getContentAsString();
JSONObject jsonObject = JSON.parseObject(contentAsString);
Assert.assertEquals("20191111123243345", jsonObject.getJSONObject("content").getString("docId"));
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)))
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1));