Java单元测试典型案例集锦

打印 上一主题 下一主题

主题 900|帖子 900|积分 2700

前言

近期,阿里巴巴CTO线卓越工程小组举行了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。本人有幸作为评委,在细致地阅读了各个小组的单元测试用例后,发现了大单元测试问题:

  • 无效验证问题:不进行有效地验证数据对象、抛出非常和调用方法。
  • 测试方法问题:不知道如何测试某些典型案例,要么错误地测试、要么不进行测试、要么利用集成测试来保证覆盖率。比如:
    ①错误地测试:利用测试返回节点占比来测试随机负载均衡策略;
    ②不进行测试:没有人针对虚基类进行单独地测试;
    ③利用集成测试:很多案例中,直接注入真实依赖对象,然后一起进行集成测试。
针对无效验证问题,在我的ATA文章《那些年,我们写过的无效单元测试》中,介绍了如何辨认和解决单元测试无效验证问题,这里就不再累述了。在本文中,作者网络了一些的Java单元测试典型案例,重要是为了解决这个测试方法问题
1. 如何测试不可达代码

在程序代码中,由于无法满足进入条件,永世都不会执行到的代码,我们称之为"不可达代码"。不可达代码的危害重要有:复杂了代码逻辑,增长了代码运行和维护成本。不可达代码是可以由单元测试检测出来的——不管如何构造单元测试用例,都无法覆盖到不可达代码。
1.1. 案例代码

在下面的案例代码中,就存在一段不可达代码。
  1. /**
  2. * 交易订单服务类
  3. */
  4. @Service
  5. public class TradeOrderService {
  6.     /** 注入依赖对象 */
  7.     /** 交易订单DAO */
  8.     @Autowired
  9.     private TradeOrderDAO tradeOrderDAO;
  10.     /**
  11.      * 查询交易订单
  12.      *
  13.      * @param orderQuery 订单查询
  14.      * @return 交易订单分页
  15.      */
  16.     public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
  17.         // 查询交易订单
  18.         // 查询交易订单: 总共数量
  19.         Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
  20.         // 查询交易订单: 数据列表
  21.         List<TradeOrderVO> dataList = null;
  22.         if (NumberHelper.isPositive(totalSize)) {
  23.             List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
  24.             if (CollectionUtils.isNotEmpty(tradeOrderList)) {
  25.                 dataList = convertTradeOrders(tradeOrderList);
  26.             }
  27.         }
  28.         // 返回分页数据
  29.         return new PageDataVO<>(totalSize, dataList);
  30.     }
  31.     /**
  32.      * 转化交易订单列表
  33.      *
  34.      * @param tradeOrderList 交易订单DO列表
  35.      * @return 交易订单VO列表
  36.      */
  37.     private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
  38.         // 检查订单列表
  39.         if (CollectionUtils.isEmpty(tradeOrderList)) {
  40.             return Collections.emptyList();
  41.         }
  42.         // 转化订单列表
  43.         return tradeOrderList.stream().map(TradeOrderService::convertTradeOrder)
  44.             .collect(Collectors.toList());
  45.     }
  46.     /**
  47.      * 转化交易订单
  48.      *
  49.      * @param tradeOrder 交易订单DO
  50.      * @return 交易订单VO
  51.      */
  52.     private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) {
  53.         TradeOrderVO tradeOrderVO = new TradeOrderVO();
  54.         tradeOrderVO.setId(tradeOrder.getId());
  55.         // ...
  56.         return tradeOrderVO;
  57.     }
  58. }
复制代码
由于方法convertTradeOrders(转化交易订单列表)传入的参数tradeOrderList(交易订单列表)不可能为空,所以“查抄订单列表”这段代码是不可达代码。
  1. // 检查订单列表
  2.         if (CollectionUtils.isEmpty(tradeOrderList)) {
  3.             return Collections.emptyList();
  4.         }
复制代码
1.2. 方案1:删除不可达代码(推荐)

最简朴的方法,就是删除方法convertTradeOrders(转化交易订单列表)中的不可达代码。
  1. /**
  2. * 转化交易订单列表
  3. *
  4. * @param tradeOrderList 交易订单DO列表
  5. * @return 交易订单VO列表
  6. */
  7. private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
  8.     return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder)
  9.         .collect(Collectors.toList());
  10. }
复制代码
1.3. 方案2:利用不可达代码(推荐)

另有一种方法,把不可达代码利用起来,可以低落方法queryTradeOrder(查询交易订单)的代码复杂度。
  1. /**
  2. * 查询交易订单
  3. *
  4. * @param orderQuery 订单查询
  5. * @return 交易订单分页
  6. */
  7. public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
  8.     // 查询交易订单
  9.     // 查询交易订单: 总共数量
  10.     Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
  11.     // 查询交易订单: 数据列表
  12.     List<TradeOrderVO> dataList = null;
  13.     if (NumberHelper.isPositive(totalSize)) {
  14.         List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
  15.         dataList = convertTradeOrders(tradeOrderList);
  16.     }
  17.     // 返回分页数据
  18.     return new PageDataVO<>(totalSize, dataList);
  19. }
复制代码
1.4. 方案3:测试不可达代码(不推荐)

对于一些祖传代码,有些小伙伴不敢删除代码。在某些环境下,可以针对不可达代码进行单独测试。
  1. /**
  2. * 测试: 转化交易订单列表-交易订单列表为空
  3. *
  4. * @throws Exception 异常信息
  5. */
  6. @Test
  7. public void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception {
  8.     List<TradeOrderDO> tradeOrderList = null;
  9.     Assert.assertSame("交易订单列表不为空", Collections.emptyList(),
  10.         Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList));
  11. }
复制代码
2. 如何测试内部的构造方法

在这次单元测试总决赛中,有一个随机负载均衡策略,需要针对Random(随机数)进行单元测试。
2.1. 代码案例

按照题目要求,编写了一个简朴的随机负载均衡策略。
  1. /**
  2. * 随机负载均衡策略类
  3. */
  4. public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
  5.     /**
  6.      * 选择服务节点
  7.      *
  8.      * @param serverNodeList 服务节点列表
  9.      * @param clientRequest 客户请求
  10.      * @return 服务节点
  11.      */
  12.     @Override
  13.     public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  14.         // 检查节点列表
  15.         if (CollectionUtils.isEmpty(serverNodeList)) {
  16.             return null;
  17.         }
  18.         // 计算随机序号
  19.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  20.         int randomIndex = new Random().nextInt(totalWeight);
  21.         // 查找对应节点
  22.         for (ServerNode serverNode : serverNodeList) {
  23.             int currentWeight = serverNode.getWeight();
  24.             if (currentWeight > randomIndex) {
  25.                 return serverNode;
  26.             }
  27.             randomIndex -= currentWeight;
  28.         }
  29.         return null;
  30.     }
  31. }
复制代码
2.2. 方法1:直接测试法(不推荐)

有些参赛选手,不知道如何测试随机数(重要原因是因为不知道如何Mock构造方法),所以直接利用测试返回节点占比来测试随机负载均衡策略。
  1. /**
  2. * 随机负载均衡策略测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class RandomLoadBalanceStrategyTest {
  6.     /** 定义测试对象 */
  7.     /** 随机负载均衡策略 */
  8.     @InjectMocks
  9.     private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
  10.     /**
  11.      * 测试: 选择服务节点-随机
  12.      *
  13.      * @throws Exception 异常信息
  14.      */
  15.     @Test
  16.     public void testSelectNodeWithRandom() throws Exception {
  17.         int nodeCount1 = 0;
  18.         int nodeCount2 = 0;
  19.         int nodeCount3 = 0;
  20.         ServerNode serverNode1 = new ServerNode(1L, 10);
  21.         ServerNode serverNode2 = new ServerNode(2L, 20);
  22.         ServerNode serverNode3 = new ServerNode(3L, 30);
  23.         List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
  24.         ClientRequest clientRequest = new ClientRequest();
  25.         for (int i = 0; i < 1000; i++) {
  26.             ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
  27.             if (serviceNode == serverNode1) {
  28.                 nodeCount1++;
  29.             } else if (serviceNode == serverNode2) {
  30.                 nodeCount2++;
  31.             } else if (serviceNode == serverNode3) {
  32.                 nodeCount3++;
  33.             }
  34.         }
  35.         Assert.assertEquals("节点1占比不一致", serverNode1.getWeight() / 60.0D, nodeCount1 / 1000.0D, 1E-3D);
  36.         Assert.assertEquals("节点2占比不一致", serverNode2.getWeight() / 60.0D, nodeCount2 / 1000.0D, 1E-3D);
  37.         Assert.assertEquals("节点3占比不一致", serverNode3.getWeight() / 60.0D, nodeCount3 / 1000.0D, 1E-3D);
  38.     }
  39. }
复制代码
这个测试用例重要存在3个问题:

  • 执行时间长:被测方法需要被执行1000遍;
  • 不肯定通过:由于随机数是随机,并不肯定保证比例,所以导致测试用例并不肯定通过;
  • 测试目的变动:单测测试的测试目的应该是负载均衡逻辑,现在感觉测试目的变成了Random方法。
2.3. 方法2:直接mock法(不推荐)

