本文讲述了Java教程——Java单元测试典型案例集锦!具有很好的参考价值,希望对大家有所帮助。一起跟随六星小编过来看看吧,具体如下:
阿里巴巴CTO线卓越工程小组举办了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。本人有幸作为评委,在仔细地阅读了各个小组的单元测试用例后,发现了两大单元测试问题:
1、无效验证问题:
不进行有效地验证数据对象、抛出异常和调用方法。
2、测试方法问题:
不知道如何测试某些典型案例,要么错误地测试、要么不进行测试、要么利用集成测试来保证覆盖率。比如:
错误地测试:利用测试返回节点占比来测试随机负载均衡策略;
不进行测试:没有人针对虚基类进行单独地测试;
利用集成测试:很多案例中,直接注入真实依赖对象,然后一起进行集成测试。
针对无效验证问题,在我的文章《那些年,我们写过的无效单元测试》中,介绍了如何识别和解决单元测试无效验证问题,这里就不再累述了。在本文中,作者收集了一些的Java单元测试典型案例,主要是为了解决这个测试方法问题。
一、如何测试不可达代码
在程序代码中,由于无法满足进入条件,永远都不会执行到的代码,我们称之为"不可达代码"。不可达代码的危害主要有:复杂了代码逻辑,增加了代码运行和维护成本。不可达代码是可以由单元测试检测出来的——不管如何构造单元测试用例,都无法覆盖到不可达代码。
1.1. 案例代码
在下面的案例代码中,就存在一段不可达代码。
/**
* 交易订单服务类
*/
@Service
public class TradeOrderService {
/** 注入依赖对象 */
/** 交易订单DAO */
@Autowired
private TradeOrderDAO tradeOrderDAO;
/**
* 查询交易订单
*
* @param orderQuery 订单查询
* @return 交易订单分页
*/
public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
// 查询交易订单
// 查询交易订单: 总共数量
Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
// 查询交易订单: 数据列表
List<TradeOrderVO> dataList = null;
if (NumberHelper.isPositive(totalSize)) {
List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
if (CollectionUtils.isNotEmpty(tradeOrderList)) {
dataList = convertTradeOrders(tradeOrderList);
}
}
// 返回分页数据
return new PageDataVO<>(totalSize, dataList);
}
/**
* 转化交易订单列表
*
* @param tradeOrderList 交易订单DO列表
* @return 交易订单VO列表
*/
private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
// 检查订单列表
if (CollectionUtils.isEmpty(tradeOrderList)) {
return Collections.emptyList();
}
// 转化订单列表
return tradeOrderList.stream().map(TradeOrderService::convertTradeOrder)
.collect(Collectors.toList());
}
/**
* 转化交易订单
*
* @param tradeOrder 交易订单DO
* @return 交易订单VO
*/
private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) {
TradeOrderVO tradeOrderVO = new TradeOrderVO();
tradeOrderVO.setId(tradeOrder.getId());
// ...
return tradeOrderVO;
}
}
由于方法convertTradeOrders(转化交易订单列表)传入的参数tradeOrderList(交易订单列表)不可能为空,所以“检查订单列表”这段代码是不可达代码。
// 检查订单列表
if (CollectionUtils.isEmpty(tradeOrderList)) {
return Collections.emptyList();
}
1.2. 方案1:删除不可达代码(推荐)
最简单的方法,就是删除方法convertTradeOrders(转化交易订单列表)中的不可达代码。
/**
* 转化交易订单列表
*
* @param tradeOrderList 交易订单DO列表
* @return 交易订单VO列表
*/
private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder)
.collect(Collectors.toList());
}
1.3. 方案2:利用不可达代码(推荐)
还有一种方法,把不可达代码利用起来,可以降低方法queryTradeOrder(查询交易订单)的代码复杂度。
/**
* 查询交易订单
*
* @param orderQuery 订单查询
* @return 交易订单分页
*/
public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
// 查询交易订单
// 查询交易订单: 总共数量
Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
// 查询交易订单: 数据列表
List<TradeOrderVO> dataList = null;
if (NumberHelper.isPositive(totalSize)) {
List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
dataList = convertTradeOrders(tradeOrderList);
}
// 返回分页数据
return new PageDataVO<>(totalSize, dataList);
}
1.4. 方案3:测试不可达代码(不推荐)
对于一些祖传代码,有些小伙伴不敢删除代码。在某些情况下,可以针对不可达代码进行单独测试。
/**
* 测试: 转化交易订单列表-交易订单列表为空
*
* @throws Exception 异常信息
*/
@Test
public void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception {
List<TradeOrderDO> tradeOrderList = null;
Assert.assertSame("交易订单列表不为空", Collections.emptyList(),
Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList));
}
二、如何测试内部的构造方法
在这次单元测试总决赛中,有一个随机负载均衡策略,需要针对Random(随机数)进行单元测试。
2.1. 代码案例
按照题目要求,编写了一个简单的随机负载均衡策略。
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
@Override
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = new Random().nextInt(totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
2.2. 方法1:直接测试法(不推荐)
有些参赛选手,不知道如何测试随机数(主要原因是因为不知道如何Mock构造方法),所以直接利用测试返回节点占比来测试随机负载均衡策略。
/**
* 随机负载均衡策略测试类
*/
@RunWith(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
@InjectMocks
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-随机
*
* @throws Exception 异常信息
*/
@Test
public void testSelectNodeWithRandom() throws Exception {
int nodeCount1 = 0;
int nodeCount2 = 0;
int nodeCount3 = 0;
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
for (int i = 0; i < 1000; i++) {
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
if (serviceNode == serverNode1) {
nodeCount1++;
} else if (serviceNode == serverNode2) {
nodeCount2++;
} else if (serviceNode == serverNode3) {
nodeCount3++;
}
}
Assert.assertEquals("节点1占比不一致", serverNode1.getWeight() / 60.0D, nodeCount1 / 1000.0D, 1E-3D);
Assert.assertEquals("节点2占比不一致", serverNode2.getWeight() / 60.0D, nodeCount2 / 1000.0D, 1E-3D);
Assert.assertEquals("节点3占比不一致", serverNode3.getWeight() / 60.0D, nodeCount3 / 1000.0D, 1E-3D);
}
}
这个测试用例主要存在3个问题:
执行时间长:被测方法需要被执行1000遍;
不一定通过:由于随机数是随机,并不一定保证比例,所以导致测试用例并不一定通过;
测试目标变更:单测测试的测试目标应该是负载均衡逻辑,现在感觉测试目标变成了Random方法。
2.3. 方法2:直接mock法(不推荐)
用过PowerMockito高级功能的,知道如何去Mock构造方法。
/**
* 随机负载均衡策略测试类
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(RandomLoadBalanceStrategy.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
@InjectMocks
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*
* @throws Exception 异常信息
*/
@Test
public void testSelectNodeWithFirstNode() throws Exception {
// 模拟依赖方法
Random random = Mockito.mock(Random.class);
Mockito.doReturn(9).when(random).nextInt(Mockito.anyInt());
PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(random);
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
Mockito.verify(random).nextInt(totalWeight);
}
}
但是,这个测试用例也存在问题:需要把RandomLoadBalanceStrategy加到@PrepareForTest注解中,导致Jacoco无法统计单元测试的覆盖率。
2.4. 方法3:工具方法法(推荐)
其实,随机数生成,还有很多工具方法,我们可以利用工具方法RandomUtils.nextInt代替构造方法。
2.4.1. 重构代码
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
@Override
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = RandomUtils.nextInt(0, totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
2.4.2. 测试用例
/**
* 随机负载均衡策略测试类
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(RandomUtils.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
@InjectMocks
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*/
@Test
public void testSelectNodeWithFirstNode() {
// 模拟依赖方法
PowerMockito.mockStatic(RandomUtils.class);
PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
PowerMockito.verifyStatic(RandomUtils.class);
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
RandomUtils.nextInt(0, totalWeight);
}
}
2.5. 方法4:注入对象法(推荐)
如果不愿意使用工具方法,也可以注入依赖对象,我们可以利用RandomProvider(随机数提供者)来代替构造方法。
2.5.1. 重构代码
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/** 注入依赖对象 */
/** 随机数提供者 */
@Autowired
private RandomProvider randomProvider;
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
@Override
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = randomProvider.nextInt(totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
2.5.2. 测试用例
/**
* 随机负载均衡策略测试类
*/
@RunWith(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest {
/** 模拟依赖方法 */
/** 随机数提供者 */
@Mock
private RandomProvider randomProvider;
/** 定义测试对象 */
/** 随机负载均衡策略 */
@InjectMocks
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*/
@Test
public void testSelectNodeWithFirstNode() {
// 模拟依赖方法
Mockito.doReturn(9).when(randomProvider).nextInt(Mockito.anyInt());
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
Mockito.verify(randomProvider).nextInt(totalWeight);
}
}
更多相关技术内容咨询欢迎前往并持续关注六星社区了解详情。
想高效系统的学习Java编程语言,推荐大家关注一个微信公众号:Java圈子。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Java入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!