前言
Aliware
那些年,为了学分,我们学会了面向过程编程;
那些年,为了就业,我们学会了面向对象编程;
那些年,为了生活,我们学会了面向工资编程;
那些年,为了升职加薪,我们学会了面向领导编程;
那些年,为了完成指标,我们学会了面向指标编程;
……
那些年,我们学会了敷衍地编程;
那些年,我们编程只是为了敷衍。
单元测试简介
Aliware
1.1. 单元测试概念
在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。
1.2. 单元测试案例
1.2.1. 服务代码案例
@Servicepublic class UserService {/** 定义依赖对象 *//** 用户DAO */@Autowiredprivate UserDAO userDAO;/*** 查询用户** @param companyId 公司标识* @param startIndex 开始序号* @param pageSize 分页大小* @return 用户分页数据*/public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {// 查询用户数据// 查询用户数据: 总共数量Long totalSize = userDAO.countByCompany(companyId);// 查询接口数据: 数据列表List<UserVO> dataList = null;if (NumberHelper.isPositive(totalSize)) {dataList = userDAO.queryByCompany(companyId, startIndex, pageSize);}// 返回分页数据return new PageDataVO<>(totalSize, dataList);}}
1.2.2. 集成测试用例
@Slf4j@RunWith(PandoraBootRunner.class)@DelegateTo(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = {ExampleApplication.class})public class UserServiceTest {/** 用户服务 */@Autowiredprivate UserService userService;/*** 测试: 查询用户*/@Testpublic void testQueryUser() {Long companyId = 123L;Long startIndex = 90L;Integer pageSize = 10;PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);log.info("testQueryUser: pageData={}", JSON.toJSONString(pageData));}}
依赖外部环境和数据; 需要启动应用并初始化测试对象; 直接使用@Autowired注入测试对象; 有时候无法验证不确定的返回值,只能靠打印日志来人工核对。
1.2.3. 单元测试用例
@Slf4j@RunWith(MockitoJUnitRunner.class)public class UserServiceTest {/** 定义静态常量 *//** 资源路径 */private static final String RESOURCE_PATH = "testUserService/";/** 模拟依赖对象 *//** 用户DAO */@Mockprivate UserDAO userDAO;/** 定义测试对象 *//** 用户服务 */@InjectMocksprivate UserService userService;/*** 测试: 查询用户-无数据*/@Testpublic void testQueryUserWithoutData() {// 模拟依赖方法// 模拟依赖方法: userDAO.countByCompanyLong companyId = 123L;Long startIndex = 90L;Integer pageSize = 10;Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);// 调用测试方法String path = RESOURCE_PATH + "testQueryUserWithoutData/";PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);String text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");Assert.assertEquals("分页数据不一致", text, JSON.toJSONString(pageData));// 验证依赖方法// 验证依赖方法: userDAO.countByCompanyMockito.verify(userDAO).countByCompany(companyId);// 验证依赖对象Mockito.verifyNoMoreInteractions(userDAO);}/*** 测试: 查询用户-有数据*/@Testpublic void testQueryUserWithData() {// 模拟依赖方法String path = RESOURCE_PATH + "testQueryUserWithData/";// 模拟依赖方法: userDAO.countByCompanyLong companyId = 123L;Mockito.doReturn(91L).when(userDAO).countByCompany(companyId);// 模拟依赖方法: userDAO.queryByCompanyLong startIndex = 90L;Integer pageSize = 10;String text = ResourceHelper.getResourceAsString(getClass(), path + "dataList.json");List<UserVO> dataList = JSON.parseArray(text, UserVO.class);Mockito.doReturn(dataList).when(userDAO).queryByCompany(companyId, startIndex, pageSize);// 调用测试方法PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");Assert.assertEquals("分页数据不一致", text, JSON.toJSONString(pageData));// 验证依赖方法// 验证依赖方法: userDAO.countByCompanyMockito.verify(userDAO).countByCompany(companyId);// 验证依赖方法: userDAO.queryByCompanyMockito.verify(userDAO).queryByCompany(companyId, startIndex, pageSize);// 验证依赖对象Mockito.verifyNoMoreInteractions(userDAO);}}
不依赖外部环境和数据;
不需要启动应用和初始化对象;
需要用@Mock来初始化依赖对象,用@InjectMocks来初始化测试对象;
需要自己模拟依赖方法,指定什么参数返回什么值或异常;
因为测试方法返回值确定,可以直接用Assert相关方法进行断言;
可以验证依赖方法的调用次数和参数值,还可以验证依赖对象的方法调用是否验证完毕。
1.3. 单元测试原则
1.3.1. AIR原则
1.3.2. FIRST原则
1.3.3. ASCII原则
1.3.4. 对比集测和单测

集成测试基本上不一定满足所有单元测试原则; 单元测试基本上一定都满足所有单元测试原则。
所以,根据这些单元测试原则,可以看出集成测试具有很大的不确定性,不能也不可能完全代替单元测试。另外,集成测试始终是集成测试,即便用于代替单元测试也还是集成测试,比如:利用H2内存数据库测试DAO方法。
无效单元测试
Aliware
2.1. 单元测试覆盖率
代码覆盖(Code Coverage)是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。
行覆盖(Line Coverage): 用于度量被测代码中每一行执行语句是否都被测试到了。 分支覆盖(Branch Coverage): 用于度量被测代码中每一个代码分支是否都被测试到了。
条件覆盖(Condition Coverage): 用于度量被测代码的条件中每一个子表达式(true和false)是否都被测试到了。
路径覆盖(Path Coverage): 用于度量被测代码中的每一个代码分支组合是否都被测试到了。
public static byte combine(boolean b0, boolean b1) {byte b = 0;if (b0) {b |= 0b01;}if (b1) {b |= 0b10;}return b;}

2.2. 单元测试编写流程

2.2.1. 定义对象阶段

2.2.2. 模拟方法阶段

2.2.3. 调用方法阶段

2.2.4. 验证方法阶段

2.3. 是否可以偷工减料

2.4. 最终可以得出结论
通过上表格,可以得出结论,偷工减料主要集中在验证阶段:
调用方法阶段
验证数据对象(返回值和异常) 验证方法阶段
验证依赖方法
验证数据对象(参数)
验证依赖对象
验证数据对象(包括属性、参数和返回值); 验证抛出异常; 验证依赖方法(包括依赖方法和依赖对象)。
验证数据对象
Aliware
3.1. 数据对象来源方式
3.1.1. 来源于被测方法的返回值
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);3.1.2. 来源于依赖方法的参数捕获
ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO).create(userCreateCaptor.capture());UserDO userCreate = userCreateCaptor.getValue();3.1.3. 来源于被测对象的属性值
userService.loadRoleMap();Map<Long, String> roleMap = Whitebox.getInternalState(userService, "roleMap");3.1.4. 来源于请求参数的属性值
OrderContext orderContext = new OrderContext();orderContext.setOrderId(12345L);orderService.supplyProducts(orderContext);List<ProductDO> productList = orderContext.getProductList();
3.2. 数据对象验证方式
3.2.1. 验证数据对象空值
// 1. 验证数据对象为空Assert.assertNull("用户标识必须为空", userId);// 2. 验证数据对象非空Assert.assertNotNull("用户标识不能为空", userId);
3.2.2. 验证数据对象布尔值
// 1. 验证数据对象为真Assert.assertTrue("返回值必须为真", NumberHelper.isPositive(1));// 2. 验证数据对象为假Assert.assertFalse("返回值必须为假", NumberHelper.isPositive(-1));
3.2.3. 验证数据对象引用
// 1. 验证数据对象一致Assert.assertSame("用户必须一致", expectedUser, actualUser);// 2. 验证数据对象不一致Assert.assertNotSame("用户不能一致", expectedUser, actualUser);
3.2.4. 验证数据对象取值
// 1. 验证简单数据对象Assert.assertNotEquals("用户名称不一致", "admin", userName);Assert.assertEquals("账户金额不一致", 10000.0D, accountAmount, 1E-6D);// 2. 验证简单集合对象Assert.assertArrayEquals("用户标识列表不一致", new Long[] {1L, 2L, 3L}, userIds);Assert.assertEquals("用户标识列表不一致", Arrays.asList(1L, 2L, 3L), userIdList);// 3. 验证复杂数据对象Assert.assertEquals("用户标识不一致", Long.valueOf(1L), user.getId());Assert.assertEquals("用户名称不一致", "admin", user.getName());...// 4. 验证复杂集合对象Assert.assertEquals("用户列表长度不一致", expectedUserList.size(), actualUserList.size());UserDO[] expectedUsers = expectedUserList.toArray(new UserDO[0]);UserDO[] actualUsers = actualUserList.toArray(new UserDO[0]);for (int i = 0; i < actualUsers.length; i++) {Assert.assertEquals(String.format("用户 (%s) 标识不一致", i), expectedUsers[i].getId(), actualUsers[i].getId());Assert.assertEquals(String.format("用户 (%s) 名称不一致", i), expectedUsers[i].getName(), actualUsers[i].getName());...};// 5. 通过序列化验证数据对象String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");Assert.assertEquals("用户列表不一致", text, JSON.toJSONString(userList));;// 6. 验证数据对象私有属性字段Assert.assertEquals("基础包不一致", "com.alibaba.example", Whitebox.getInternalState(configurer, "basePackage"));
3.3. 验证数据对象问题
public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {// 查询用户数据// 查询用户数据: 总共数量Long totalSize = userDAO.countByCompany(companyId);// 查询接口数据: 数据列表List<UserVO> dataList = null;if (NumberHelper.isPositive(totalSize)) {List<UserDO> userList = userDAO.queryByCompany(companyId, startIndex, pageSize);dataList = userList.stream().map(UserService::convertUser).collect(Collectors.toList());}// 返回分页数据return new PageDataVO<>(totalSize, dataList);}private static UserVO convertUser(UserDO userDO) {UserVO userVO = new UserVO();userVO.setId(userDO.getId());userVO.setName(userDO.getName());userVO.setDesc(userDO.getDesc());...return userVO;}
3.3.1. 不验证数据对象
// 调用测试方法userService.queryUser(companyId, startIndex, pageSize);
存在问题:
// 返回分页数据return null;
3.3.2. 验证数据对象非空
// 调用测试方法PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);Assert.assertNotNull("分页数据不为空", pageData);
存在问题:
// 返回分页数据return new PageDataVO<>();
3.3.3. 验证数据对象部分属性
// 调用测试方法PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);Assert.assertEquals("数据总量不为空", totalSize, pageData.getTotalSize());
// 返回分页数据return new PageDataVO<>(totalSize, null);
3.3.4. 验证数据对象全部属性
// 调用测试方法PageDataVO<UserVO> pageData = userService.queryUser(companyId);Assert.assertEquals("数据总量不为空", totalSize, pageData.getTotalSize());Assert.assertEquals("数据列表不为空", dataList, pageData.getDataList());存在问题:
上面的代码看起来很完美,验证了PageDataVO中两个属性值totalSize和dataList。但是,如果有一天在PageDataVO中添加了startIndex和pageSize,就无法验证这两个新属性是否赋值正确。代码如下:
// 返回分页数据return new PageDataVO<>(startIndex, pageSize, totalSize, dataList);
3.3.5. 完美地验证数据对象
// 调用测试方法PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");Assert.assertEquals("分页数据不一致", text, JSON.toJSONString(pageData));
3.4. 模拟数据对象准则
3.4.1. 除触发条件分支外,模拟对象所有属性值不能为空
private static UserVO convertUser(UserDO userDO) {UserVO userVO = new UserVO();userVO.setId(userDO.getId());userVO.setName(userDO.getDesc());userVO.setDesc(userDO.getName());...return userVO;}
3.4.2. 新增数据类属性字段时,必须模拟数据对象的属性值
userVO.setAge(userDO.getAge());
3.5. 验证数据对象准则
3.5.1. 必须验证所有数据对象
来源于被测方法的返回值 来源于依赖方法的参数捕获 来源于被测对象的属性值 来源于请求参数的属性值。
3.5.2. 必须使用明确语义的断言
Assert.assertTrue("返回值不为真", NumberHelper.isPositive(1));Assert.assertEquals("用户不一致", user, userService.getUser(userId));
Assert.assertNotNull("用户不能为空", userService.getUser(userId));Assert.assertNotEquals("用户不能一致", user, userService.getUser(userId));
Assert.assertTrue("用户不能为空", Objects.nonNull(userService.getUser(userId)));3.5.3. 尽量采用整体验证方式
UserVO user = userService.getUser(userId);String text = ResourceHelper.getResourceAsString(getClass(), path + "user.json");Assert.assertEquals("用户不一致", text, JSON.toJSONString(user));
反例:
UserVO user = userService.getUser(userId);Assert.assertEquals("用户标识不一致", Long.valueOf(123L), user.getId());Assert.assertEquals("用户名称不一致", "changyi", user.getName());...
验证抛出异常
Aliware
4.1. 抛出异常来源方式
4.1.1. 来源于属性字段的判断
private Map<String, MessageHandler> messageHandlerMap = ...;public void handleMessage(Message message) {...// 判断处理器映射非空if (CollectionUtils.isEmpty(messageHandlerMap)) {throw new ExampleException("消息处理器映射不能为空");}...}
4.1.2. 来源于输入参数的判断
public void handleMessage(Message message) {...// 判断获取处理器非空MessageHandler messageHandler = messageHandlerMap.get(message.getType());if (CollectionUtils.isEmpty(messageHandler)) {throw new ExampleException("获取消息处理器不能为空");}...}
4.1.3. 来源于返回值的判断
public void handleMessage(Message message) {...// 进行消息处理器处理boolean result = messageHandler.handleMessage(message);if (!reuslt) {throw new ExampleException("处理消息异常");}...}
4.1.4.来源于模拟方法的调用
public void handleMessage(Message message) {...// 进行消息处理器处理boolean result = messageHandler.handleMessage(message); // 直接抛出异常...}
4.1.5. 来源于静态方法的调用
// 可能会抛出IOExceptionString response = HttpHelper.httpGet(url, parameterMap);
4.2. 抛出异常验证方式
4.2.1. 通过try-catch语句验证抛出异常
@Testpublic void testCreateUserWithException() {// 模拟依赖方法Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());// 调用测试方法UserCreateVO userCreate = new UserCreateVO();try {userCreate.setName("changyi");userCreate.setDescription("Java Programmer");userService.createUser(userCreate);} catch (ExampleException e) {Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, e.getCode());Assert.assertEquals("异常消息不一致", "用户已存在", e.getMessage());}// 验证依赖方法Mockito.verify(userDAO).existName(userCreate.getName());}
4.2.2. 通过@Test注解验证抛出异常
@Test(expected = ExampleException.class)public void testCreateUserWithException() {// 模拟依赖方法Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());// 调用测试方法UserCreateVO userCreate = new UserCreateVO();userCreate.setName("changyi");userCreate.setDescription("Java Programmer");userService.createUser(userCreate);// 验证依赖方法(不会执行)Mockito.verify(userDAO).existName(userCreate.getName());}
4.2.3. 通过@Rule注解验证抛出异常
@Rulepublic ExpectedException exception = ExpectedException.none();@Testpublic void testCreateUserWithException1() {// 模拟依赖方法Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());// 调用测试方法UserCreateVO userCreate = new UserCreateVO();userCreate.setName("changyi");userCreate.setDescription("Java Programmer");exception.expect(ExampleException.class);exception.expectMessage("用户已存在");userService.createUser(userCreate);// 验证依赖方法(不会执行)Mockito.verify(userDAO).existName(userCreate.getName());}
4.2.4. 通过Assert.assertThrows方法验证抛出异常
@Testpublic void testCreateUserWithException() {// 模拟依赖方法Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());// 调用测试方法UserCreateVO userCreate = new UserCreateVO();userCreate.setName("changyi");userCreate.setDescription("Java Programmer");ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreate));Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, exception.getCode());Assert.assertEquals("异常消息不一致", "用户已存在", exception.getMessage());// 验证依赖方法Mockito.verify(userDAO).existName(userCreate.getName());}
4.2.5. 四种抛出异常验证方式对比

4.3. 验证抛出异常问题
private UserDAO userDAO;public void createUser(@Valid UserCreateVO userCreateVO) {try {UserDO userCreateDO = new UserDO();userCreateDO.setName(userCreateVO.getName());userCreateDO.setDesc(userCreateVO.getDesc());userDAO.create(userCreateDO);} catch (RuntimeException e) {log.error("创建用户异常: userName={}", userName, e)throw new ExampleException(ErrorCode.DATABASE_ERROR, "创建用户异常", e);}}
4.3.1. 不验证抛出异常类型
在验证抛出异常时,很多人使用@Test注解的expected属性,并且指定取值为Exception.class,主要原因是:
单元测试用例的代码简洁,只有一行@Test注解;
不管抛出什么异常,都能保证单元测试用例通过。
@Test(expected = Exception.class)public void testCreateUserWithException() {// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法UserCreateVO userCreateVO = ...;userService.createUser(userCreate);}
throw new RuntimeException("创建用户异常", e);
4.3.2. 不验证抛出异常属性
@Test(expected = ExampleException.class)public void testCreateUserWithException() {// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法UserCreateVO userCreateVO = ...;userService.createUser(userCreate);}
存在问题:
throw new ExampleException(ErrorCode.PARAMETER_ERROR, "创建用户异常", e);
4.3.3. 只验证抛出异常部分属性
// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法UserCreateVO userCreateVO = ...;ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreateVO));Assert.assertEquals("异常编码不一致", ErrorCode.DATABASE_ERROR, exception.getCode());
存在问题:
throw new ExampleException(ErrorCode.DATABASE_ERROR, "创建用户错误", e);
4.3.4. 不验证抛出异常原因
// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法UserCreateVO userCreateVO = ...;ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreateVO));Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, exception.getCode());Assert.assertEquals("异常消息不一致", “创建用户异常”, exception.getMessage());
存在问题:
throw new ExampleException(ErrorCode.DATABASE_ERROR, "创建用户异常");
4.3.5. 不验证相关方法调用
// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法UserCreateVO userCreateVO = ...;ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreateVO));Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, exception.getCode());Assert.assertEquals("异常消息不一致", “创建用户异常”, exception.getMessage());Assert.assertEquals("异常原因不一致", e, exception.getCause());
存在问题:
// 检查用户名称有效String userName = userCreateVO.getName();if (StringUtils.length(userName) < USER_NAME_LENGTH) {throw new ExampleException(ErrorCode.INVALID_USERNAME, "无效用户名称");}
4.3.6. 完美地验证抛出异常
// 模拟依赖方法Throwable e = new RuntimeException();Mockito.doThrow(e).when(userDAO).create(Mockito.any(UserCreateVO.class));// 调用测试方法String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateVO.json");UserCreateVO userCreateVO = JSON.parseObject(text, UserCreateVO.class);ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreateVO));Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, exception.getCode());Assert.assertEquals("异常消息不一致", “创建用户异常”, exception.getMessage());Assert.assertEquals("异常原因不一致", e, exception.getCause());// 验证依赖方法ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO).create(userCreateCaptor.capture());text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateDO.json");Assert.assertEquals("用户创建不一致", text, JSON.toJSONString(userCreateCaptor.getValue()));
4.4.1. 必须验证所有抛出异常
来源于属性字段的判断
来源于输入参数的判断
来源于返回值的判断
来源于模拟方法的调用
来源于静态方法的调用
4.4.2. 必须验证异常类型、异常属性、异常原因
ExampleException exception = Assert.assertThrows("异常类型不一致", ExampleException.class, () -> userService.createUser(userCreateVO));Assert.assertEquals("异常编码不一致", ErrorCode.OBJECT_EXIST, exception.getCode());Assert.assertEquals("异常消息不一致", "用户已存在", exception.getMessage());Assert.assertEquals("异常原因不一致", e, exception.getCause());
反例:
@Test(expected = ExampleException.class)public void testCreateUserWithException() {...userService.createUser(userCreateVO);}
4.4.3. 验证抛出异常后,必须验证相关方法调用
// 调用测试方法...// 验证依赖方法ArgumentCaptor<UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO).create(userCreateCaptor.capture());text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateDO.json");Assert.assertEquals("用户创建不一致", text, JSON.toJSONString(userCreateCaptor.getValue()));
验证方法调用
Aliware
5.1. 方法调用来源方式
5.1.1. 来源于注入对象的方法调用
private UserDAO userDAO;public UserVO getUser(Long userId) {UserDO user = userDAO.get(userId); // 方法调用return convertUser(user);}
5.1.2. 来源于输入参数的方法调用
public <T> List<T> executeQuery(String sql, DataParser<T> dataParser) {List<T> dataList = new ArrayList<>();List<Record> recordList = SQLTask.getResult(sql);for (Record record : recordList) {T data = dataParser.parse(record); // 方法调用if (Objects.nonNull(data)) {dataList.add(data);}}return dataList;}5.1.3. 来源于返回值的方法调用
private UserHsfService userHsfService;public User getUser(Long userId) {Result<User> result = userHsfService.getUser(userId);if (!result.isSuccess()) { // 方法调用1throw new ExampleException("获取用户异常");}return result.getData(); // 方法调用2}5.1.4. 来源于静态方法的调用
String text = JSON.toJSONString(user); // 方法调用
5.2.1. 验证依赖方法的调用参数
// 1.验证无参数依赖方法调用Mockito.verify(userDAO).deleteAll();// 2.验证指定参数依赖方法调用Mockito.verify(userDAO).delete(userId);// 3.验证任意参数依赖方法调用Mockito.verify(userDAO).delete(Mockito.anyLong());// 4.验证可空参数依赖方法调用Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));// 5.验证必空参数依赖方法调用Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());// 6.验证可变参数依赖方法调用Mockito.verify(userService).delete(1L, 2L, 3L);Mockito.verify(userService).delete(Mockito.any(Long.class)); // 匹配一个Mockito.verify(userService).delete(Mockito.<Long>any()); // 匹配多个
5.2.2. 验证依赖方法的调用次数
// 1.验证依赖方法默认调用1次Mockito.verify(userDAO).delete(userId);// 2.验证依赖方法从不调用Mockito.verify(userDAO, Mockito.never()).delete(userId);// 3.验证依赖方法调用n次Mockito.verify(userDAO, Mockito.times(n)).delete(userId);// 4.验证依赖方法调用至少1次Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);// 5.验证依赖方法调用至少n次Mockito.verify(userDAO, Mockito.atLeast(n)).delete(userId);// 6.验证依赖方法调用最多1次Mockito.verify(userDAO, Mockito.atMostOnce()).delete(userId);// 7.验证依赖方法调用最多n次Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);// 8.验证依赖方法调用指定n次Mockito.verify(userDAO, Mockito.call(n)).delete(userId); // 不会被标记为已验证// 9.验证依赖对象及其方法仅调用1次Mockito.verify(userDAO, Mockito.only()).delete(userId);
5.2.3. 验证依赖方法并捕获参数值
// 1.使用ArgumentCaptor.forClass方法定义参数捕获器ArgumentCaptor<UserDO> userCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO).modify(userCaptor.capture());UserDO user = userCaptor.getValue();// 2.使用@Captor注解定义参数捕获器@Captorprivate ArgumentCaptor<UserDO> userCaptor;// 3.捕获多次方法调用的参数值列表ArgumentCaptor<UserDO> userCaptor = ArgumentCaptor.forClass(UserDO.class);Mockito.verify(userDAO, Mockito.atLeastOnce()).modify(userCaptor.capture());List<UserDO> userList = userCaptor.getAllValues();
5.2.4. 验证其它类型的依赖方法调用
// 1.验证 final 方法调用final方法的验证跟普通方法类似。// 2.验证私有方法调用PowerMockito.verifyPrivate(mockClass, times(1)).invoke("unload", any(List.class));// 3.验证构造方法调用PowerMockito.verifyNew(MockClass.class).withNoArguments();PowerMockito.verifyNew(MockClass.class).withArguments(someArgs);// 4.验证静态方法调用PowerMockito.verifyStatic(StringUtils.class);StringUtils.isEmpty(string);
5.2.5. 验证依赖对象没有更多方法调用
// 1.验证模拟对象没有任何方法调用Mockito.verifyNoInteractions(idGenerator, userDAO);// 2.验证模拟对象没有更多方法调用Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
5.3. 验证依赖方法问题
private UserCache userCache;public boolean cacheUser(List<User> userList) {boolean result = true;for (User user : userList) {result = result && userCache.set(user.getId(), user);}return result;}
5.3.1. 不验证依赖方法调用
// 模拟依赖方法Mockito.doReturn(true).when(userCache).set(Mockito.anyLong(), Mockito.any(User.class));// 调用测试方法List<User> userList = ...;Assert.assertTrue("处理结果不为真", userService.cacheUser(userList));// 不验证依赖方法
存在问题:
// 清除用户列表userList = Collections.emptyList();
5.3.2. 不验证依赖方法调用次数
// 验证依赖方法Mockito.verify(userCache, Mockito.atLeastOnce()).set(Mockito.anyLong(), Mockito.any(User.class));
// 写了两次缓存result = result && userCache.set(user.getId(), user);result = result && userCache.set(user.getId(), user);
5.3.3. 不验证依赖方法调用参数
// 验证依赖方法Mockito.verify(userCache, Mockito.times(userList.size())).set(Mockito.anyLong(), Mockito.any(User.class));存在问题:
User user = userList.get(0);for (int i = 0; i < userList.size(); i++) {result = result && userCache.set(user.getId(), user);}
5.3.4. 不验证所有依赖方法调用
Mockito.verify(userCache).set(user1.getId(), user1);Mockito.verify(userCache).set(user2.getId(), user2);
// 缓存最后一个用户User user = userList.get(userList.size() - 1);userCache.set(user.getId(), user);
5.3.5. 验证所有依赖方法调用
for (User user : userList) {Mockito.verify(userCache).set(user.getId(), user);}
// 删除所有用户缓存userCache.clearAll();
5.3.6. 完美地验证依赖方法调用
// 验证依赖方法ArgumentCaptor<Long> userIdCaptor = ArgumentCaptor.forClass(Long.class);ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);Mockito.verify(userCache, Mockito.atLeastOnce()).set(userIdCaptor.capture(), userCaptor.capture());Assert.assertEquals("用户标识列表不一致", userIdList, userIdCaptor.getAllValues());Assert.assertEquals("用户信息列表不一致", userList, userCaptor.getAllValues());// 验证依赖对象Mockito.verifyNoMoreInteractions(userCache);
5.4. 验证方法调用准则
5.4.1. 必须验证所有的模拟方法调用
来源于注入对象的方法调用
来源于输入参数的方法调用
来源于返回值的方法调用
来源于静态方法的调用
5.4.2. 必须验证所有的模拟对象没有更多方法调用
// 验证依赖对象Mockito.verifyNoMoreInteractions(userDAO, userCache);
备注:
@Afterpublic void afterTest() {Mockito.verifyNoMoreInteractions(userDAO, userCache);}
5.4.3. 必须使用明确语义的参数值或匹配器
Mockito.verify(userDAO).get(userId);Mockito.verify(userDAO).query(Mockito.eq(companyId), Mockito.isNull());
Mockito.verify(userDAO).get(Mockito.anyLong());Mockito.verify(userDAO).query(Mockito.anyLong(), Mockito.isNotNull());
后记
Aliware
《单元测试》
单元测试分真假,
工匠精神贯始终。
覆盖追求非目的,
回归验证显奇功。
一定要知道如何去分辨单元测试的真假,
一定要把工匠精神贯彻单元测试的始终。
追求单测覆盖率并不是单元测试的目的,
回归验证代码才能彰显单元测试的功效。