用过PowerMockito高级功能的,知道如何去Mock构造方法。
  1. /**
  2. * 随机负载均衡策略测试类
  3. */
  4. @RunWith(PowerMockRunner.class)
  5. @PrepareForTest(RandomLoadBalanceStrategy.class)
  6. public class RandomLoadBalanceStrategyTest {
  7.     /** 定义测试对象 */
  8.     /** 随机负载均衡策略 */
  9.     @InjectMocks
  10.     private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
  11.     /**
  12.      * 测试: 选择服务节点-第一个节点
  13.      *
  14.      * @throws Exception 异常信息
  15.      */
  16.     @Test
  17.     public void testSelectNodeWithFirstNode() throws Exception {
  18.         // 模拟依赖方法
  19.         Random random = Mockito.mock(Random.class);
  20.         Mockito.doReturn(9).when(random).nextInt(Mockito.anyInt());
  21.         PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(random);
  22.         // 调用测试方法
  23.         ServerNode serverNode1 = new ServerNode(1L, 10);
  24.         ServerNode serverNode2 = new ServerNode(2L, 20);
  25.         ServerNode serverNode3 = new ServerNode(3L, 30);
  26.         List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
  27.         ClientRequest clientRequest = new ClientRequest();
  28.         ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
  29.         Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
  30.         // 验证依赖方法
  31.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  32.         Mockito.verify(random).nextInt(totalWeight);
  33.     }
  34. }
复制代码
但是,这个测试用例也存在问题:需要把RandomLoadBalanceStrategy加到@PrepareForTest注解中,导致Jacoco无法统计单元测试的覆盖率。
2.4. 方法3:工具方法法(推荐)

其实,随机数天生,另有很多工具方法,我们可以利用工具方法RandomUtils.nextInt取代构造方法。
2.4.1. 重构代码

  1. /**
  2. * 随机负载均衡策略类
  3. */
  4. public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
  5.     /**
  6.      * 选择服务节点
  7.      *
  8.      * @param serverNodeList 服务节点列表
  9.      * @param clientRequest 客户请求
  10.      * @return 服务节点
  11.      */
  12.     @Override
  13.     public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  14.         // 检查节点列表
  15.         if (CollectionUtils.isEmpty(serverNodeList)) {
  16.             return null;
  17.         }
  18.         // 计算随机序号
  19.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  20.         int randomIndex = RandomUtils.nextInt(0, totalWeight);
  21.         // 查找对应节点
  22.         for (ServerNode serverNode : serverNodeList) {
  23.             int currentWeight = serverNode.getWeight();
  24.             if (currentWeight > randomIndex) {
  25.                 return serverNode;
  26.             }
  27.             randomIndex -= currentWeight;
  28.         }
  29.         return null;
  30.     }
  31. }
复制代码
2.4.2. 测试用例

  1. /**
  2. * 随机负载均衡策略测试类
  3. */
  4. @RunWith(PowerMockRunner.class)
  5. @PrepareForTest(RandomUtils.class)
  6. public class RandomLoadBalanceStrategyTest {
  7.     /** 定义测试对象 */
  8.     /** 随机负载均衡策略 */
  9.     @InjectMocks
  10.     private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
  11.     /**
  12.      * 测试: 选择服务节点-第一个节点
  13.      */
  14.     @Test
  15.     public void testSelectNodeWithFirstNode() {
  16.         // 模拟依赖方法
  17.         PowerMockito.mockStatic(RandomUtils.class);
  18.         PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);
  19.         // 调用测试方法
  20.         ServerNode serverNode1 = new ServerNode(1L, 10);
  21.         ServerNode serverNode2 = new ServerNode(2L, 20);
  22.         ServerNode serverNode3 = new ServerNode(3L, 30);
  23.         List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
  24.         ClientRequest clientRequest = new ClientRequest();
  25.         ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
  26.         Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
  27.         // 验证依赖方法
  28.         PowerMockito.verifyStatic(RandomUtils.class);
  29.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  30.         RandomUtils.nextInt(0, totalWeight);
  31.     }
  32. }
复制代码
2.5. 方法4:注入对象法(推荐)

假如不愿意使用工具方法,也可以注入依赖对象,我们可以利用RandomProvider(随机数提供者)来取代构造方法。
2.5.1. 重构代码

  1. /**
  2. * 随机负载均衡策略类
  3. */
  4. public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
  5.     /** 注入依赖对象 */
  6.     /** 随机数提供者 */
  7.     @Autowired
  8.     private RandomProvider randomProvider;
  9.     /**
  10.      * 选择服务节点
  11.      *
  12.      * @param serverNodeList 服务节点列表
  13.      * @param clientRequest 客户请求
  14.      * @return 服务节点
  15.      */
  16.     @Override
  17.     public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  18.         // 检查节点列表
  19.         if (CollectionUtils.isEmpty(serverNodeList)) {
  20.             return null;
  21.         }
  22.         // 计算随机序号
  23.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  24.         int randomIndex = randomProvider.nextInt(totalWeight);
  25.         // 查找对应节点
  26.         for (ServerNode serverNode : serverNodeList) {
  27.             int currentWeight = serverNode.getWeight();
  28.             if (currentWeight > randomIndex) {
  29.                 return serverNode;
  30.             }
  31.             randomIndex -= currentWeight;
  32.         }
  33.         return null;
  34.     }
  35. }
复制代码
2.5.2. 测试用例

  1. /**
  2. * 随机负载均衡策略测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class RandomLoadBalanceStrategyTest {
  6.     /** 模拟依赖方法 */
  7.     /** 随机数提供者 */
  8.     @Mock
  9.     private RandomProvider randomProvider;
  10.     /** 定义测试对象 */
  11.     /** 随机负载均衡策略 */
  12.     @InjectMocks
  13.     private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
  14.     /**
  15.      * 测试: 选择服务节点-第一个节点
  16.      */
  17.     @Test
  18.     public void testSelectNodeWithFirstNode() {
  19.         // 模拟依赖方法
  20.         Mockito.doReturn(9).when(randomProvider).nextInt(Mockito.anyInt());
  21.         // 调用测试方法
  22.         ServerNode serverNode1 = new ServerNode(1L, 10);
  23.         ServerNode serverNode2 = new ServerNode(2L, 20);
  24.         ServerNode serverNode3 = new ServerNode(3L, 30);
  25.         List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
  26.         ClientRequest clientRequest = new ClientRequest();
  27.         ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
  28.         Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
  29.         // 验证依赖方法
  30.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  31.         Mockito.verify(randomProvider).nextInt(totalWeight);
  32.     }
  33. }
复制代码
3. 如何测试虚基类和子类

在这次单元测试比赛中,很多选手都编写了虚基类,但是没有看到任何一个选手针对虚基类进行了单独的测试。
3.1. 案例代码

这里,以Diamond属性设置加载为例说明。
3.1.1. 虚基类定义

首先,定义一个通用的虚基类,定义了需要子类实现的虚方法,实现了通用的设置剖析方法。
  1. /**
  2. * 虚属性回调类
  3. *
  4. * @param <T> 配置类型
  5. */
  6. @Slf4j
  7. public abstract class AbstractPropertiesCallback<T> implements DiamondDataCallback {
  8.     /** 注入依赖对象 */
  9.     /** 环境 */
  10.     @Autowired
  11.     private Environment environment;
  12.     /** 转化服务 */
  13.     @Autowired
  14.     private ConversionService conversionService;
  15.     /**
  16.      * 接收到数据
  17.      *
  18.      * @param data 配置数据
  19.      */
  20.     @Override
  21.     public void received(String data) {
  22.         // 获取配置参数
  23.         String configName = getConfigName();
  24.         Assert.notNull(configName, "配置名称不能为空");
  25.         T configInstance = getConfigInstance();
  26.         Assert.notNull(configInstance, "配置实例不能为空");
  27.         // 解析配置数据
  28.         try {
  29.             log.info("绑定属性配置文件开始: configName={}", configName);
  30.             Properties properties = new Properties();
  31.             byte[] bytes = Optional.ofNullable(data.getBytes()).orElseGet(() -> new byte[0]);
  32.             InputStream inputStream = new ByteArrayInputStream(bytes);
  33.             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
  34.             properties.load(bufferedReader);
  35.             Bindable<T> bindable = Bindable.ofInstance(configInstance);
  36.             Binder binder = new Binder(ConfigurationPropertySources.from(
  37.                 new PropertiesPropertySource(configName, properties)),
  38.                 new PropertySourcesPlaceholdersResolver(environment), conversionService);
  39.             BindResult<T> result = binder.bind(configName, bindable);
  40.             if (!result.isBound()) {
  41.                 log.error("绑定属性配置文件失败: configName={}", configName);
  42.                 return;
  43.             }
  44.             log.info("绑定属性配置文件成功: configName={}, configInstance={}", configName, JSON.toJSONString(configInstance));
  45.         } catch (IOException | RuntimeException e) {
  46.             log.error("绑定属性配置文件异常: configName={}", configName, e);
  47.         }
  48.     }
  49.     /**
  50.      * 获取配置名称
  51.      *
  52.      * @return 配置名称
  53.      */
  54.     @NonNull
  55.     protected abstract String getConfigName();
  56.     /**
  57.      * 获取配置实例
  58.      *
  59.      * @return 配置实例
  60.      */
  61.     @NonNull
  62.     protected abstract T getConfigInstance();
  63. }
复制代码
3.1.2. 子类实现

其次,定义了具体设置的子类,简朴地实现了基类定义的虚方法。
  1. /**
  2. * 例子配置回调类
  3. */
  4. @DiamondListener(groupId = "unittest-example", dataId = "example.properties", executeAfterInit = true)
  5. public class ExampleConfigCallback extends AbstractPropertiesCallback<ExampleConfig> {
  6.     /** 注入依赖对象 */
  7.     /** 例子配置 */
  8.     @Resource
  9.     private ExampleConfig exampleConfig;
  10.     /**
  11.      * 获取配置名称
  12.      *
  13.      * @return 配置名称
  14.      */
  15.     @Override
  16.     protected String getConfigName() {
  17.         return "example";
  18.     }
  19.     /**
  20.      * 获取配置实例
  21.      *
  22.      * @return 配置实例
  23.      */
  24.     @Override
  25.     protected ExampleConfig getConfigInstance() {
  26.         return exampleConfig;
  27.     }
  28. }
复制代码
3.2. 方法1:团结测试法(不推荐)

