Java编程技巧之单元测试用例简化方法(内含案例)
前言
一、简化模拟数据对象
1.1. 利用JSON反序列化简化数据对象赋值语句
List<UserCreateVO> userCreateList = new ArrayList<>();
UserCreateVO userCreate0 = new UserCreateVO();
userCreate0.setName("Changyi");
... // 约几十行
userCreateList.add(userCreate0);
UserCreateVO userCreate1 = new UserCreateVO();
userCreate1.setName("Tester");
... // 约几十行
userCreateList.add(userCreate1);
... // 约几十条
userService.batchCreate(userCreateList);
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
List<UserCreateVO> userCreateList = JSON.parseArray(text, UserCreateVO.class);
userService.batchCreate(userCreateList);
public ExampleResult<UserVO> getUser( Long userId) {
UserVO user = userService.getUser(userId);
return ExampleResult.success(user);
}
原始用例:
// 模拟依赖方法
String path = RESOURCE_PATH + "testGetUser/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "user.json");
UserVO user = JSON.parseObject(text, UserVO.class);
Mockito.doReturn(user).when(userService).getUser(user.getId());
// 调用测试方法
ExampleResult<UserVO> result = userController.getUser(user.getId());
Assert.assertEquals("结果编码不一致", ResultCode.SUCCESS.getCode(), result.getCode());
Assert.assertEquals("结果数据不一致", user, result.getData());
简化用例:
// 模拟依赖方法
Long userId = 12345L;
UserVO user = Mockito.mock(UserVO.class); // 也可以使用new UserVO()
Mockito.doReturn(user).when(userService).getUser(userId);
// 调用测试方法
ExampleResult<UserVO> result = userController.getUser(userId);
Assert.assertEquals("结果编码不一致", ResultCode.SUCCESS.getCode(), result.getCode());
Assert.assertSame("结果数据不一致", user, result.getData());
1.3. 利用虚拟数据对象简化参数值模拟语句
public ExampleResult<Void> createUser( UserCreateVO userCreate) {
userService.createUser(userCreate);
return ExampleResult.success();
}
原始用例:
// 调用测试方法
String path = RESOURCE_PATH + "testCreateUser/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreate.json");
UserCreateVO userCreate = JSON.parseObject(text, UserCreateVO.class);
ExampleResult<Void> result = userController.createUser(userCreate);
Assert.assertEquals("结果编码不一致", ResultCode.SUCCESS.getCode(), result.getCode());
// 验证依赖方法
Mockito.verify(userService).createUser(userCreate);
简化用例:
// 调用测试方法
UserCreateVO userCreate = Mockito.mock(UserCreateVO.class); // 也可以使用new UserCreateVO()
ExampleResult<Void> result = userController.createUser(userCreate);
Assert.assertEquals("结果编码不一致", ResultCode.SUCCESS.getCode(), result.getCode());
// 验证依赖方法
Mockito.verify(userService).createUser(userCreate);
二、简化模拟依赖方法
2.1. 利用默认返回值简化模拟依赖方法
Mockito.doReturn(false).when(userDAO).existName(userName);
Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);
Mockito.doReturn(null).when(userDAO).queryByCompany(companyId, startIndex, pageSize);
简化用例:
2.2. 利用任意匹配参数简化模拟依赖方法
// 模拟依赖方法
String path = RESOURCE_PATH + "testCreateUserWithSuccess/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");
UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);
Mockito.doReturn(false).when(userDAO).existName(userCreateVO.getName());
...
// 调用测试方法
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreateVO));
// 验证依赖方法
Mockito.verify(userDAO).existName(userCreateVO.getName());
...
简化用例:
// 模拟依赖方法
Mockito.doReturn(false).when(userDAO).existName(Mockito.anyString());
...
// 调用测试方法
String path = RESOURCE_PATH + "testCreateUserWithSuccess/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");
UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreateVO));
// 验证依赖方法
Mockito.verify(userDAO).existName(userCreateVO.getName());
...
String text = ResourceHelper.getResourceAsString(getClass(), path + "user1.json");
UserDO user1 = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(user1).when(userDAO).get(user1.getId());
text = ResourceHelper.getResourceAsString(getClass(), path + "user2.json");
UserDO user2 = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(user2).when(userDAO).get(user2.getId());
...
简化用例:
String text = ResourceHelper.getResourceAsString(getClass(), path + "userMap.json");
Map<Long, UserDO> userMap = JSON.parseObject(text, new TypeReference<Map<Long, UserDO>>() {});
Mockito.doAnswer(invocation -> userMap.get(invocation.getArgument(0))).when(userDAO).get(Mockito.anyLong());
2.4. 利用Mock参数简化模拟链式调用方法
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(MAX_AGE)
.allowedHeaders("*");
}
原始用例:
public void testAddCorsMappings() {
// 模拟依赖方法
CorsRegistry registry = Mockito.mock(CorsRegistry.class);
CorsRegistration registration = Mockito.mock(CorsRegistration.class);
Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any());
Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any());
Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean());
Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong());
Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any());
// 调用测试方法
webAuthInterceptConfig.addCorsMappings(registry);
// 验证依赖方法
Mockito.verify(registry).addMapping("/**");
Mockito.verify(registration).allowedOrigins("*");
Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
Mockito.verify(registration).allowCredentials(true);
Mockito.verify(registration).maxAge(3600L);
Mockito.verify(registration).allowedHeaders("*");
}
简化用例:
public void testAddCorsMapping() {
// 模拟依赖方法
CorsRegistry registry = Mockito.mock(CorsRegistry.class);
CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF);
Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
// 调用测试方法
webAuthInterceptConfig.addCorsMappings(registry);
// 验证依赖方法
Mockito.verify(registry).addMapping("/**");
Mockito.verify(registration).allowedOrigins("*");
Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
Mockito.verify(registration).allowCredentials(true);
Mockito.verify(registration).maxAge(3600L);
Mockito.verify(registration).allowedHeaders("*");
}
代码说明:
在mock对象时,对于自返回对象,需要指定Mockito.RETURNS_SELF参数;
在mock方法时,无需对自返回对象进行mock方法,因为框架已经mock方法返回了自身;
在verify方法时,可以像普通测试法一样优美地验证所有方法调用。
RETURNS_SELF参数:mock调用方法语句最少,适合于链式调用返回相同值;
RETURNS_DEEP_STUBS参数:mock调用方法语句较少,适合于链式调用返回不同值。
三、简化验证数据对象
3.1. 利用JSON序列化简化数据对象验证语句
List<UserVO> userList = userService.queryByCompanyId(companyId);
UserVO user0 = userList.get(0);
Assert.assertEquals("name不一致", "Changyi", user0.getName());
... // 约几十行
UserVO user1 = userList.get(1);
Assert.assertEquals("name不一致", "Tester", user1.getName());
... // 约几十行
... // 约几十条
简化用例:
List<UserVO> userList = userService.queryByCompanyId(companyId);
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
Assert.assertEquals("用户列表不一致", text, JSON.toJSONString(userList));
如果数据对象中存在Map对象,为了保证序列化后的字段顺序一致,需要添加SerializerFeature.MapSortField特征。
JSON.toJSONString(userMap, SerializerFeature.MapSortField);
如果数据对象中存在随机对象,比如时间、随机数等,需要使用过滤器过滤这些字段。
List<UserVO> userList = ...;
SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
filter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
Assert.assertEquals("用户信息不一致", text, JSON.toJSONString(user, filter));
排除单个类的属性字段:
List<UserVO> userList = ...;
SimplePropertyPreFilter filter = new SimplePropertyPreFilter(UserVO.class);
filter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
Assert.assertEquals("用户信息不一致", text, JSON.toJSONString(user, filter));
排除多个类的属性字段:
Pair<UserVO, CompanyVO> userCompanyPair = ...;
SimplePropertyPreFilter userFilter = new SimplePropertyPreFilter(UserVO.class);
userFilter.getExcludes().addAll(Arrays.asList("gmtCreate", "gmtModified"));
SimplePropertyPreFilter companyFilter = new SimplePropertyPreFilter(CompanyVO.class);
companyFilter.getExcludes().addAll(Arrays.asList("createTime", "modifyTime"));
Assert.assertEquals("用户公司对不一致", text, JSON.toJSONString(userCompanyPair, new SerializeFilter[]{userFilter, companyFilter});
3.2. 利用数据对象相等简化返回值验证语句
List<Long> userIdList = userService.getAllUserIds(companyId);
String text = JSON.toJSONString(Arrays.asList(1L, 2L, 3L));
Assert.assertEquals("用户标识列表不一致", text, JSON.toJSONString(userIdList));
简化用例:
List<Long> userIdList = userService.getAllUserIds(companyId);
Assert.assertEquals("用户标识列表不一致", Arrays.asList(1L, 2L, 3L), userIdList);
Assert.assertSame用于相同类实例验证——类实例相同;
Assert.assertEquals用于相等类实例验证——类实例相同或相等(equals为true)。
3.3. 利用数据对象相等简化参数值验证语句
ArgumentCaptor<List<Long>> userIdListCaptor = CastHelper.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchDelete(userIdListCaptor.capture());
Assert.assertEquals("用户标识列表不一致", Arrays.asList(1L, 2L, 3L), userIdListCaptor.getValue());
简化用例:
Mockito.verify(userDAO).batchDelete(Arrays.asList(1L, 2L, 3L));
注意:
四、简化验证依赖方法
4.1. 利用ArgumentCaptor简化验证依赖方法
Mockito.verify(userDAO).get(user1.getId());
Mockito.verify(userDAO).get(user2.getId());
...
简化用例:
ArgumentCaptor<Long> userIdCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(userDAO, Mockito.atLeastOnce()).get(userIdCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), path + "userIdList.json");
Assert.assertEquals("用户标识列表不一致", text, JSON.toJSONString(userIdCaptor.getAllValues()));
五、简化单元测试用例
5.1. 利用直接测试私有方法简化单元测试用例
public UserVO getUser(Long userId) {
// 获取用户信息
UserDO userDO = userDAO.get(userId);
if (Objects.isNull(userDO)) {
throw new ExampleException(ErrorCode.OBJECT_NONEXIST, "用户不存在");
}
// 返回用户信息
UserVO userVO = new UserVO();
userVO.setId(userDO.getId());
userVO.setName(userDO.getName());
userVO.setVip(isVip(userDO.getRoleIdList()));
...
return userVO;
}
private static boolean isVip(List<Long> roleIdList) {
for (Long roleId : roleIdList) {
if (VIP_ROLE_ID_SET.contains(roleId)) {
return true;
}
}
return false;
}
原始用例:
public void testGetUserWithVip() {
// 模拟依赖方法
String path = RESOURCE_PATH + "testGetUserWithVip/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "userDO.json");
UserDO userDO = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(userDO).when(userDAO).get(userDO.getId());
// 调用测试方法
UserVO userVO = userService.getUser(userDO.getId());
text = ResourceHelper.getResourceAsString(getClass(), path + "userVO.json");
Assert.assertEquals("用户信息不一致", text, JSON.toJSONString(userVO));
// 验证依赖方法
Mockito.verify(userDAO).get(userDO.getId());
}
public void testGetUserWithNotVip() {
... // 代码跟testGetUserWithVip一致, 只是测试数据不同而已
}
简化用例:
public void testGetUserWithNormal() {
... // 代码跟原testGetUserWithVip一致
}
public void testIsVipWithTrue() throws Exception {
List<Long> roleIdList = ...; // 包含VIP角色标识
Assert.assertTrue("返回值不为真", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));
}
public void testIsVipWithFalse() throws Exception {
List<Long> roleIdList = ...; // 不含VIP角色标识
Assert.assertFalse("返回值不为假", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));
}
5.2. 利用JUnit的参数化测试简化单元测试用例
"vip/", "notVip/"}) (strings = {
public void testGetUserWithNormal(String dir) {
// 模拟依赖方法
String path = RESOURCE_PATH + "testGetUserWithNormal/" + dir;
String text = ResourceHelper.getResourceAsString(getClass(), path + "userDO.json");
UserDO userDO = JSON.parseObject(text, UserDO.class);
Mockito.doReturn(userDO).when(userDAO).get(userDO.getId());
// 调用测试方法
UserVO userVO = userService.getUser(userDO.getId());
text = ResourceHelper.getResourceAsString(getClass(), path + "userVO.json");
Assert.assertEquals("用户信息不一致", text, JSON.toJSONString(userVO));
// 验证依赖方法
Mockito.verify(userDAO).get(userDO.getId());
}
如上简化用例所示:在资源目录testGetUserWithNormal中创建了两个目录vip和notVip,用于存储相同名称的JSON文件userDO.json和userVO.json,但是其文件内容根据场景又有所不同。
后记
那些年,我们写过的无效单元测试
Java工程师必读手册
工匠追求“术”到极致,其实就是在寻“道”,且离悟“道”也就不远了,亦或是已经得道,这就是“工匠精神”——一种追求“以术得道”的精神。 如果一个工匠只满足于“术”,不能追求“术”到极致去悟“道”,那只是一个靠“术”养家糊口的工匠而已。作者根据多年来的实践探索,总结了大量的Java编码之“术”,试图阐述出心中的Java编码之“道”。
点击阅读原文查看详情。
微信扫码关注该文公众号作者