在代码中,我们经常会发现大量的外部依赖项,比如对数据库的依赖、对第三方库的依赖、对文件系统的依赖等等。Mockito
是一个Mock框架,能够自行构造数据,从而让我们更加专注地去测试代码逻辑。本文将对SpringMVC框架中使用Mockito
进行单元测试所会遇到的问题进行记录,从而更好地完成代码的测试。本文所述的方法对SpringBoot项目依然适用。
文章目录
RESTful API的单元测试
GET请求的测试
GET请求一般会在请求的路径中包含查询条件,一个典型的API如下:
@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
@ResponseBody
public ResponseMessage<?> getUser(@PathVariable("id") String id) {
User user = new User();
user.setName("test");
return new ResponseMessage(user);
}
测试时只需要在路径中传递id
的值即可,不需要再写id=xxx,如下所示:
this.mockMvc.perform(get("/user/123"))
.andExpect(status().isOk());
GET API中有HttpServletRequest的测试
RESTful API能否被访问到取决于映射参数是否正确,和API本身的参数无关。Mockito
中,在执行MockMvc.perform()
时会自动创建MockHttpServletRequest
并传递给目标API,因此只需要使用.param()
方法添加参数即可。参数写在HttpServletRequest
中和用@RequestParam
标记没有本质区别,只不过后者可以设定该参数为必选项。
@RequestMapping(value = "/list/{username}", method = RequestMethod.GET)
@ResponseBody
public ResponseMessage<?> list(@PathVariable("username") String username, @RequestParam int pageNumber, @RequestParam int pageSize, HttpServletRequest request) {
String noticeId = request.getParameter("omNoticeId");
...
}
perform(restfulGetBuilder("/list/admin")
.param("pageNumber", "1").param("pageSize", "10").param("omNoticeId", "omNoticeId"))
.statusOk().resultSuccess().total(1);
请求中包含@RequestBody的测试
如果接受请求的API中不含@RequestBody
参数,并且在请求路径中传递参数,那么测试方法和上述GET请求的测试没有大的区别。下面介绍的是含有@RequestBody
的这种特殊情况,典型API如下:
@RequestMapping(value = "/user", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseMessage<?> updateUser(@RequestBody User user) {
userService.updateUser(user);
return new ResponseMessage(user);
}
从API的请求参数中可以看出,该API需要传入JSON字符串,因此测试时需要把User转换成JSON格式传给该API。关键在于JSON字符串放在了请求体中,而不是当作参数传递,如下所示:
User user = new User();
user.setName("test");
this.mockMvc.perform(put("/user"))
.contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(user))
.andExpect(status().isOk());
请求中包含@RequestParam的自定义类型
一个典型的接口定义如下所示:
@RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> create(@RequestParam String userStr, UriComponentsBuilder uriBuilder) {
User user = (User) JSONHelper.json2Object(userStr, User.class);
// do other things
}
上述代码需要注意的是,wmInQmIstr
实际上是一个自定义类型的JSON字符串,所以在发送请求的时候需要***将自定义类型的内容当作参数传递***,如下所示:
User user = new User();
user.setName("bruce");
this.mockMvc.perform(post("")..contentType(MediaType.APPLICATION_JSON)
.param("userStr", JSONObject.toJSONString(user)))
.andExpect(status().isOk());
测试含有复杂自定义类型的接口
复杂自定义类型包括含有自定义类型的List、数组、Map或这些类型的包装类。这种情况下,数据一定是通过JSON格式传递给接口,SpringMVC对JSON格式的数据进行解析。因此,接口需要显示指定为@RequestBody
,如下所示:
//UserController.java
@RequestMapping("userController")
public UserController {
@RequestMapping(params = "updateUsers")
public void updateUsers(@RequestBody List<User> users) {
// update users.
}
@RequestMapping(params = "saveUsers")
public void saveUsers(@RequestBody UserList users) {
// update users.
}
}
//UserList.java
public class UserList {
private List<User> users;
// getter and setter
}
Mockito中在测试该接口时需要指定ContentType
为APPLICATION_JSON
,同时将参数通过body
传递,如下所示:
@Test
public void testSaveUsers() throws Exception {
User user = new User();
user.setName("bruce");
List<User> userList = new ArrayList<>();
userList.add(user);
String jsonStr = JSONObject.toJSONString(userList);
this.mockMvc.perform(get("/userController").param("saveUsers", ""))
.contentType(MediaType.APPLICATION_JSON).content(jsonStr);
}
非RESTful API的单元测试
GET请求测试
GET请求是通过URL向服务器传递访问链接的。正常情况下,在访问链接中传递路径参数即可,比如:
http://localhost:8080/UserController.do?list
其中,UserController
定义如下:
public class UserController {
@RequestMapping(params = "list")
public void list(HttpServletRequest req) {
//do something here
}
}
对应的单元测试代码如下:
this.mockMvc.perform(get("/UserController.do?list"));
但有些时候,我们测试的代码中存在两个访问路径相同的API,其中一个是RESTful API,这时如果还按照上述代码,perform
方法将映射到一个错误的路径,从而无法进行测试。下面看一下这种情况的代码:
public class UserController {
@RequestMapping(params = "list")
public void list(HttpServletRequest req) {
//do something here
}
@RequestMapping(method = "GET")
public List<User> list() {
//do something here
}
}
这个时候如果想对上述代码进行测试就不能在访问路径中直接填写方法路径,否则会被当成一个RESTful的GET请求,请求参数为list,从而映射到错误的API。但是MockMvc提供了一个param方法,使用该方法添加list参数则会映射到@RequestMapping(params = "list")
方法上,如下所示:
this.mockMvc.perform(get("/UserController.do")).param("list", "");
如果想查看两种访问方式的区别可以查看MockMvc
类中的perform
方法源码,其中MockFilterChain
中给出了映射器的详细信息。
测试含有自定义类型参数的接口
比如addUser接口中有个自定义的User类,用于接受User的详细信息,这种类型的接口数据是通过表单得到的。在测试时,如果需要为自定义类赋值,那么只需要通过属性传递对应的参数即可,例如:
@RequestMapping(params = "addUser")
public void addUser(User user) {
//do add operation
}
//User.java
public class User {
private String userId;
}
this.mockmvc.perform(get("/addUser").param("userId", "test01"));
测试复杂类型的接口
上述RESTful API中复杂接口的测试主要依赖@RequestBody,但是在前后端不分离的项目中,前台主要通过JS向后台发送请求,而JS很难将类中复杂关系用JSON表示,比如有下述接口:
@RequestMapping(params = "saveRows")
@ResponseBody
public AjaxJson saveRows(Delrowpage page) {
}
其中Delrowpage定义如下:
public class Delrowpage {
private List<WmToDownGoodsEntity> downRows;
public List<WmToDownGoodsEntity> getDownrows() {
return downRows;
}
public void setDownrows(List<WmToDownGoodsEntity> downRows) {
this.downRows = downRows;
}
}
这时如果想测试saveRows
接口就不能通过传递JSON字符串,而要通过参数形式,如下所示:
@Test
public void testSaveRows() throw Exception() {
this.mockMvc.perform(post("").param("saveRows")
.param("downRows[0].id", "id001")
.param("downRows[0].goodsName", "name"));
}
其他
验证方法的调用次数
有些时候我们希望知道代码的哪个部分被执行了,往往通过判断一个方法是否被调用即可实现。比如如果想判断上述saveOrUpdate
是否被执行,可用如下方式:
verify(saveOrUpdate, times(1)).saveOrUpdate(entity);
测试只有泛型参数的方法
在CRUD应用中,泛型的操作CRUD方法使用非常频繁。如果Mockito使用不合适,就无法对这些方法进行模拟。比如有如下方法:
public interface SystemService {
<T> void saveOrUpdate(T entity);
}
Mockito在模拟该方法时,如果使用如下的调用方式doNothing.when(systemService.saveOrUpdate(any(Entity.class));
会出现编译时错误:
no instance of type variable t exists so that void conforms to t
出现这种错误的原因是JVM在编译泛型参数时会进行泛型擦除(详见Java编程思想),所以Mockito并不知道saveOrUpdate
中传入的到底是什么类型的参数。正确的调用方法为:
doNothing().when(systemService).saveOrUpdate(entity);
测试MultipartHttpServletRequest
单元测试无法测该类型的接口,只能通过集成测试。对于Multipart类型的请求需要注册MultipartResolver
。集成测试时使用的WebApplicationContext
是通过ComponentScan获取的,所以配置在spring-mvc.xml中的组件会被加载,但是单元测试时使用的WebApplicationContext
是通过StandaloneMockMvcBuilder
创建的,该类的initWebAppContext
方法会创建一个StubWebApplicationContext
。从该类的JAVADOC可知,该类不会自动注册bean,而且也没有提供外部接口注入MultipartResolver
,所以单元测试也就无法处理该类请求。
模拟Session
MockMvc
创建时自带Session
,只不过Attribute
中没有值。测试时如果需要通过Session
添加指定特性,可以通过以下方式:
User user = new User();
user.setId(1);
this.mockMvc.perform(get("/url").sessionAttr("user", user));