
前言
一、简化模拟数据对象
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);
@GetMapping("/get")public ExampleResult<UserVO> getUser(@RequestParam(value = "userId", required = true) 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. 利用虚拟数据对象简化参数值模拟语句
@GetMapping("/create")public ExampleResult<Void> createUser(@Valid @RequestBody 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("*");}
原始用例:
@Testpublic 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("*");}
简化用例:
@Testpublic 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;}
原始用例:
@Testpublic 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());}@Testpublic void testGetUserWithNotVip() {... // 代码跟testGetUserWithVip一致, 只是测试数据不同而已}
简化用例:
@Testpublic void testGetUserWithNormal() {... // 代码跟原testGetUserWithVip一致}@Testpublic void testIsVipWithTrue() throws Exception {List<Long> roleIdList = ...; // 包含VIP角色标识Assert.assertTrue("返回值不为真", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));}@Testpublic void testIsVipWithFalse() throws Exception {List<Long> roleIdList = ...; // 不含VIP角色标识Assert.assertFalse("返回值不为假", Whitebox.invokeMethod(UserService.class, "isVip", roleIdList));}
5.2. 利用JUnit的参数化测试简化单元测试用例
@ParameterizedTest@ValueSource(strings = { "vip/", "notVip/"})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编码之“道”。
点击阅读原文查看详情。




