什么是Mock?
在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。
使用mock工具可以直接模拟http请求,不用直接产生网络的请求环境,简化了测试流程。
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
Spring MVC的测试往往看似比较复杂。其实他的不同在于,他需要一个ServletContext来模拟我们的请求和响应。但是Spring也针对Spring MVC 提供了请求和响应的模拟测试接口,以方便我们的单元测试覆盖面不只是service,dao层。
为什么使用Mock对象?
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
在以下情况可以采用模拟对象来替代真实对象:
- 真实对象的行为是不确定的(例如,当前的时间或温度);
- 真实对象很难搭建起来;
- 真实对象的行为很难触发(例如,网络错误);
- 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
- 真实的对象是用户界面,或包括用户界面在内;
- 真实的对象使用了回调机制;
- 真实对象可能还不存在;
- 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。
测试流程:
1 mockMvc调用perform,调用controller的业务处理逻辑
2 perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式。
3 使用StatusResultMatchers对请求结果进行验证
4 使用ContentResultMatchers对请求返回的内容进行验证
MockMvc
MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。
举例: Junit 测试Springboot +RestFul风格API
1)导入启动器,创建SpringBoot项目中默认引入的spring-boot-starter-test间接引入了spring-test,因此无需再额外引入jar包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2)具体实例
package com.inventec.studentManagement.controller;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentControllerTest {
@Autowired
StudentController studentController;
private MockMvc mvc;
@Before
public void setUp() {
mvc = MockMvcBuilders.standaloneSetup(studentController).build();
}
/**
* @throws Exception
*
* 1.测试所有学生,有数据,正向测试,测试成功
*/
@Test
public void selectStudentsTest() throws Exception {
RequestBuilder request;
/*
* 1、mockMvc.perform执行一个请求。
* 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
* 3、ResultActions.param添加请求传值
* 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
* 5、ResultActions.andExpect添加执行完成后的断言。
* 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
* 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
* 7、ResultActions.andReturn表示执行完成后返回相应的结果。
*/
/*方法:GET
URL:http://localhost:8080/students
请求参数:无*/
// 1、get查询所有学生
request = get("/students/")
// 设置返回值类型为utf-8,否则默认为ISO-8859-1
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(not("")))
.andExpect(content().string(not("[]")));
}
/**
* @throws Exception
*
* 1.测试所有学生,无数据,正向测试,测试成功
*/
@Test
public void selectStudentsNotStudentTest() throws Exception {
RequestBuilder request;
/*方法:GET
URL:http://localhost:8080/students
请求参数:无*/
// 1、get查询所有学生,无数据。
request = get("/students/");
mvc.perform(request).andExpect(status().isOk())
.andExpect(content().string(equalTo("{\"state\":1,\"message\":null,\"data\":[]}")));
}
/**
* @throws Exception
*
* 2. 测试根据学号查询某个学生 测试成功
*/
@Test
public void selectStudentTest() throws Exception {
RequestBuilder request;
/*方法:GET
URL:http://localhost:8080/students/”student_sno”
请求参数: String student_sno(15位任意数字或字母)*/
// 1、get根据正确学号查询某个学生
request = get("/students/773456789012345");
mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(equalTo(
"{\"state\":1,\"message\":null,\"data\":{\"student_sno\":\"773456789012345\",\"student_sname\":\"rose\",\"student_sex\":1,\"student_age\":18,\"student_time\":null}}")));
}
/**
* @throws Exception
*
* 3. 测试 新增一个学生信息, 正向测试,失败
*
*/
@Test
public void addStudentTest() throws Exception {
RequestBuilder request;
/*方法:POST
URL:http://localhost:8080/students
请求参数:JSON
{
String student_sno;(学生编号) 必填(15位数字或者字母)
String student_sname;(学生姓名) 必填
int student_sex; (学生性别) 必填
int student_age; (学生年龄) 必填
String student_time;(创建时间) 选填(yyy-MM-SS DD:MM:SS)
}
*/
// 学号 新增一个学生信息,无时间,测试成功。
request = post("/students/").contentType(MediaType.APPLICATION_JSON)
.content("{\"student_sno\":\"773456789012345\",\"student_sname\":\"rose\",\"student_sex\":1,\"student_age\":18}");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
}
/**
* @throws Exception
*
* 4.修改一个学生信息,反向测试,测试部分成功,部分失败。
*/
@Test
public void updateStudentReverseTest() throws Exception {
RequestBuilder request;
/*String student_sno 必填 (15位任意数字或字母)
JSON
{
String student_sno;(学生编号) 选填(15位数字或者字母)
String student_sname;(学生姓名) 选填
int student_sex; (学生性别) 选填
int student_age; (学生年龄) 选填
String student_time;(创建时间) 选填(yyy-MM-SS DD:MM:SS)
}
*/
// put 修改id长度, 不通过测试, 数据修改成功,数据库没有更新数据。 测试失败。
request = patch("/students/123456789012345").contentType(MediaType.APPLICATION_JSON).content(
"{\"student_sno\":12534,\"student_sname\":\"tom\",\"student_sex\":\"0\",\"student_age\":18}");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
// put 修改不存在的id, 通过测试
/* request = patch("/students/903456789012345").contentType(MediaType.APPLICATION_JSON)
.content("{\"student_sno\":12534,\"student_sname\":\"tom\",\"student_sex\":\"0\",\"student_age\":18}");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
*/
}
/**
* @throws Exception 5.删除一个学生信息,正向删除 通过测试
*/
@Test
public void deleteStudentTest() throws Exception {
RequestBuilder request;
/*方法:DELETE
URL:http://localhost:8080/students/"student_sno"
请求参数:String student_sno 必填 (15位任意数字或字母)*/
// 删除成功的情况
/*
request = delete("/students/663456789012345");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
*/
}
/**
* @throws Exception 5.删除一个学生信息,逆向测试, 全部通过测试
*/
@Test
public void deleteStudentReverseTest() throws Exception {
RequestBuilder request;
/*方法:DELETE
URL:http://localhost:8080/students/"student_sno"
请求参数:String student_sno 必填 (15位任意数字或字母)*/
// 请求参数:String student_sno 必填 (15位任意数字或字母)
// 删除失败的情况,不存在的id删除
request = delete("/students/403456789012345");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
// 删除失败的情况,位数不够的id删除
request = delete("/students/56789012345");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
// 删除失败的情况,重复删除
request = delete("/students/113456789012345");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
// 删除失败的情况,重复删除
request = delete("/students/113456789012345");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
// 删除失败的情况,不存在的的id
request = delete("/students/3456789012345aa");
mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));
// 删除失败的情况,不传id
request = delete("/students/");
mvc.perform(request).andExpect(status().is4xxClientError());
}
}
3)常用的例子
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"));
MockMvcResultHandlers.print()打印结果中body中文乱码的解决
ResultActions resultActions = mockMvc.perform(post(requestUrl)
.accept(MediaType.APPLICATION_JSON)
.content(requestParam));
resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
//添加断言
resultActions.andDo(print()).andExpect(status().isOk());
4)方法解释说明。
常用注解
- RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试;
- WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;作用是模拟ServletContext
- ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;
常用方法说明:
- perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
- get:声明发送一个get请求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。
- param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法
- andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断);
- andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);
- andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断);
如果要进行远端测试:可以使用httpclient
httpclient 测试RestFul风格接口详细教程 https://blog.csdn.net/huzecom/article/details/103589457