最简朴的测试方法,就是通过子类对虚基类进行团结测试,这样同时把子类和虚基类都测试了。
  1. /**
  2. * 例子配置回调测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class ExampleConfigCallbackTest {
  6.     /** 定义静态常量 */
  7.     /** 资源路径 */
  8.     private static final String RESOURCE_PATH = "testExampleConfigCallback/";
  9.     /** 模拟依赖对象 */
  10.     /** 配置环境 */
  11.     @Mock
  12.     private ConfigurableEnvironment environment;
  13.     /** 转化服务 */
  14.     @Mock
  15.     private ConversionService conversionService;
  16.     /** 定义测试对象 */
  17.     /** BOSS取消费配置回调 */
  18.     @InjectMocks
  19.     private ExampleConfigCallback exampleConfigCallback;
  20.     /**
  21.      * 测试: 接收-正常
  22.      */
  23.     @Test
  24.     public void testReceivedWithNormal() {
  25.         // 模拟依赖对象
  26.         ExampleConfig exampleConfig = new ExampleConfig();
  27.         Whitebox.setInternalState(exampleConfigCallback, "exampleConfig", exampleConfig);
  28.         // 调用测试方法
  29.         String text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties");
  30.         exampleConfigCallback.received(text);
  31.         // 验证依赖对象
  32.         text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json");
  33.         Assert.assertEquals("取消费用配置不一致", text, JSON.toJSONString(exampleConfig, SerializerFeature.MapSortField));
  34.     }
  35. }
复制代码
3.3. 方法2:独立测试法(推荐)

其实,更好的方法是对虚基类和子类独立单元测试。
3.3.1. 基类测试

虚基类的单元测试,专注于虚基类的通用设置剖析。
  1. /**
  2. * 虚属性回调测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class AbstractPropertiesCallbackTest {
  6.     /** 静态常量相关 */
  7.     /** 资源目录 */
  8.     private static final String RESOURCE_PATH = "testAbstractPropertiesCallback/";
  9.     /** 模拟依赖对象 */
  10.     /** 环境 */
  11.     @Mock
  12.     private ConfigurableEnvironment environment;
  13.     /** 转化服务 */
  14.     @Mock
  15.     private ConversionService conversionService;
  16.     /** 定义测试对象 */
  17.     /** 虚属性回调 */
  18.     @InjectMocks
  19.     private AbstractPropertiesCallback<ExampleConfig> propertiesCallback =
  20.         CastUtils.cast(Mockito.spy(AbstractPropertiesCallback.class));
  21.     /**
  22.      * 测试: 接收到-正常
  23.      */
  24.     @Test
  25.     public void testReceivedWithNormal() {
  26.         // 模拟依赖方法
  27.         // 模拟依赖方法: propertiesCallback.getConfigName
  28.         String configName = "example";
  29.         Mockito.doReturn(configName).when(propertiesCallback).getConfigName();
  30.         // 模拟依赖方法: propertiesCallback.getConfigInstance
  31.         ExampleConfig configInstance = new ExampleConfig();
  32.         Mockito.doReturn(configInstance).when(propertiesCallback).getConfigInstance();
  33.         // 调用测试方法
  34.         String text1 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties");
  35.         propertiesCallback.received(text1);
  36.         String text2 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json");
  37.         Assert.assertEquals("任务配置不一致", text2, JSON.toJSONString(configInstance));
  38.         // 验证依赖方法
  39.         // 验证依赖方法: propertiesCallback.received
  40.         Mockito.verify(propertiesCallback).received(text1);
  41.         // 验证依赖方法: propertiesCallback.getConfigName
  42.         Mockito.verify(propertiesCallback).getConfigName();
  43.         // 验证依赖方法: propertiesCallback.getConfigInstance
  44.         Mockito.verify(propertiesCallback).getConfigInstance();
  45.     }
  46. }
复制代码
3.3.2. 子类测试

子类的单元测试,专注于对虚基类定义虚方法的实现,制止了每个子类都要针对虚基类的通用设置剖析进行测试。
  1. /**
  2. * 例子配置回调测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class ExampleConfigCallbackTest {
  6.     /** 定义测试对象 */
  7.     /** BOSS取消费配置回调 */
  8.     @InjectMocks
  9.     private ExampleConfigCallback exampleConfigCallback;
  10.    
  11.     /**
  12.      * 测试: 获取配置实例
  13.      */
  14.     @Test
  15.     public void testGetConfigInstance() {
  16.         Assert.assertEquals("配置实例不一致", exampleConfig, exampleConfigCallback.getConfigInstance());
  17.     }
  18.     /**
  19.      * 测试: 获取配置名称
  20.      */
  21.     @Test
  22.     public void testGetConfigName() {
  23.         Assert.assertEquals("配置名称不一致", "example", exampleConfigCallback.getConfigName());
  24.     }
  25. }
复制代码
4. 如何测试策略模式的策略服务

4.1. 案例代码

在这次单元测试比赛中,很多选手都编写了策略服务类,但是没有看到任何一个选手针对策略服务类进行了单独的测试。这里,还是以负载均衡的策略服务为例说明。
4.1.1. 策略接口

首先,定义一个负载均衡策略接口。
  1. /**
  2. * 负载均衡策略接口
  3. */
  4. public interface LoadBalanceStrategy {
  5.     /**
  6.      * 支持策略类型
  7.      *
  8.      * @return 策略类型
  9.      */
  10.     LoadBalanceStrategyType supportType();
  11.     /**
  12.      * 选择服务节点
  13.      *
  14.      * @param serverNodeList 服务节点列表
  15.      * @param clientRequest 客户请求
  16.      * @return 服务节点
  17.      */
  18.     ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest);
  19. }
复制代码
4.1.2. 策略服务

其次,实现一个负载均衡策略服务,根据负载均衡策略类型选择对应的负载均衡策略来执行。
  1. /**
  2. * 负载均衡服务类
  3. */
  4. public class LoadBalanceService {
  5.     /** 负载均衡策略映射 */
  6.     private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap;
  7.     /**
  8.      * 构造方法
  9.      *
  10.      * @param strategyList 负载均衡策略列表
  11.      */
  12.     public LoadBalanceService(List<LoadBalanceStrategy> strategyList) {
  13.         strategyMap = new EnumMap<>(LoadBalanceStrategyType.class);
  14.         for (LoadBalanceStrategy strategy : strategyList) {
  15.             strategyMap.put(strategy.supportType(), strategy);
  16.         }
  17.     }
  18.     /**
  19.      * 选择服务节点
  20.      *
  21.      * @param strategyType 策略类型
  22.      * @param serverNodeList 服务节点列表
  23.      * @param clientRequest 客户请求
  24.      * @return 服务节点
  25.      */
  26.     public ServerNode selectNode(LoadBalanceStrategyType strategyType,
  27.         List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  28.         // 获取负载均衡策略
  29.         LoadBalanceStrategy strategy = strategyMap.get(strategyType);
  30.         if (Objects.isNull(strategy)) {
  31.             throw new BusinessException("负载均衡策略不存在");
  32.         }
  33.         // 执行负载均衡策略
  34.         return strategy.selectNode(serverNodeList, clientRequest);
  35.     }
  36. }
复制代码
4.1.3. 策略实现

最后,实现一个随机负载均衡策略实现类。
  1. /**
  2. * 随机负载均衡策略类
  3. */
  4. public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
  5.     /**
  6.      * 支持策略类型
  7.      *
  8.      * @return 策略类型
  9.      */
  10.     @Override
  11.     public LoadBalanceStrategyType supportType() {
  12.         return LoadBalanceStrategyType.RANDOM;
  13.     }
  14.     /**
  15.      * 选择服务节点
  16.      *
  17.      * @param serverNodeList 服务节点列表
  18.      * @param clientRequest 客户请求
  19.      * @return 服务节点
  20.      */
  21.     @Override
  22.     public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  23.         // 检查节点列表
  24.         if (CollectionUtils.isEmpty(serverNodeList)) {
  25.             return null;
  26.         }
  27.         // 计算随机序号
  28.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  29.         int randomIndex = RandomUtils.nextInt(0, totalWeight);
  30.         // 查找对应节点
  31.         for (ServerNode serverNode : serverNodeList) {
  32.             int currentWeight = serverNode.getWeight();
  33.             if (currentWeight > randomIndex) {
  34.                 return serverNode;
  35.             }
  36.             randomIndex -= currentWeight;
  37.         }
  38.         return null;
  39.     }
  40. }
复制代码
4.2. 方法1:团结测试法(不推荐)

很多时候,策略模式是用来优化if-else代码的。所以,采用团结测试法(策略服务和策略实现同时测试),可以大概最大限度地利用原有的单元测试代码。
  1. /**
  2. * 负载均衡服务测试类
  3. */
  4. @RunWith(PowerMockRunner.class)
  5. @PrepareForTest(RandomUtils.class)
  6. public class LoadBalanceServiceTest {
  7.     /**
  8.      * 测试: 选择服务节点-正常
  9.      */
  10.     @Test
  11.     public void testSelectNodeWithNormal() {
  12.         // 模拟依赖方法
  13.         PowerMockito.mockStatic(RandomUtils.class);
  14.         PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);
  15.         // 调用测试方法
  16.         ServerNode serverNode1 = new ServerNode(1L, 10);
  17.         ServerNode serverNode2 = new ServerNode(2L, 20);
  18.         ServerNode serverNode3 = new ServerNode(3L, 30);
  19.         List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
  20.         ClientRequest clientRequest = new ClientRequest();
  21.         RandomLoadBalanceStrategy randomLoadBalanceStrategy = new RandomLoadBalanceStrategy();
  22.         LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(randomLoadBalanceStrategy));
  23.         ServerNode serviceNode = loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM,
  24.             serverNodeList, clientRequest);
  25.         Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
  26.         // 验证依赖方法
  27.         PowerMockito.verifyStatic(RandomUtils.class);
  28.         int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
  29.         RandomUtils.nextInt(0, totalWeight);
  30.     }
  31. }
