MockMvc探索—SpringMVC单元测试

一、简介

为什么使用MockMvc ?

  1. 只对service层进行测试,测试面就覆盖不到controller层,无法做到模拟前端的请求,也无法使用到一些例如@NotNull这样的参数校验。
  2. 如果借助其他工具如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的运行逻辑:

  1.  MockMvcBuilders构造MockMvc的构造器
  2.  MockMvcRequestBuilders构造RequestBuilder请求            -→ MockHttpServletRequestBuilder
  3.  mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务层处理逻辑
  4.  perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式。
  5. 使用StatusResultMatchers对结果进行验证                          → status().isOk()
  6. 使用ContentResultMatchers对请求返回的内容进行验证      → 
  7. 使用ResultHandler对请求结果进行处理                                → print()
  8. 获取方法的返回值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有以下三种处理:

  1. ResultActions.andExpect:添加执行完成后的断言。添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确。
  2. ResultActions.andDo:添加一个结果处理器,比如此处使用.andDo(MockMvcResultHandlers.print())输出整个响应结果信息,可以在调试的时候使用。
  3. 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));

 

 

 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值