简介: 近期,阿里巴巴CTO线**卓越工程小组**举办了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。
作者 | 陈昌毅(常意)
来源 | 阿里开发者公众号
近期,阿里巴巴CTO线卓越工程小组举办了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。本人有幸作为评委,在仔细地阅读了各个小组的单元测试用例后,发现了两大单元测试问题:
针对无效验证问题,在我的ATA文章《那些年,我们写过的无效单元测试》中,介绍了如何识别和解决单元测试无效验证问题,这里就不再赘述了。在本文中,作者收集了一些的Java单元测试典型案例,主要是为了解决这个测试方法问题。
在程序代码中,由于无法满足进入条件,永远都不会执行到的代码,我们称之为"不可达代码"。不可达代码的危害主要有:复杂了代码逻辑,增加了代码运行和维护成本。不可达代码是可以由单元测试检测出来的——不管如何构造单元测试用例,都无法覆盖到不可达代码。
在下面的案例代码中,就存在一段不可达代码。
/** * 交易订单服务类 */@Servicepublic 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(); }
最简单的方法,就是删除方法convertTradeOrders(转化交易订单列表)中的不可达代码。
/** * 转化交易订单列表 * * @param tradeOrderList 交易订单DO列表 * @return 交易订单VO列表 */private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) { return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder) .collect(Collectors.toList());}
还有一种方法,把不可达代码利用起来,可以降低方法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);}
对于一些祖传代码,有些小伙伴不敢删除代码。在某些情况下,可以针对不可达代码进行单独测试。
/** * 测试: 转化交易订单列表-交易订单列表为空 * * @throws Exception 异常信息 */@Testpublic void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception { List<TradeOrderDO> tradeOrderList = null; Assert.assertSame("交易订单列表不为空", Collections.emptyList(), Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList));}
在这次单元测试总决赛中,有一个随机负载均衡策略,需要针对Random(随机数)进行单元测试。
按照题目要求,编写了一个简单的随机负载均衡策略。
/** * 随机负载均衡策略类 */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; }}
有些参赛选手,不知道如何测试随机数(主要原因是因为不知道如何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个问题:
用过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无法统计单元测试的覆盖率。
其实,随机数生成,还有很多工具方法,我们可以利用工具方法RandomUtils.nextInt代替构造方法。
/** * 随机负载均衡策略类 */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; }}
/** * 随机负载均衡策略测试类 */@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); }}
如果不愿意使用工具方法,也可以注入依赖对象,我们可以利用RandomProvider(随机数提供者)来代替构造方法。
/** * 随机负载均衡策略类 */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; }}
/** * 随机负载均衡策略测试类 */@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); }}
在这次单元测试比赛中,很多选手都编写了虚基类,但是没有看到任何一个选手针对虚基类进行了单独的测试。
这里,以Diamond属性配置加载为例说明。
首先,定义一个通用的虚基类,定义了需要子类实现的虚方法,实现了通用的配置解析方法。
/** * 虚属性回调类 * * @param <T> 配置类型 */@Slf4jpublic abstract class AbstractPropertiesCallback<T> implements DiamondDataCallback { /** 注入依赖对象 */ /** 环境 */ @Autowired private Environment environment; /** 转化服务 */ @Autowired private ConversionService conversionService; /** * 接收到数据 * * @param data 配置数据 */ @Override public void received(String data) { // 获取配置参数 String configName = getConfigName(); Assert.notNull(configName, "配置名称不能为空"); T configInstance = getConfigInstance(); Assert.notNull(configInstance, "配置实例不能为空"); // 解析配置数据 try { log.info("绑定属性配置文件开始: configName={}", configName); Properties properties = new Properties(); byte[] bytes = Optional.ofNullable(data.getBytes()).orElseGet(() -> new byte[0]); InputStream inputStream = new ByteArrayInputStream(bytes); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); properties.load(bufferedReader); Bindable<T> bindable = Bindable.ofInstance(configInstance); Binder binder = new Binder(ConfigurationPropertySources.from( new PropertiesPropertySource(configName, properties)), new PropertySourcesPlaceholdersResolver(environment), conversionService); BindResult<T> result = binder.bind(configName, bindable); if (!result.isBound()) { log.error("绑定属性配置文件失败: configName={}", configName); return; } log.info("绑定属性配置文件成功: configName={}, configInstance={}", configName, JSON.toJSONString(configInstance)); } catch (IOException | RuntimeException e) { log.error("绑定属性配置文件异常: configName={}", configName, e); } } /** * 获取配置名称 * * @return 配置名称 */ @NonNull protected abstract String getConfigName(); /** * 获取配置实例 * * @return 配置实例 */ @NonNull protected abstract T getConfigInstance();}
其次,定义了具体配置的子类,简单地实现了基类定义的虚方法。
/** * 例子配置回调类 */@DiamondListener(groupId = "unittest-example", dataId = "example.properties", executeAfterInit = true)public class ExampleConfigCallback extends AbstractPropertiesCallback<ExampleConfig> { /** 注入依赖对象 */ /** 例子配置 */ @Resource private ExampleConfig exampleConfig; /** * 获取配置名称 * * @return 配置名称 */ @Override protected String getConfigName() { return "example"; } /** * 获取配置实例 * * @return 配置实例 */ @Override protected ExampleConfig getConfigInstance() { return exampleConfig; }}
最简单的测试方法,就是通过子类对虚基类进行联合测试,这样同时把子类和虚基类都测试了。
/** * 例子配置回调测试类 */@RunWith(MockitoJUnitRunner.class)public class ExampleConfigCallbackTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testExampleConfigCallback/"; /** 模拟依赖对象 */ /** 配置环境 */ @Mock private ConfigurableEnvironment environment; /** 转化服务 */ @Mock private ConversionService conversionService; /** 定义测试对象 */ /** BOSS取消费配置回调 */ @InjectMocks private ExampleConfigCallback exampleConfigCallback; /** * 测试: 接收-正常 */ @Test public void testReceivedWithNormal() { // 模拟依赖对象 ExampleConfig exampleConfig = new ExampleConfig(); Whitebox.setInternalState(exampleConfigCallback, "exampleConfig", exampleConfig); // 调用测试方法 String text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties"); exampleConfigCallback.received(text); // 验证依赖对象 text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json"); Assert.assertEquals("取消费用配置不一致", text, JSON.toJSONString(exampleConfig, SerializerFeature.MapSortField)); }}
其实,更好的方法是对虚基类和子类独立单元测试。
虚基类的单元测试,专注于虚基类的通用配置解析。
/** * 虚属性回调测试类 */@RunWith(MockitoJUnitRunner.class)public class AbstractPropertiesCallbackTest { /** 静态常量相关 */ /** 资源目录 */ private static final String RESOURCE_PATH = "testAbstractPropertiesCallback/"; /** 模拟依赖对象 */ /** 环境 */ @Mock private ConfigurableEnvironment environment; /** 转化服务 */ @Mock private ConversionService conversionService; /** 定义测试对象 */ /** 虚属性回调 */ @InjectMocks private AbstractPropertiesCallback<ExampleConfig> propertiesCallback = CastUtils.cast(Mockito.spy(AbstractPropertiesCallback.class)); /** * 测试: 接收到-正常 */ @Test public void testReceivedWithNormal() { // 模拟依赖方法 // 模拟依赖方法: propertiesCallback.getConfigName String configName = "example"; Mockito.doReturn(configName).when(propertiesCallback).getConfigName(); // 模拟依赖方法: propertiesCallback.getConfigInstance ExampleConfig configInstance = new ExampleConfig(); Mockito.doReturn(configInstance).when(propertiesCallback).getConfigInstance(); // 调用测试方法 String text1 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties"); propertiesCallback.received(text1); String text2 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json"); Assert.assertEquals("任务配置不一致", text2, JSON.toJSONString(configInstance)); // 验证依赖方法 // 验证依赖方法: propertiesCallback.received Mockito.verify(propertiesCallback).received(text1); // 验证依赖方法: propertiesCallback.getConfigName Mockito.verify(propertiesCallback).getConfigName(); // 验证依赖方法: propertiesCallback.getConfigInstance Mockito.verify(propertiesCallback).getConfigInstance(); }}
子类的单元测试,专注于对虚基类定义虚方法的实现,避免了每个子类都要针对虚基类的通用配置解析进行测试。
/** * 例子配置回调测试类 */@RunWith(MockitoJUnitRunner.class)public class ExampleConfigCallbackTest { /** 定义测试对象 */ /** BOSS取消费配置回调 */ @InjectMocks private ExampleConfigCallback exampleConfigCallback; /** * 测试: 获取配置实例 */ @Test public void testGetConfigInstance() { Assert.assertEquals("配置实例不一致", exampleConfig, exampleConfigCallback.getConfigInstance()); } /** * 测试: 获取配置名称 */ @Test public void testGetConfigName() { Assert.assertEquals("配置名称不一致", "example", exampleConfigCallback.getConfigName()); }}
在这次单元测试比赛中,很多选手都编写了策略服务类,但是没有看到任何一个选手针对策略服务类进行了单独的测试。这里,还是以负载均衡的策略服务为例说明。
首先,定义一个负载均衡策略接口。
/** * 负载均衡策略接口 */public interface LoadBalanceStrategy { /** * 支持策略类型 * * @return 策略类型 */ LoadBalanceStrategyType supportType(); /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest);}
其次,实现一个负载均衡策略服务,根据负载均衡策略类型选择对应的负载均衡策略来执行。
/** * 负载均衡服务类 */public class LoadBalanceService { /** 负载均衡策略映射 */ private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap; /** * 构造方法 * * @param strategyList 负载均衡策略列表 */ public LoadBalanceService(List<LoadBalanceStrategy> strategyList) { strategyMap = new EnumMap<>(LoadBalanceStrategyType.class); for (LoadBalanceStrategy strategy : strategyList) { strategyMap.put(strategy.supportType(), strategy); } } /** * 选择服务节点 * * @param strategyType 策略类型 * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ public ServerNode selectNode(LoadBalanceStrategyType strategyType, List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 获取负载均衡策略 LoadBalanceStrategy strategy = strategyMap.get(strategyType); if (Objects.isNull(strategy)) { throw new BusinessException("负载均衡策略不存在"); } // 执行负载均衡策略 return strategy.selectNode(serverNodeList, clientRequest); }}
最后,实现一个随机负载均衡策略实现类。
/** * 随机负载均衡策略类 */public class RandomLoadBalanceStrategy implements LoadBalanceStrategy { /** * 支持策略类型 * * @return 策略类型 */ @Override public LoadBalanceStrategyType supportType() { return LoadBalanceStrategyType.RANDOM; } /** * 选择服务节点 * * @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; }}
很多时候,策略模式是用来优化if-else代码的。所以,采用联合测试法(策略服务和策略实现同时测试),能够最大限度地利用原有的单元测试代码。
/** * 负载均衡服务测试类 */@RunWith(PowerMockRunner.class)@PrepareForTest(RandomUtils.class)public class LoadBalanceServiceTest { /** * 测试: 选择服务节点-正常 */ @Test public void testSelectNodeWithNormal() { // 模拟依赖方法 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(); RandomLoadBalanceStrategy randomLoadBalanceStrategy = new RandomLoadBalanceStrategy(); LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(randomLoadBalanceStrategy)); ServerNode serviceNode = loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest); Assert.assertEquals("服务节点不一致", serverNode1, serviceNode); // 验证依赖方法 PowerMockito.verifyStatic(RandomUtils.class); int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); RandomUtils.nextInt(0, totalWeight); }}
策略模式的联合测试法主要有以下问题:
如果我们对策略服务进行以下破坏,该单元测试并不能发现问题:
/** * 负载均衡服务类 */public class LoadBalanceService { /** 负载均衡策略映射 */ private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap; /** * 构造方法 * * @param strategyList 负载均衡策略列表 */ public LoadBalanceService(List<LoadBalanceStrategy> strategyList) { strategyMap = new EnumMap<>(LoadBalanceStrategyType.class); } /** * 选择服务节点 * * @param strategyType 策略类型 * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ public ServerNode selectNode(LoadBalanceStrategyType strategyType, List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 获取负载均衡策略 LoadBalanceStrategy strategy = strategyMap.get(strategyType); if (Objects.isNull(strategy)) { strategy = new RandomLoadBalanceStrategy(); } // 执行负载均衡策略 return strategy.selectNode(serverNodeList, clientRequest); }}
现在,先假设策略实现RandomLoadBalanceStrategy(随机负载均衡策略)不存在,直接对策略服务LoadBalanceService(负载均衡服务)独立测试,而且是分别对构造方法和selectNode(选择服务节点)方法进行独立测试。其中,测试构造方法是为了保证strategyMap构造逻辑没有问题,测试selectNode(选择服务节点)方法是为了保证选择策略逻辑没有问题。
/** * 负载均衡服务测试类 */public class LoadBalanceServiceTest { /** * 测试: 构造方法 */ @Test public void testConstructor() { // 模拟依赖方法 LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class); Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType(); // 调用测试方法 LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy)); Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap = Whitebox.getInternalState(loadBalanceService, "strategyMap"); Assert.assertEquals("策略映射大小不一致", 1, strategyMap.size()); Assert.assertEquals("策略映射对象不一致", loadBalanceStrategy, strategyMap.get(LoadBalanceStrategyType.RANDOM)); // 验证依赖方法 Mockito.verify(loadBalanceStrategy).supportType(); } /** * 测试: 选择服务节点-正常 */ @Test public void testSelectNodeWithNormal() { // 模拟依赖方法 LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class); // 模拟依赖方法: loadBalanceStrategy.supportType Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType(); // 模拟依赖方法: loadBalanceStrategy.selectNode ServerNode serverNode = Mockito.mock(ServerNode.class); Mockito.doReturn(serverNode).when(loadBalanceStrategy) .selectNode(Mockito.anyList(), Mockito.any(ClientRequest.class)); // 调用测试方法 List<ServerNode> serverNodeList = CastUtils.cast(Mockito.mock(List.class)); ClientRequest clientRequest = Mockito.mock(ClientRequest.class); LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy)); Assert.assertEquals("服务节点不一致", serverNode, loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest)); // 验证依赖方法 // 验证依赖方法: loadBalanceStrategy.supportType Mockito.verify(loadBalanceStrategy).supportType(); // 验证依赖方法: loadBalanceStrategy.selectNode Mockito.verify(loadBalanceStrategy).selectNode(serverNodeList, clientRequest); }}
其实,不只是策略模式,很多模式下都不建议联合测试,而是推荐采用独立的单元测试。因为单元测试是白盒测试——一种专注于自身代码逻辑的测试。
在有些单元测试中,Lambda表达式并不一定被执行,所以导致Lambda表达式没有被测试。
点击查看原文,获取更多福利!
https://developer.aliyun.com/article/1153151?utm_content=g_1000368183
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。