复制代码
策略模式的团结测试法重要有以下问题:

  • 策略服务依赖于策略实现,需要了解策略实现的具体逻辑,才气写出策略服务的单元测试;
  • 对于策略服务来说,该单元测试并不关心策略服务的实现,这是黑盒测试而不是白盒测试。
假如我们对策略服务进行以下破坏,该单元测试并不能发现问题:

  • strategyMap没有根据strategyList天生;
  • strategyMap.get(strategyType)为空时,初始化一个RandomLoadBalanceStrategy。
  1. /**
  2. * 负载均衡服务类
  3. */
  4. public class LoadBalanceService {
  5.     /** 负载均衡策略映射 */
  6.     private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap;
  7.     /**
  8.      * 构造方法
  9.      *
  10.      * @param strategyList 负载均衡策略列表
  11.      */
  12.     public LoadBalanceService(List<LoadBalanceStrategy> strategyList) {
  13.         strategyMap = new EnumMap<>(LoadBalanceStrategyType.class);
  14.     }
  15.     /**
  16.      * 选择服务节点
  17.      *
  18.      * @param strategyType 策略类型
  19.      * @param serverNodeList 服务节点列表
  20.      * @param clientRequest 客户请求
  21.      * @return 服务节点
  22.      */
  23.     public ServerNode selectNode(LoadBalanceStrategyType strategyType,
  24.         List<ServerNode> serverNodeList, ClientRequest clientRequest) {
  25.         // 获取负载均衡策略
  26.         LoadBalanceStrategy strategy = strategyMap.get(strategyType);
  27.         if (Objects.isNull(strategy)) {
  28.             strategy = new RandomLoadBalanceStrategy();
  29.         }
  30.         // 执行负载均衡策略
  31.         return strategy.selectNode(serverNodeList, clientRequest);
  32.     }
  33. }
复制代码
4.3. 方法2:独立测试法(推荐)

现在,先假设策略实现RandomLoadBalanceStrategy(随机负载均衡策略)不存在,直接对策略服务LoadBalanceService(负载均衡服务)独立测试,而且是分别对构造方法和selectNode(选择服务节点)方法进行独立测试。其中,测试构造方法是为了保证strategyMap构造逻辑没有问题,测试selectNode(选择服务节点)方法是为了保证选择策略逻辑没有问题。
  1. /**
  2. * 负载均衡服务测试类
  3. */
  4. public class LoadBalanceServiceTest {
  5.     /**
  6.      * 测试: 构造方法
  7.      */
  8.     @Test
  9.     public void testConstructor() {
  10.         // 模拟依赖方法
  11.         LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class);
  12.         Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType();
  13.         // 调用测试方法
  14.         LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy));
  15.         Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap =
  16.             Whitebox.getInternalState(loadBalanceService, "strategyMap");
  17.         Assert.assertEquals("策略映射大小不一致", 1, strategyMap.size());
  18.         Assert.assertEquals("策略映射对象不一致", loadBalanceStrategy, strategyMap.get(LoadBalanceStrategyType.RANDOM));
  19.         // 验证依赖方法
  20.         Mockito.verify(loadBalanceStrategy).supportType();
  21.     }
  22.     /**
  23.      * 测试: 选择服务节点-正常
  24.      */
  25.     @Test
  26.     public void testSelectNodeWithNormal() {
  27.         // 模拟依赖方法
  28.         LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class);
  29.         // 模拟依赖方法: loadBalanceStrategy.supportType
  30.         Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType();
  31.         // 模拟依赖方法: loadBalanceStrategy.selectNode
  32.         ServerNode serverNode = Mockito.mock(ServerNode.class);
  33.         Mockito.doReturn(serverNode).when(loadBalanceStrategy)
  34.             .selectNode(Mockito.anyList(), Mockito.any(ClientRequest.class));
  35.         // 调用测试方法
  36.         List<ServerNode> serverNodeList = CastUtils.cast(Mockito.mock(List.class));
  37.         ClientRequest clientRequest = Mockito.mock(ClientRequest.class);
  38.         LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy));
  39.         Assert.assertEquals("服务节点不一致", serverNode,
  40.             loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest));
  41.         // 验证依赖方法
  42.         // 验证依赖方法: loadBalanceStrategy.supportType
  43.         Mockito.verify(loadBalanceStrategy).supportType();
  44.         // 验证依赖方法: loadBalanceStrategy.selectNode
  45.         Mockito.verify(loadBalanceStrategy).selectNode(serverNodeList, clientRequest);
  46.     }
  47. }
复制代码
其实,不光是策略模式,很多模式下都不发起团结测试,而是推荐采用独立的单元测试。因为单元测试是白盒测试——一种专注于自身代码逻辑的测试。
5. 如何测试Lambda表达式

在有些单元测试中,Lambda表达式并不肯定被执行,所以导致Lambda表达式没有被测试。
5.1. 案例代码

这里,以从ODPS中查询用户交易订单为例说明。
5.1.1. 被测代码

交易订单查询服务,其中有一段转化订单的Lambda表达式。
  1. /**
  2. * 交易订单服务
  3. */
  4. @Service
  5. public class TradeOrderService {
  6.     /** 注入依赖对象 */
  7.     /** 交易ODPS服务 */
  8.     @Autowired
  9.     private TradeOdpsService tradeOdpsService;
  10.     /**
  11.      * 查询交易订单
  12.      *
  13.      * @param userId 用户标识
  14.      * @param maxCount 最大数量
  15.      * @return 交易订单列表
  16.      */
  17.     public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) {
  18.         String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
  19.         String sql = String.format(format, userId, maxCount);
  20.         return tradeOdpsService.executeQuery(sql, record -> {
  21.             TradeOrderVO tradeOrder = new TradeOrderVO();
  22.             tradeOrder.setId(record.getBigint("id"));
  23.             // ...
  24.             return tradeOrder;
  25.         });
  26.     }
  27. }
复制代码
5.1.2. 依赖代码

封装了通用的ODPS查询方法。
  1. /**
  2. * 交易ODPS服务类
  3. */
  4. @Slf4j
  5. @Service
  6. public class TradeOdpsService {
  7.     /** 注入依赖对象 */
  8.     /** 交易ODPS */
  9.     @Resource(name = "tradeOdps")
  10.     private Odps tradeOdps;
  11.     /**
  12.      * 执行查询
  13.      *
  14.      * @param <T> 模板类型
  15.      * @param sql SQL语句
  16.      * @param dataParser 数据解析器
  17.      * @return 查询结果列表
  18.      */
  19.     public <T> List<T> executeQuery(String sql, Function<Record, T> dataParser) {
  20.         try {
  21.             // 打印提示信息
  22.             log.info("开始执行ODPS数据查询...");
  23.             // 执行ODPS查询
  24.             Instance instance = SQLTask.run(tradeOdps, sql);
  25.             instance.waitForSuccess();
  26.             // 获取查询结果
  27.             List<Record> recordList = SQLTask.getResult(instance);
  28.             if (CollectionUtils.isEmpty(recordList)) {
  29.                 log.info("完成执行ODPS数据查询: totalSize=0");
  30.                 return Collections.emptyList();
  31.             }
  32.             // 依次读取数据
  33.             List<T> dataList = new ArrayList<>();
  34.             for (Record record : recordList) {
  35.                 T data = dataParser.apply(record);
  36.                 if (Objects.nonNull(data)) {
  37.                     dataList.add(data);
  38.                 }
  39.             }
  40.             // 打印提示信息
  41.             log.info("完成执行ODPS数据查询: totalSize={}", dataList.size());
  42.             // 返回查询结果
  43.             return dataList;
  44.         } catch (OdpsException e) {
  45.             log.warn("执行ODPS数据查询异常: sql={}", sql, e);
  46.             throw new BusinessException("执行ODPS数据查询异常", e);
  47.         }
  48.     }
  49. }
复制代码
5.2. 方法1:直接测试法(不推荐)

按照通用的单元测试方法进行测试,发现Lambda表达式没有被测试到。
  1. /**
  2. * 交易订单服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class TradeOrderServiceTest {
  6.     /** 定义静态常量 */
  7.     /** 资源路径 */
  8.     private static final String RESOURCE_PATH = "testTradeOrderService/";
  9.     /** 模拟依赖对象 */
  10.     /** 交易ODPS服务 */
  11.     @Mock
  12.     private TradeOdpsService tradeOdpsService;
  13.     /** 定义测试对象 */
  14.     /** 交易订单服务 */
  15.     @InjectMocks
  16.     private TradeOrderService tradeOrderService;
  17.     /**
  18.      * 测试: 查询交易订单-正常
  19.      */
  20.     @Test
  21.     public void testQueryTradeOrderWithNormal() {
  22.         // 模拟依赖方法
  23.         // 模拟依赖方法: tradeOdpsService.executeQuery
  24.         List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
  25.         Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());
  26.         // 调用测试方法
  27.         Long userId = 12345L;
  28.         Integer maxCount = 100;
  29.         Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));
  30.         // 验证依赖方法
  31.         // 验证依赖方法: tradeOdpsService.executeQuery
  32.         String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
  33.         String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
  34.         Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
  35.     }
  36. }
复制代码
5.3. 方法2:团结测试法(不推荐)

