SpringBoot基础之MockMvc单元测试

SpringBoot建立的Maven项目中,会默认添加spring-boot-starter-test依赖。在《5分钟快速上手SpringBoot》中编写的单元测试使用了MockMvc。本篇文章就围绕MockMvc在SpringBoot中的使用进行讲解。html

 

什么是Mockweb

 

在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程当中,一般经过模拟一些输入数据,来验证程序是否达到预期结果。spring

 

为何使用Mock对象数据库

 

使用模拟对象,能够模拟复杂的、真实的对象行为。若是在单元测试中没法使用真实对象,可采用模拟对象进行替代。编程

 

在如下状况能够采用模拟对象来替代真实对象:json

 

- 真实对象的行为是不肯定的(例如,当前的时间或温度);服务器

- 真实对象很难搭建起来;网络

- 真实对象的行为很难触发(例如,网络错误);app

- 真实对象速度很慢(例如,一个完整的数据库,在测试以前可能须要初始化);异步

- 真实的对象是用户界面,或包括用户界面在内;

- 真实的对象使用了回调机制;

- 真实对象可能还不存在;

- 真实对象可能包含不能用做测试(而不是为实际工做)的信息和方法。

 

使用Mockito通常分三个步骤:一、模拟测试类所需的外部依赖;二、执行测试代码;三、判断执行结果是否达到预期;

 

MockMvc

 

MockMvc是由spring-test包提供,实现了对Http请求的模拟,可以直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。

 

接口MockMvcBuilder,提供一个惟一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是经过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的建立方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用便可。

 

SpringBoot中使用

 

第一步:jar包引入。建立SpringBoot项目中默认引入的spring-boot-starter-test间接引入了spring-test,所以无需再额外引入jar包。

 

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency>

 

第二步:建立HelloWorldController类,并提供hello方法做为待测试的业务接口。

 

  •  
@RestControllerpublic class HelloWorldController {
@RequestMapping public String hello(String name){ return "Hello " + name + "!"; }}

 

第三步:编写测试类。实例化MockMvc有两种形式,一种是使用StandaloneMockMvcBuilder,另一种是使用DefaultMockMvcBuilder。测试类及初始化MockMvc初始化:

 

