那些年,我们写过的无效单元测试
前言
那些年,为了学分,我们学会了面向过程编程;
那些年,为了就业,我们学会了面向对象编程;
那些年,为了生活,我们学会了面向工资编程;
那些年,为了升职加薪,我们学会了面向领导编程;
那些年,为了完成指标,我们学会了面向指标编程;
……
那些年,我们学会了敷衍地编程;
那些年,我们编程只是为了敷衍。
一、单元测试简介
1.1. 单元测试概念
在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。
1.2. 单元测试案例
1.2.1. 服务代码案例
public class UserService {
/** 定义依赖对象 */
/** 用户DAO */
private 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. 集成测试用例
public class UserServiceTest {
/** 用户服务 */
private UserService userService;
/**
* 测试: 查询用户
*/
public 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. 单元测试用例
(MockitoJUnitRunner.class)
public class UserServiceTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testUserService/";
/** 模拟依赖对象 */
/** 用户DAO */
private UserDAO userDAO;
/** 定义测试对象 */
/** 用户服务 */
private UserService userService;
/**
* 测试: 查询用户-无数据
*/
public void testQueryUserWithoutData() {
// 模拟依赖方法
// 模拟依赖方法: userDAO.countByCompany
Long 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.countByCompany
Mockito.verify(userDAO).countByCompany(companyId);
// 验证依赖对象
Mockito.verifyNoMoreInteractions(userDAO);
}
/**
* 测试: 查询用户-有数据
*/
public void testQueryUserWithData() {
// 模拟依赖方法
String path = RESOURCE_PATH + "testQueryUserWithData/";
// 模拟依赖方法: userDAO.countByCompany
Long companyId = 123L;
Mockito.doReturn(91L).when(userDAO).countByCompany(companyId);
// 模拟依赖方法: userDAO.queryByCompany
Long 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.countByCompany
Mockito.verify(userDAO).countByCompany(companyId);
// 验证依赖方法: userDAO.queryByCompany
Mockito.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方法。
二、无效单元测试
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. 最终可以得出结论
通过上表格,可以得出结论,偷工减料主要集中在验证阶段:
调用方法阶段
验证数据对象(返回值和异常) 验证方法阶段
验证依赖方法
验证数据对象(参数)
验证依赖对象
验证数据对象(包括属性、参数和返回值); 验证抛出异常; 验证依赖方法(包括依赖方法和依赖对象)。
三、验证数据对象
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());
...
四、验证抛出异常
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. 来源于静态方法的调用
// 可能会抛出IOException
String response = HttpHelper.httpGet(url, parameterMap);
4.2. 抛出异常验证方式
4.2.1. 通过try-catch语句验证抛出异常
public 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注解验证抛出异常
(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注解验证抛出异常
public ExpectedException exception = ExpectedException.none();
public 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方法验证抛出异常
public 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注解;
不管抛出什么异常,都能保证单元测试用例通过。
(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. 不验证抛出异常属性
(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()));
五、验证方法调用
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()) { // 方法调用1
throw 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注解定义参数捕获器
private 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);
备注:
public 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());
后记
《单元测试》
单元测试分真假,
工匠精神贯始终。
覆盖追求非目的,
回归验证显奇功。
一定要知道如何去分辨单元测试的真假,
一定要把工匠精神贯彻单元测试的始终。
追求单测覆盖率并不是单元测试的目的,
回归验证代码才能彰显单元测试的功效。
微信扫码关注该文公众号作者