有人发起,可以把TradeOrderService(交易订单服务)和TradeOdpsService(交易ODPS服务)团结测试,这样就可以保证Lambda表达式被测试到。
  1. /**
  2. * 交易订单服务测试类
  3. */
  4. @RunWith(PowerMockRunner.class)
  5. @PrepareForTest({SQLTask.class})
  6. public class TradeOrderServiceTest {
  7.     /** 定义静态常量 */
  8.     /** 资源路径 */
  9.     private static final String RESOURCE_PATH = "testTradeOrderService/";
  10.     /** 模拟依赖对象 */
  11.     /** 交易ODPS */
  12.     @Mock(name = "tradeOdps")
  13.     private Odps tradeOdps;
  14.     /** 定义测试对象 */
  15.     /** 交易ODPS服务 */
  16.     @InjectMocks
  17.     private TradeOdpsService tradeOdpsService = Mockito.spy(TradeOdpsService.class);
  18.     /** 交易订单服务 */
  19.     @InjectMocks
  20.     private TradeOrderService tradeOrderService;
  21.     /**
  22.      * 测试: 查询交易订单-正常
  23.      *
  24.      * @throws OdpsException ODPS异常
  25.      */
  26.     @Test
  27.     public void testQueryTradeOrderWithNormal() throws OdpsException {
  28.         // 模拟依赖方法
  29.         PowerMockito.mockStatic(SQLTask.class);
  30.         // 模拟依赖方法: SQLTask.run
  31.         Instance instance = Mockito.mock(Instance.class);
  32.         PowerMockito.when(SQLTask.run(Mockito.eq(tradeOdps), Mockito.anyString())).thenReturn(instance);
  33.         // 模拟依赖方法: SQLTask.getResult
  34.         Record record1 = PowerMockito.mock(Record.class);
  35.         Record record2 = PowerMockito.mock(Record.class);
  36.         List<Record> recordList = Arrays.asList(record1, record2);
  37.         PowerMockito.when(SQLTask.getResult(instance)).thenReturn(recordList);
  38.         // 模拟依赖方法: record.getString
  39.         Mockito.doReturn(1L).when(record1).getBigint("id");
  40.         Mockito.doReturn(2L).when(record2).getBigint("id");
  41.         // 调用测试方法
  42.         Long userId = 12345L;
  43.         Integer maxCount = 100;
  44.         List<TradeOrderVO> tradeOrderList = tradeOrderService.queryTradeOrder(userId, maxCount);
  45.         String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
  46.         String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json");
  47.         Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList));
  48.         // 验证依赖方法
  49.         PowerMockito.verifyStatic(SQLTask.class);
  50.         // 验证依赖方法: SQLTask.run
  51.         text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
  52.         SQLTask.run(tradeOdps, text);
  53.         // 验证依赖方法: SQLTask.getResult
  54.         SQLTask.getResult(instance);
  55.         // 验证依赖方法: instance.waitForSuccess
  56.         Mockito.verify(instance).waitForSuccess();
  57.         // 验证依赖方法: record.getString
  58.         Mockito.verify(record1).getBigint("id");
  59.         Mockito.verify(record2).getBigint("id");
  60.     }
  61. }
复制代码
重要问题:需要了解TradeOdpsService.executeQuery(执行查询)方法的逻辑并构建单元测试用例,导致TradeOrderService.queryTradeOrder(查询交易订单)方法的单测测试用例非常复杂。
5.3. 方法3:重构测试法(推荐)

其实,只需要把这段Lambda表达式提取成一个convertTradeOrder(转化交易订单)方法,即可让代码变得清晰明了,又可以让代码更轻易单元测试。
5.3.1. 重构代码

提取Lambda表达式为convertTradeOrder(转化交易订单)方法。
  1. /**
  2. * 交易订单服务类
  3. */
  4. @Service
  5. public class TradeOrderService {
  6.     /** 注入依赖对象 */
  7.     /** 交易ODPS服务 */
  8.     @Autowired
  9.     private TradeOdpsService tradeOdpsService;
  10.     /**
  11.      * 查询交易订单
  12.      *
  13.      * @param userId 用户标识
  14.      * @param maxCount 最大数量
  15.      * @return 交易订单列表
  16.      */
  17.     public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) {
  18.         String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
  19.         String sql = String.format(format, userId, maxCount);
  20.         return tradeOdpsService.executeQuery(sql, TradeOrderService2::convertTradeOrder);
  21.     }
  22.     /**
  23.      * 转化交易订单
  24.      *
  25.      * @param record ODPS记录
  26.      * @return 交易订单
  27.      */
  28.     private static TradeOrderVO convertTradeOrder(Record record) {
  29.         TradeOrderVO tradeOrder = new TradeOrderVO();
  30.         tradeOrder.setId(record.getBigint("id"));
  31.         // ...
  32.         return tradeOrder;
  33.     }
  34. }
复制代码
5.3.2. 测试用例

针对queryTradeOrder(查询交易订单)方法和convertTradeOrder(转化交易订单)方法分别进行单元测试。
  1. /**
  2. * 交易订单服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class TradeOrderServiceTest {
  6.     /** 定义静态常量 */
  7.     /** 资源路径 */
  8.     private static final String RESOURCE_PATH = "testTradeOrderService/";
  9.     /** 模拟依赖对象 */
  10.     /** 交易ODPS服务 */
  11.     @Mock
  12.     private TradeOdpsService tradeOdpsService;
  13.     /** 定义测试对象 */
  14.     /** 交易订单服务 */
  15.     @InjectMocks
  16.     private TradeOrderService tradeOrderService;
  17.     /**
  18.      * 测试: 查询交易订单-正常
  19.      */
  20.     @Test
  21.     public void testQueryTradeOrderWithNormal() {
  22.         // 模拟依赖方法
  23.         // 模拟依赖方法: tradeOdpsService.executeQuery
  24.         List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
  25.         Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());
  26.         // 调用测试方法
  27.         Long userId = 12345L;
  28.         Integer maxCount = 100;
  29.         Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));
  30.         // 验证依赖方法
  31.         // 验证依赖方法: tradeOdpsService.executeQuery
  32.         String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
  33.         String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
  34.         Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
  35.     }
  36.     /**
  37.      * 测试: 转化交易订单
  38.      *
  39.      * @throws Exception 异常信息
  40.      */
  41.     @Test
  42.     public void testConvertTradeOrder() throws Exception {
  43.         // 模拟依赖方法
  44.         Long id = 12345L;
  45.         Record record = Mockito.mock(Record.class);
  46.         Mockito.doReturn(id).when(record).getBigint("id");
  47.         // 调用测试方法
  48.         TradeOrderVO tradeOrder = Whitebox.invokeMethod(TradeOrderService2.class, "convertTradeOrder", record);
  49.         Assert.assertEquals("订单标识不一致", id, tradeOrder.getId());
  50.         // 验证依赖方法
  51.         Mockito.verify(record).getBigint("id");
  52.     }
  53. }
复制代码
6. 如何测试链式调用

在日常编码过程中,很多人都喜好使用链式调用,这样可以让代码变得更简便。
6.1. 案例代码

这里,通过修改后的添加跨域支持代码来举例说明(原方法没有返回值)。
  1. /**
  2. * 跨域辅助类
  3. */
  4. public class CorsHelper {
  5.     /** 定义静态常量 */
  6.     /** 最大生命周期 */
  7.     private static final long MAX_AGE = 3600L;
  8.     /**
  9.      * 添加跨域支持
  10.      *
  11.      * @param registry 跨域注册器
  12.      * @return 跨域注册
  13.      */
  14.     public static CorsRegistration addCorsMapping(CorsRegistry registry) {
  15.         return registry.addMapping("/**")
  16.             .allowedOrigins("*")
  17.             .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
  18.             .allowCredentials(true)
  19.             .maxAge(MAX_AGE)
  20.             .allowedHeaders("*");
  21.     }
  22. }
复制代码
6.2. 方法1:平凡测试法(不推荐)

正常环境下,每一个依赖对象及其调用方法都要mock,编写的代码如下:
  1. /**
  2. * 跨域辅助测试类
  3. */
  4. public class CorsHelperTest {
  5.     /**
  6.      * 测试: 添加跨域支持
  7.      */
  8.     @Test
  9.     public void testAddCorsMapping() {
  10.         // 模拟依赖方法
  11.         CorsRegistry registry = Mockito.mock(CorsRegistry.class);
  12.         CorsRegistration registration = Mockito.mock(CorsRegistration.class);
  13.         Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
  14.         Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any());
  15.         Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any());
  16.         Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean());
  17.         Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong());
  18.         Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any());
  19.         // 调用测试方法
  20.         Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
  21.         // 验证依赖方法
  22.         Mockito.verify(registry).addMapping("/**");
  23.         Mockito.verify(registration).allowedOrigins("*");
  24.         Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
  25.         Mockito.verify(registration).allowCredentials(true);
  26.         Mockito.verify(registration).maxAge(3600L);
  27.         Mockito.verify(registration).allowedHeaders("*");
  28.     }
  29. }
复制代码
6.3. 方法2:利用RETURNS_DEEP_STUBS参数法(推荐)

对于链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_DEEP_STUBS参数,实现链式调用返回对象的自动mock。利用Mockito.RETURNS_DEEP_STUBS参数编写的测试用例如下:
  1. /**
  2. * 跨域辅助测试类
  3. */
  4. public class CorsHelperTest {
  5.     /**
  6.      * 测试: 添加跨域支持
  7.      */
  8.     @Test
  9.     public void testAddCorsMapping() {
  10.         // 模拟依赖方法
  11.         CorsRegistry registry = Mockito.mock(CorsRegistry.class, Answers.RETURNS_DEEP_STUBS);
  12.         CorsRegistration registration = Mockito.mock(CorsRegistration.class);
  13.         Mockito.when(registry.addMapping(Mockito.anyString())
  14.             .allowedOrigins(Mockito.any())
  15.             .allowedMethods(Mockito.any())
  16.             .allowCredentials(Mockito.anyBoolean())
  17.             .maxAge(Mockito.anyLong())
  18.             .allowedHeaders(Mockito.any()))
  19.             .thenReturn(registration);
  20.         // 调用测试方法
  21.         Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
  22.         // 验证依赖方法
  23.         Mockito.verify(registry.addMapping("/**")
  24.             .allowedOrigins("*")
  25.             .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
  26.             .allowCredentials(true)
  27.             .maxAge(3600L))
  28.             .allowedHeaders("*");
  29.     }
  30. }