//SpringBoot1.4版本以前用的是SpringJUnit4Cla***unner.class@RunWith(SpringRunner.class)//SpringBoot1.4版本以前用的是@SpringApplicationConfiguration(classes = Application.class)@SpringBootTest//测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的@WebAppConfigurationpublic class HelloWorldTest {
private MockMvc mockMvc;
@Autowired private WebApplicationContext webApplicationContext;
@Before public void setup() { // 实例化方式一 mockMvc = MockMvcBuilders.standaloneSetup(new HelloWorldController()).build(); // 实例化方式二// mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); }

单元测试方法:

 

@Testpublic void testHello() throws Exception {
/* * 一、mockMvc.perform执行一个请求。 * 二、MockMvcRequestBuilders.get("XXX")构造一个请求。 * 三、ResultActions.param添加请求传值 * 四、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型 * 五、ResultActions.andExpect添加执行完成后的断言。 * 六、ResultActions.andDo添加一个结果处理器,表示要对结果作点什么事情 * 好比此处使用MockMvcResultHandlers.print()输出整个响应结果信息。 * 七、ResultActions.andReturn表示执行完成后返回相应的结果。 */ mockMvc.perform(MockMvcRequestBuilders .get("/hello") // 设置返回值类型为utf-8,不然默认为ISO-8859-1 .accept(MediaType.APPLICATION_JSON_UTF8_VALUE) .param("name", "Tom")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("Hello Tom!")) .andDo(MockMvcResultHandlers.print());}

测试结果打印:

 

FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"10"] Content type = application/json;charset=UTF-8 Body = Hello Tom! Forwarded URL = null Redirected URL = null Cookies = []2019-04-02 21:34:27.954 INFO 6937 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'

 

整个过程以下:

一、准备测试环境

二、经过MockMvc执行请求

三、添加验证断言

四、添加结果处理器

五、获得MvcResult进行自定义断言/进行下一步的异步请求

六、卸载测试环境

 

注意事项:若是使用DefaultMockMvcBuilder进行MockMvc实例化时需在SpringBoot启动类上添加组件扫描的package的指定,不然会出现404。如:

  •  
@ComponentScan(basePackages = "com.secbro2")

 

相关API

 

RequestBuilder提供了一个方法buildRequest(ServletContext servletContext)用于构建MockHttpServletRequest;其主要有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(如文件上传使用)。

 

MockMvcRequestBuilders提供get、post等多种方法用来实例化RequestBuilder。

 

ResultActions,MockMvc.perform(RequestBuilder requestBuilder)的返回值,提供三种能力:andExpect,添加断言判断结果是否达到预期;andDo,添加结果处理器,好比示例中的打印;andReturn,返回验证成功后的MvcResult,用于自定义验证/下一步的异步处理。

 

一些经常使用的测试

 

1.测试普通控制器

  •  
  •  
  •  
  •  
  •  
  •  
mockMvc.perform(get("/user/{id}", 1)) //执行请求  .andExpect(model().attributeExists("user")) //验证存储模型数据  .andExpect(view().name("user/view")) //验证viewName  .andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp"))//验证视图渲染时forward到的jsp  .andExpect(status().isOk())//验证状态码             .andDo(print()); //输出MvcResult到控制台

 

2.获得MvcResult自定义验证

  •  
  •  
  •  
MvcResult result = mockMvc.perform(get("/user/{id}", 1))//执行请求  .andReturn(); //返回MvcResult Assert.assertNotNull(result.getModelAndView().getModel().get("user")); //自定义断言 

 

3.验证请求参数绑定到模型数据及Flash属性

  •  
  •  
  •  
  •  
  •  
  •  
  •  
mockMvc.perform(post("/user").param("name", "zhang")) //执行传递参数的POST请求(也能够post("/user?name=zhang"))  .andExpect(handler().handlerType(UserController.class)) //验证执行的控制器类型  .andExpect(handler().methodName("create")) //验证执行的控制器方法名  .andExpect(model().hasNoErrors()) //验证页面没有错误  .andExpect(flash().attributeExists("success")) //验证存在flash属性  .andExpect(view().name("redirect:/user")); //验证视图 

 

4.文件上传

  •  
  •  
  •  
  •  
byte[] bytes = new byte[] {1, 2}; mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //执行文件上传  .andExpect(model().attribute("icon", bytes)) //验证属性相等性  .andExpect(view().name("success")); //验证视图 

 

5.JSON请求/响应验证

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
String requestBody = "{\"id\":1, \"name\":\"zhang\"}";  mockMvc.perform(post("/user")  .contentType(MediaType.APPLICATION_JSON).content(requestBody)  .accept(MediaType.APPLICATION_JSON)) //执行请求  .andExpect(content().contentType(MediaType.APPLICATION_JSON)) //验证响应contentType  .andExpect(jsonPath("$.id").value(1)); //使用Json path验证JSON 请参考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)) //执行请求  .andExpect(status().isBadRequest()) //400错误请求  .andReturn();     Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//错误的请求内容体

 

6.异步测试

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
//Callable 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));  

 

7.全局配置

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
mockMvc = webAppContextSetup(wac)  .defaultRequest(get("/user/1").requestAttr("default", true)) //默认请求 若是其是Mergeable类型的,会自动合并的哦mockMvc.perform中的RequestBuilder  .alwaysDo(print()) //默认每次执行请求后都作的动做  .alwaysExpect(request().attribute("default", true)) //默认每次执行后进行验证的断言  .build(); mockMvc.perform(get("/user/1"))  .andExpect(model().attributeExists("user"));
部份内容参考:https://www.cnblogs.com/lyy-2016/p/6122144.html