复制代码
代码说明:

  • 在mock对象时,需要指定Mockito.RETURNS_DEEP_STUBS参数;
  • 在mock方法时,采用when-then模式,when内容是链式调用,then内容是返回的值;
  • 在verify方法时,只需要验证最后1次方法调用,verify内容是前n次链式调用;假如验证时某个方法调用的某个参数指定错误时,最后一个方法调用验证将因为这个mock对象没有方法调用而抛出非常。
6.4. 方法3:利用RETURNS_SELF参数法(推荐)

对于类似返回值的链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_SELF参数,实现链式调用返回对象的自动mock,而且还能返回同一mock对象。利用Mockito.RETURNS_SELF参数编写的测试用例如下:
  1. /**
  2. * 跨域辅助测试类
  3. */
  4. public class CorsHelperTest {
  5.     /**
  6.      * 测试: 添加跨域支持
  7.      */
  8.     @Test
  9.     public void testAddCorsMapping() {
  10.         // 模拟依赖方法
  11.         CorsRegistry registry = Mockito.mock(CorsRegistry.class);
  12.         CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF);
  13.         Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
  14.         // 调用测试方法
  15.         Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
  16.         // 验证依赖方法
  17.         Mockito.verify(registry).addMapping("/**");
  18.         Mockito.verify(registration).allowedOrigins("*");
  19.         Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
  20.         Mockito.verify(registration).allowCredentials(true);
  21.         Mockito.verify(registration).maxAge(3600L);
  22.         Mockito.verify(registration).allowedHeaders("*");
  23.     }
  24. }
复制代码
代码说明:

  • 在mock对象时,对于自返回对象,需要指定Mockito.RETURNS_SELF参数;
  • 在mock方法时,无需对自返回对象进行mock方法,因为框架已经mock方法返回了自身;
  • 在verify方法时,可以像平凡测试法一样精美地验证所有方法调用。
方法对比:

  • 平凡测试法:mock调用方法语句较多;
  • 利用RETURNS_DEEP_STUBS参数法:mock调用方法语句较少,适合于链式调用返回差别值;
  • 利用RETURNS_SELF参数法:mock调用方法语句最少,适合于链式调用返回类似值。
7. 如何测试类似参数返回差别值

在有些场景下,存在类似参数多次调用返回差别值的环境,比如:读取文本文件的readLine方法。
7.1. 案例代码

这里,以ODPS的RecordReader为例,读取每一行数据记录。
  1. /**
  2. * 读取数据
  3. *
  4. * @param <T> 模板类型
  5. * @param recordReader 记录读取器
  6. * @param dataParser 数据解析器
  7. * @return 数据列表
  8. * @throws IOException IO异常
  9. */
  10. public static <T> List<T> readData(RecordReader recordReader, Function<Record, T> dataParser) throws IOException {
  11.     Record record;
  12.     List<T> dataList = new ArrayList<>();
  13.     while (Objects.nonNull(record = recordReader.read())) {
  14.         T data = dataParser.apply(record);
  15.         if (Objects.nonNull(data)) {
  16.             dataList.add(data);
  17.         }
  18.     }
  19.     return dataList;
  20. }
复制代码
7.2. 测试用例

为了mock类似参数返回差别值,需要使用到Mockito.doReturn的可变数组功能。
  1. /**
  2. * 测试: 读取数据-正常
  3. *
  4. * @throws IOException IO异常
  5. */
  6. @Test
  7. public void testReadDataWithNormal() throws IOException {
  8.     // 模拟依赖方法
  9.     // 模拟依赖方法: recordReader.read
  10.     Record record1 = Mockito.mock(Record.class);
  11.     Record record2 = Mockito.mock(Record.class);
  12.     TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
  13.     Mockito.doReturn(record1, record2, null).when(recordReader).read();
  14.     // 模拟依赖方法: dataParser.apply
  15.     Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
  16.     Object object1 = new Object();
  17.     Object object2 = new Object();
  18.     Mockito.doReturn(object1).when(dataParser).apply(record1);
  19.     Mockito.doReturn(object2).when(dataParser).apply(record2);
  20.     // 调用测试方法
  21.     List<Object> dataList = OdpsHelper.readData(recordReader, dataParser);
  22.     Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList);
  23.     // 验证依赖方法
  24.     // 验证依赖方法: recordReader.read
  25.     Mockito.verify(recordReader, Mockito.times(3)).read();
  26.     // 验证依赖方法: dataParser.apply
  27.     Mockito.verify(dataParser).apply(record1);
  28.     Mockito.verify(dataParser).apply(record2);
  29. }
复制代码
8. 如何测试已变动的方法参数值

在单元测试中,我们通常通过ArgumentCaptor进行方法参数捕获并验证。但是,在有些环境下,我们捕获的可能是已经变动的方法参数,所以无法对这些方法参数值进行验证。
8.1. 案例代码

这里,以分批读取并保存ODPS数据为例说明。其中,dataList在每次存储后,都进行了一次清除操作。
  1. /**
  2. * 读取数据
  3. *
  4. * @param <T> 模板类型
  5. * @param recordReader 记录读取器
  6. * @param batchSize 批量大小
  7. * @param dataParser 数据解析器
  8. * @param dataStorage 数据存储器
  9. * @throws IOException IO异常
  10. */
  11. public static <T> void readData(RecordReader recordReader, int batchSize,
  12.     Function<Record, T> dataParser, Consumer<List<T>> dataStorage) throws IOException {
  13.     // 依次读取数据
  14.     Record record;
  15.     List<T> dataList = new ArrayList<>(batchSize);
  16.     while (Objects.nonNull(record = recordReader.read())) {
  17.         // 解析添加数据
  18.         T data = dataParser.apply(record);
  19.         if (Objects.nonNull(data)) {
  20.             dataList.add(data);
  21.         }
  22.         // 批量存储数据
  23.         if (dataList.size() == batchSize) {
  24.             dataStorage.accept(dataList);
  25.             dataList.clear();
  26.         }
  27.     }
  28.     // 存储剩余数据
  29.     if (CollectionUtils.isNotEmpty(dataList)) {
  30.         dataStorage.accept(dataList);
  31.         dataList.clear();
  32.     }
  33. }
复制代码
8.2. 问题测试

通常环境下,我们利用ArgumentCaptor编写的测试用例如下:
  1. /**
  2. * 测试: 读取数据-正常
  3. *
  4. * @throws IOException IO异常
  5. */
  6. @Test
  7. public void testReadDataWithNormal() throws IOException {
  8.     // 模拟依赖方法
  9.     // 模拟依赖方法: recordReader.read
  10.     Record record1 = Mockito.mock(Record.class);
  11.     Record record2 = Mockito.mock(Record.class);
  12.     TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
  13.     Mockito.doReturn(record1, record2, null).when(recordReader).read();
  14.     // 模拟依赖方法: dataParser.apply
  15.     Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
  16.     Object object1 = new Object();
  17.     Object object2 = new Object();
  18.     Mockito.doReturn(object1).when(dataParser).apply(record1);
  19.     Mockito.doReturn(object2).when(dataParser).apply(record2);
  20.     // 调用测试方法
  21.     int batchSize = 2;
  22.     Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class));
  23.     OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage);
  24.     // 验证依赖方法
  25.     // 验证依赖方法: recordReader.read
  26.     Mockito.verify(recordReader, Mockito.times(3)).read();
  27.     // 验证依赖方法: dataParser.apply
  28.     Mockito.verify(dataParser).apply(record1);
  29.     Mockito.verify(dataParser).apply(record2);
  30.     // 验证依赖方法: dataStorage.test
  31.     ArgumentCaptor<List<Object>> dataListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
  32.     Mockito.verify(dataStorage).accept(dataListCaptor.capture());
  33.     Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataListCaptor.getValue());
  34. }
复制代码
执行该单元测试后,会出现以下错误:
  1. java.lang.AssertionError: 数据列表不一致 expected:<[java.lang.Object@7eaa2bc6, java.lang.Object@6dae70f9]> but was:<[]>
复制代码
因为,我们捕获的方法参数dataList只是一个对象引用,其数据内容早已被clear方法清除干净了。
8.3. 正确测试

对于这种环境,我们可以利用Mockito.doAnswer来保存这些暂时值,最后再进行统一的数据验证。
  1. /**
  2. * 测试: 读取数据-正常
  3. *
  4. * @throws IOException IO异常
  5. */
  6. @Test
  7. public void testReadDataWithNormal() throws IOException {
  8.     // 模拟依赖方法
  9.     // 模拟依赖方法: recordReader.read
  10.     Record record1 = Mockito.mock(Record.class);
  11.     Record record2 = Mockito.mock(Record.class);
  12.     TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
  13.     Mockito.doReturn(record1, record2, null).when(recordReader).read();
  14.     // 模拟依赖方法: dataParser.apply
  15.     Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
  16.     Object object1 = new Object();
  17.     Object object2 = new Object();
  18.     Mockito.doReturn(object1).when(dataParser).apply(record1);
  19.     Mockito.doReturn(object2).when(dataParser).apply(record2);
  20.     // 模拟依赖方法: dataStorage.test
  21.     List<Object> dataList = new ArrayList<>();
  22.     Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class));
  23.     Mockito.doAnswer(invocation -> dataList.addAll(invocation.getArgument(0)))
  24.         .when(dataStorage).accept(Mockito.anyList());
  25.     // 调用测试方法
  26.     int batchSize = 2;
  27.     OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage);
  28.     Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList);
  29.     // 验证依赖方法
  30.     // 验证依赖方法: recordReader.read
  31.     Mockito.verify(recordReader, Mockito.times(3)).read();
  32.     // 验证依赖方法: dataParser.apply
  33.     Mockito.verify(dataParser).apply(record1);
  34.     Mockito.verify(dataParser).apply(record2);
  35.     // 验证依赖方法: dataStorage.test
  36.     Mockito.verify(dataStorage).accept(Mockito.anyList());
  37. }
复制代码
9. 如何测试类似返回值的代码分支

在业务代码中,经常会出现差别的代码分支返回类似值的环境。这个时候,仅通过验证返回值是没法判定是否掷中了对应的代码分支的。那么,这种环境如何进行单元测试呢?
9.1. 案例代码

这里,以灰度发布服务判定方法为例说明。
  1. /**
  2. * 灰度发布服务类
  3. */
  4. @Slf4j
  5. @Service
  6. public class GrayReleaseService {
  7.     /** 定义静态常量 */
  8.     /** 灰度发布分子 */
  9.     private static final long GRAY_NUMERATOR = 0L;
  10.     /** 灰度发布分母 */
  11.     private static final long GRAY_DENOMINATOR = 10000L;
  12.     /** 注入依赖对象 */
  13.     /** 灰度发布配置 */
  14.     @Autowired
  15.     private GrayReleaseConfig grayReleaseConfig;
  16.     /**
  17.      * 是否灰度发布
  18.      *
  19.      * @param key 主键
  20.      * @param channel 渠道
  21.      * @param userId 用户标识
  22.      * @param value 取值
  23.      * @return 判断结果
  24.      */
  25.     public boolean isGrayRelease(String key, String channel, String userId, Object value) {
  26.         // 判断灰度发布取值
  27.         if (Objects.isNull(value)) {
  28.             log.info("命中灰度取值为空");
  29.             return false;
  30.         }
  31.         // 获取灰度发布映射
  32.         Map<String, GrayReleaseItem> grayReleaseMap = grayReleaseConfig.getGrayReleaseMap();
  33.         if (MapUtils.isEmpty(grayReleaseMap)) {
  34.             log.info("命中灰度发布映射为空");
  35.             return false;
  36.         }
  37.         // 获取灰度发布项
  38.         GrayReleaseItem grayReleaseItem = grayReleaseMap.get(key);
  39.         if (Objects.isNull(grayReleaseItem)) {
  40.             log.info("命中灰度发布映项为空: key={}", key);
  41.             return false;
  42.         }
  43.         // 判断渠道白名单
  44.         Set<String> channelWhiteSet = grayReleaseItem.getChannelWhiteSet();
  45.         if (CollectionUtils.isNotEmpty(channelWhiteSet) && channelWhiteSet.contains(channel)) {
  46.             log.info("命中渠道白名单灰度: key={}, channel={}", key, channel);
  47.             return true;
  48.         }
  49.         // 判断用户白名单
  50.         Set<String> userIdWhiteSet = grayReleaseItem.getUserIdWhiteSet();
  51.         if (CollectionUtils.isNotEmpty(userIdWhiteSet) && userIdWhiteSet.contains(userId)) {
  52.             log.info("命中用户白名单灰度: key={}, userId={}", key, userId);
  53.             return true;
  54.         }
  55.         // 判断灰度发布比例
  56.         long grayNumerator = Optional.ofNullable(grayReleaseItem.getGrayNumerator()).orElse(GRAY_NUMERATOR);
  57.         long grayDenominator = Optional.ofNullable(grayReleaseItem.getGrayDenominator()).orElse(GRAY_DENOMINATOR);
  58.         boolean isGray = Math.abs(Objects.hashCode(value)) % grayDenominator <= grayNumerator;
  59.         log.info("命中灰度发布比例: key={}, value={}, isGray={}", key, value, isGray);
  60.         return isGray;
  61.     }
  62. }
复制代码
9.2. 平凡测试法(不推荐)

这里,只测试了掷中渠道白名单的环境。
  1. /**
  2. * 灰度发布服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class GrayReleaseServiceTest {
  6.     /** 模拟依赖方法 */
  7.     /** 灰度发布配置 */
  8.     @Mock
  9.     private GrayReleaseConfig grayReleaseConfig;
  10.     /** 定义测试对象 */
  11.     /** 灰度发布服务 */
  12.     @InjectMocks
  13.     private GrayReleaseService grayReleaseService;
  14.     /**
  15.      * 测试: 是否灰度发布-命中渠道白名单
  16.      */
  17.     @Test
  18.     public void testIsGrayReleaseWithChannelWhiteSet() {
  19.         // 模拟依赖方法
  20.         GrayReleaseItem grayReleaseItem = new GrayReleaseItem();
  21.         grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay"));
  22.         grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456"));
  23.         Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
  24.         grayReleaseMap.put("test", grayReleaseItem);
  25.         Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
  26.         // 调用测试方法
  27.         String key = "test";
  28.         String channel = "alipay";
  29.         String userId = "123456";
  30.         Object value = 1234567890L;
  31.         Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
  32.         // 验证依赖方法
  33.         Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
  34.     }
  35. }
复制代码
在一次代码重构中,把"判定用户白名单"放在"判定渠道白名单"之前,这个单元测试是无法检测出来的。
9.3. 验证测试法(推荐)

通过对mock方法的验证,可以相对准确地确定掷中的代码分支。
  1. /**
  2. * 灰度发布服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class GrayReleaseServiceTest {
  6.     /** 模拟依赖方法 */
  7.     /** 灰度发布配置 */
  8.     @Mock
  9.     private GrayReleaseConfig grayReleaseConfig;
  10.     /** 定义测试对象 */
  11.     /** 灰度发布服务 */
  12.     @InjectMocks
  13.     private GrayReleaseService grayReleaseService;
  14.     /**
  15.      * 测试: 是否灰度发布-命中渠道白名单
  16.      */
  17.     @Test
  18.     public void testIsGrayReleaseWithChannelWhiteSet() {
  19.         // 模拟依赖方法
  20.         // 模拟依赖方法: grayReleaseItem.getChannelWhiteSet
  21.         GrayReleaseItem grayReleaseItem = Mockito.mock(GrayReleaseItem.class);
  22.         Mockito.doReturn(Sets.newHashSet("alipay")).when(grayReleaseItem).getChannelWhiteSet();
  23.         // 模拟依赖方法: grayReleaseItem.getUserIdWhiteSet
  24.         Mockito.doReturn(Sets.newHashSet("123456")).when(grayReleaseItem).getUserIdWhiteSet();
  25.         // 模拟依赖方法: grayReleaseConfig.getGrayReleaseMap
  26.         Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
  27.         grayReleaseMap.put("test", grayReleaseItem);
  28.         Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
  29.         // 调用测试方法
  30.         String key = "test";
  31.         String channel = "alipay";
  32.         String userId = "123456";
  33.         Object value = 1234567890L;
  34.         Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
  35.         // 验证依赖方法
  36.         // 验证依赖方法: grayReleaseConfig.getGrayReleaseMap
  37.         Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
  38.         // 验证依赖方法: grayReleaseItem.getChannelWhiteSet
  39.         Mockito.verify(grayReleaseItem).getChannelWhiteSet();
  40.         // 验证依赖对象
  41.         Mockito.verifyNoMoreInteractions(grayReleaseConfig, grayReleaseItem);
  42.     }
  43. }
复制代码
假如把"判定用户白名单"放在"判定渠道白名单"之前,这个单元测试会报出以下错误日记:
  1. Wanted but not invoked:
  2. grayReleaseItem.getChannelWhiteSet();
复制代码
错误日记告诉我们,grayReleaseItem.getChannelWhiteSet方法并没有被调用,所以不可能掷中渠道白名单代码分支。
9.4. 日记测试法(推荐)

对于有日记打印的代码,可以通过验证日记方法来确定掷中的代码分支,而且这种验证方法是非常简朴直白的。假如没有日记打印,我们也可以添加日记打印(可能会涉及日记存储成本的增长)。
  1. /**
  2. * 灰度发布服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class GrayReleaseServiceTest {
  6.     /** 模拟依赖方法 */
  7.     /** 日志器 */
  8.     @Mock
  9.     private Logger log;
  10.     /** 灰度发布配置 */
  11.     @Mock
  12.     private GrayReleaseConfig grayReleaseConfig;
  13.     /** 定义测试对象 */
  14.     /** 灰度发布服务 */
  15.     @InjectMocks
  16.     private GrayReleaseService grayReleaseService;
  17.     /**
  18.      * 在测试前
  19.      */
  20.     @Before
  21.     public void beforeTest() {
  22.         FieldHelper.writeStaticFinalField(GrayReleaseService.class, "log", log);
  23.     }
  24.     /**
  25.      * 测试: 是否灰度发布-命中渠道白名单
  26.      */
  27.     @Test
  28.     public void testIsGrayReleaseWithChannelWhiteSet() {
  29.         // 模拟依赖方法
  30.         GrayReleaseItem grayReleaseItem = new GrayReleaseItem();
  31.         grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay"));
  32.         grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456"));
  33.         Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
  34.         grayReleaseMap.put("test", grayReleaseItem);
  35.         Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
  36.         // 调用测试方法
  37.         String key = "test";
  38.         String channel = "alipay";
  39.         String userId = "123456";
  40.         Object value = 1234567890L;
  41.         Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
  42.         // 验证依赖方法
  43.         // 验证依赖方法: grayReleaseConfig.getGrayReleaseMap
  44.         Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
  45.         // 验证依赖方法: log.info
  46.         Mockito.verify(log).info("命中渠道白名单灰度: key={}, channel={}", key, channel);
  47.         // 验证依赖对象
  48.         Mockito.verifyNoInteractions(log, grayReleaseConfig);
  49.     }
  50. }
复制代码
假如把"判定用户白名单"放在"判定渠道白名单"之前,这个单元测试会报出以下错误日记:
  1. Argument(s) are different! Wanted:
  2. log.info(
  3.     "命中渠道白名单灰度: key={}, channel={}",
  4.     "test",
  5.     "alipay"
  6. );
  7. -> at ...
  8. Actual invocations have different arguments:
  9. log.info(
  10.     "命中用户白名单灰度: key={}, userId={}",
  11.     "test",
  12.     "123456"
  13. );
复制代码
错误日记告诉我们,我们期望掷中渠道白名单灰度代码分支,实际却掷中的是用户白名单灰度代码分支。
10. 如何测试多线程并发编程

Java多线程并发编程,就是通过多个线程同时执行多个任务来缩短执行时间、提高执行效率的方法。在JDK1.8中,新增了CompletableFuture类,实现了对任务编排的本事——可以轻松地组织差别任务的运行顺序、规则及方式。
10.1. 案例代码

这里,以并行获取批量交易订单为例说明。
  1. /**
  2. * 交易订单服务类
  3. */
  4. @Slf4j
  5. @Service
  6. public class TradeOrderService {
  7.     /** 定义静态常量 */
  8.     /** 等待时间(毫秒) */
  9.     private static final long WAIT_TIME = 1000L;
  10.     /** 注入依赖对象 */
  11.     /** 交易订单DAO */
  12.     @Autowired
  13.     private TradeOrderDAO tradeOrderDAO;
  14.     /** 执行器服务 */
  15.     @Autowired
  16.     private ExecutorService executorService;
  17.     /**
  18.      * 获取交易订单列表
  19.      *
  20.      * @param orderIdList 订单标识列表
  21.      * @return 交易订单列表
  22.      */
  23.     public List<TradeOrderVO> getTradeOrders(List<Long> orderIdList) {
  24.         // 检查订单标识列表
  25.         if (CollectionUtils.isEmpty(orderIdList)) {
  26.             return Collections.emptyList();
  27.         }
  28.         // 获取交易订单期望
  29.         List<CompletableFuture<TradeOrderVO>> futureList = orderIdList.stream()
  30.             .map(this::getTradeOrder).collect(Collectors.toList());
  31.         // 聚合交易订单期望
  32.         CompletableFuture<List<TradeOrderVO>> joinFuture =
  33.             CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]))
  34.                 .thenApply(v -> futureList.stream().map(CompletableFuture::join).collect(Collectors.toList()));
  35.         // 返回交易订单列表
  36.         try {
  37.             return joinFuture.get(WAIT_TIME, TimeUnit.MILLISECONDS);
  38.         } catch (InterruptedException e) {
  39.             Thread.currentThread().interrupt();
  40.             log.warn("获取订单中断异常", e);
  41.             throw new BusinessException("获取订单中断异常", e);
  42.         } catch (ExecutionException | TimeoutException | RuntimeException e) {
  43.             log.warn("获取订单其它异常", e);
  44.             throw new BusinessException("获取订单其它异常", e);
  45.         }
  46.     }
  47.     /**
  48.      * 获取交易订单
  49.      *
  50.      * @param orderId 订单标识
  51.      * @return 交易订单期望
  52.      */
  53.     private CompletableFuture<TradeOrderVO> getTradeOrder(Long orderId) {
  54.         return CompletableFuture.supplyAsync(() -> tradeOrderDAO.get(orderId), executorService)
  55.             .thenApply(TradeOrderService::convertTradeOrder);
  56.     }
  57.     /**
  58.      * 转化交易订单
  59.      *
  60.      * @param tradeOrder 交易订单DO
  61.      * @return 交易订单VO
  62.      */
  63.     private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) {
  64.         TradeOrderVO tradeOrderVO = new TradeOrderVO();
  65.         tradeOrderVO.setId(tradeOrder.getId());
  66.         // ...
  67.         return tradeOrderVO;
  68.     }
  69. }
复制代码
10.2. 测试用例

对于多线程并发编程,假如采集mock静态方法的方式进行单元测试,将会使单元测试用例变得非常复杂。通过实践总结,采用注入线程池的方式,将会使单元测试用例变得非常简朴。
  1. /**
  2. * 交易订单服务测试类
  3. */
  4. @RunWith(MockitoJUnitRunner.class)
  5. public class TradeOrderServiceTest {
  6.     /** 定义静态常量 */
  7.     /** 资源路径 */
  8.     private static final String RESOURCE_PATH = "testTradeOrderService/";
  9.     /** 模拟依赖对象 */
  10.     /** 交易订单DAO */
  11.     @Mock
  12.     private TradeOrderDAO tradeOrderDAO;
  13.     /** 执行器服务 */
  14.     @Spy
  15.     private ExecutorService executorService = Executors.newFixedThreadPool(10);
  16.     /** 定义测试对象 */
  17.     /** 交易订单服务 */
  18.     @InjectMocks
  19.     private TradeOrderService tradeOrderService;
  20.     /**
  21.      * 测试: 获取交易订单列表-正常
  22.      */
  23.     @Test
  24.     public void testGetTradeOrdersWithNormal() {
  25.         // 模拟依赖方法
  26.         // 模拟依赖方法: tradeOrderDAO.get
  27.         String path = RESOURCE_PATH + "testGetTradeOrdersWithNormal/";
  28.         String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderMap.json");
  29.         Map<Long, TradeOrderDO> tradeOrderMap = JSON.parseObject(text, new TypeReference<Map<Long, TradeOrderDO>>() {});
  30.         Mockito.doAnswer(invocation -> tradeOrderMap.get(invocation.getArgument(0)))
  31.             .when(tradeOrderDAO).get(Mockito.anyLong());
  32.         // 调用测试方法
  33.         text = ResourceHelper.getResourceAsString(getClass(), path + "orderIdList.json");
  34.         List<Long> orderIdList = JSON.parseArray(text, Long.class);
  35.         List<TradeOrderVO> tradeOrderList = tradeOrderService.getTradeOrders(orderIdList);
  36.         text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json");
  37.         Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList));
  38.         // 验证依赖方法
  39.         // 验证依赖方法: tradeOrderDAO.get
  40.         ArgumentCaptor<Long> orderIdCaptor = ArgumentCaptor.forClass(Long.class);
  41.         Mockito.verify(tradeOrderDAO, Mockito.atLeastOnce()).get(orderIdCaptor.capture());
  42.         Assert.assertEquals("订单标识列表不一致", orderIdList, orderIdCaptor.getAllValues());
  43.     }
  44. }
复制代码
11. 附录

11.1. 引入Maven单测包

  1. <dependency>
  2.     <groupId>junit</groupId>
  3.     <artifactId>junit</artifactId>
  4.     <version>4.13.1</version>
  5.     <scope>test</scope>
  6. </dependency>
  7. <dependency>
  8.     <groupId>org.mockito</groupId>
  9.     <artifactId>mockito-core</artifactId>
  10.     <version>3.3.3</version>
  11.     <scope>test</scope>
  12. </dependency>
  13. <dependency>
  14.     <groupId>org.powermock</groupId>
  15.     <artifactId>powermock-module-junit4</artifactId>
  16.     <version>2.0.9</version>
  17.     <scope>test</scope>
  18. </dependency>
  19. <dependency>
  20.     <groupId>org.powermock</groupId>
  21.     <artifactId>powermock-api-mockito2</artifactId>
  22.     <version>2.0.9</version>
  23.     <scope>test</scope>
  24. </dependency>
复制代码
11.2. 使用到的工具方法

11.2.1. 以字符串方式获取资源

ResourceHelper.getResourceAsString(以字符串方式获取资源)通过Apache的IOUtils.toString方法实现,提供以字符串方式获取资源的功能。
  1. /**
  2. * 资源辅助类
  3. */
  4. public final class ResourceHelper {
  5.     /**
  6.      * 以字符串方式获取资源
  7.      *
  8.      * @param clazz 类
  9.      * @param name 资源名称
  10.      * @return 字符串
  11.      */
  12.     public static <T> String getResourceAsString(Class<T> clazz, String name) {
  13.         try (InputStream is = clazz.getResourceAsStream(name)) {
  14.             return IOUtils.toString(is, StandardCharsets.UTF_8);
  15.         } catch (IOException e) {
  16.             throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e);
  17.         }
  18.     }
  19. }
复制代码
11.2.2. 写入静态常量字段

FieldHelper.writeStaticFinalField(写入静态常量字段)通过Apache的FieldUtils相关方法实现,提供写入静态常量字段的功能。
  1. /**
  2. * 字段辅助类
  3. */
  4. public final class FieldHelper {
  5.     /**
  6.      * 写入静态常量字段
  7.      *
  8.      * @param clazz 类
  9.      * @param fieldName 字段名称
  10.      * @param fieldValue 字段取值
  11.      */
  12.     public static void writeStaticFinalField(Class<?> clazz, String fieldName, Object fieldValue) {
  13.         try {
  14.             Field field = clazz.getDeclaredField(fieldName);
  15.             FieldUtils.removeFinalModifier(field);
  16.             FieldUtils.writeStaticField(field, fieldValue, true);
  17.         } catch (NoSuchFieldException | IllegalAccessException e) {
  18.             throw new UnsupportedOperationException("写入静态常量字段异常", e);
  19.         }
  20.     }
  21. }
复制代码
后记

其着实很久之前,有人就盼望我整理一个单元测试案例库。我迟迟没有举措,重要原因如下:

  • 单元测试案例是无穷无尽的,如何体系化地呈现给读者是个大工程;
  • 单元测试案例必须典型、公道、故意义,如何构建这些案例也很斲丧精力。
现在,终于鼓起勇气整理这篇《Java单元测试典型案例集锦》,重要是因为单元测试案例是单元测试告急的一环,也是为了给我的Java单元测试系列文章划上一个完善的句号。
最后,根据本文主题吟诗一首:
   《单测案例》单元测试百家说,  
案例总结方法多。  
芳草满园花满目,  
绿肥红瘦自斟酌。  原文链接
本文为阿里云原创内容,未经允许不得转载。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

北冰洋以北

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表