Java单元测试典型案例集锦

Posted 阿里云云栖号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java单元测试典型案例集锦相关的知识,希望对你有一定的参考价值。

前言

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

  1. 无效验证问题:不进行有效地验证数据对象、抛出异常和调用方法。
  2. 测试方法问题:不知道如何测试某些典型案例,要么错误地测试、要么不进行测试、要么利用集成测试来保证覆盖率。比如:
    ①错误地测试:利用测试返回节点占比来测试随机负载均衡策略;
    ②不进行测试:没有人针对虚基类进行单独地测试;
    ③利用集成测试:很多案例中,直接注入真实依赖对象,然后一起进行集成测试。

针对无效验证问题,在我的ATA文章《那些年,我们写过的无效单元测试》中,介绍了如何识别和解决单元测试无效验证问题,这里就不再累述了。在本文中,作者收集了一些的Java单元测试典型案例,主要是为了解决这个测试方法问题

1. 如何测试不可达代码

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

1.1. 案例代码

在下面的案例代码中,就存在一段不可达代码。

/**
 * 交易订单服务类
 */
@Service
public class TradeOrderService 
    /** 注入依赖对象 */
    /** 交易订单DAO */
    @Autowired
    private TradeOrderDAO tradeOrderDAO;

    /**
     * 查询交易订单
     * 
     * @param orderQuery 订单查询
     * @return 交易订单分页
     */
    public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) 
        // 查询交易订单
        // 查询交易订单: 总共数量
        Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
        // 查询交易订单: 数据列表
        List<TradeOrderVO> dataList = null;
        if (NumberHelper.isPositive(totalSize)) 
            List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
            if (CollectionUtils.isNotEmpty(tradeOrderList)) 
                dataList = convertTradeOrders(tradeOrderList);
            
        

        // 返回分页数据
        return new PageDataVO<>(totalSize, dataList);
    

    /**
     * 转化交易订单列表
     * 
     * @param tradeOrderList 交易订单DO列表
     * @return 交易订单VO列表
     */
    private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) 
        // 检查订单列表
        if (CollectionUtils.isEmpty(tradeOrderList)) 
            return Collections.emptyList();
        

        // 转化订单列表
        return tradeOrderList.stream().map(TradeOrderService::convertTradeOrder)
            .collect(Collectors.toList());
    

    /**
     * 转化交易订单
     * 
     * @param tradeOrder 交易订单DO
     * @return 交易订单VO
     */
    private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) 
        TradeOrderVO tradeOrderVO = new TradeOrderVO();
        tradeOrderVO.setId(tradeOrder.getId());
        // ...
        return tradeOrderVO;
    

由于方法convertTradeOrders(转化交易订单列表)传入的参数tradeOrderList(交易订单列表)不可能为空,所以“检查订单列表”这段代码是不可达代码。

// 检查订单列表
        if (CollectionUtils.isEmpty(tradeOrderList)) 
            return Collections.emptyList();
        

1.2. 方案1:删除不可达代码(推荐)

最简单的方法,就是删除方法convertTradeOrders(转化交易订单列表)中的不可达代码。

/**
 * 转化交易订单列表
 * 
 * @param tradeOrderList 交易订单DO列表
 * @return 交易订单VO列表
 */
private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) 
    return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder)
        .collect(Collectors.toList());

1.3. 方案2:利用不可达代码(推荐)

还有一种方法,把不可达代码利用起来,可以降低方法queryTradeOrder(查询交易订单)的代码复杂度。

/**
 * 查询交易订单
 * 
 * @param orderQuery 订单查询
 * @return 交易订单分页
 */
public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) 
    // 查询交易订单
    // 查询交易订单: 总共数量
    Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
    // 查询交易订单: 数据列表
    List<TradeOrderVO> dataList = null;
    if (NumberHelper.isPositive(totalSize)) 
        List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
        dataList = convertTradeOrders(tradeOrderList);
    

    // 返回分页数据
    return new PageDataVO<>(totalSize, dataList);

1.4. 方案3:测试不可达代码(不推荐)

对于一些祖传代码,有些小伙伴不敢删除代码。在某些情况下,可以针对不可达代码进行单独测试。

/**
 * 测试: 转化交易订单列表-交易订单列表为空
 * 
 * @throws Exception 异常信息
 */
@Test
public void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception 
    List<TradeOrderDO> tradeOrderList = null;
    Assert.assertSame("交易订单列表不为空", Collections.emptyList(),
        Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList));

2. 如何测试内部的构造方法

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

2.1. 代码案例

按照题目要求,编写了一个简单的随机负载均衡策略。

/**
 * 随机负载均衡策略类
 */
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy 
    /**
     * 选择服务节点
     * 
     * @param serverNodeList 服务节点列表
     * @param clientRequest 客户请求
     * @return 服务节点
     */
    @Override
    public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) 
        // 检查节点列表
        if (CollectionUtils.isEmpty(serverNodeList)) 
            return null;
        

        // 计算随机序号
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        int randomIndex = new Random().nextInt(totalWeight);

        // 查找对应节点
        for (ServerNode serverNode : serverNodeList) 
            int currentWeight = serverNode.getWeight();
            if (currentWeight > randomIndex) 
                return serverNode;
            
            randomIndex -= currentWeight;
        
        return null;
    

2.2. 方法1:直接测试法(不推荐)

有些参赛选手,不知道如何测试随机数(主要原因是因为不知道如何Mock构造方法),所以直接利用测试返回节点占比来测试随机负载均衡策略。

/**
 * 随机负载均衡策略测试类
 */
@RunWith(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest 
    /** 定义测试对象 */
    /** 随机负载均衡策略 */
    @InjectMocks
    private RandomLoadBalanceStrategy randomLoadBalanceStrategy;

    /**
     * 测试: 选择服务节点-随机
     * 
     * @throws Exception 异常信息
     */
    @Test
    public void testSelectNodeWithRandom() throws Exception 
        int nodeCount1 = 0;
        int nodeCount2 = 0;
        int nodeCount3 = 0;
        ServerNode serverNode1 = new ServerNode(1L, 10);
        ServerNode serverNode2 = new ServerNode(2L, 20);
        ServerNode serverNode3 = new ServerNode(3L, 30);
        List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
        ClientRequest clientRequest = new ClientRequest();
        for (int i = 0; i < 1000; i++) 
            ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
            if (serviceNode == serverNode1) 
                nodeCount1++;
             else if (serviceNode == serverNode2) 
                nodeCount2++;
             else if (serviceNode == serverNode3) 
                nodeCount3++;
            
        
        Assert.assertEquals("节点1占比不一致", serverNode1.getWeight() / 60.0D, nodeCount1 / 1000.0D, 1E-3D);
        Assert.assertEquals("节点2占比不一致", serverNode2.getWeight() / 60.0D, nodeCount2 / 1000.0D, 1E-3D);
        Assert.assertEquals("节点3占比不一致", serverNode3.getWeight() / 60.0D, nodeCount3 / 1000.0D, 1E-3D);
    

这个测试用例主要存在3个问题:

  1. 执行时间长:被测方法需要被执行1000遍;
  2. 不一定通过:由于随机数是随机,并不一定保证比例,所以导致测试用例并不一定通过;
  3. 测试目标变更:单测测试的测试目标应该是负载均衡逻辑,现在感觉测试目标变成了Random方法。

2.3. 方法2:直接mock法(不推荐)

用过PowerMockito高级功能的,知道如何去Mock构造方法。

/**
 * 随机负载均衡策略测试类
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest(RandomLoadBalanceStrategy.class)
public class RandomLoadBalanceStrategyTest 
    /** 定义测试对象 */
    /** 随机负载均衡策略 */
    @InjectMocks
    private RandomLoadBalanceStrategy randomLoadBalanceStrategy;

    /**
     * 测试: 选择服务节点-第一个节点
     * 
     * @throws Exception 异常信息
     */
    @Test
    public void testSelectNodeWithFirstNode() throws Exception 
        // 模拟依赖方法
        Random random = Mockito.mock(Random.class);
        Mockito.doReturn(9).when(random).nextInt(Mockito.anyInt());
        PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(random);

        // 调用测试方法
        ServerNode serverNode1 = new ServerNode(1L, 10);
        ServerNode serverNode2 = new ServerNode(2L, 20);
        ServerNode serverNode3 = new ServerNode(3L, 30);
        List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
        ClientRequest clientRequest = new ClientRequest();
        ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
        Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);

        // 验证依赖方法
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        Mockito.verify(random).nextInt(totalWeight);
    

但是,这个测试用例也存在问题:需要把RandomLoadBalanceStrategy加到@PrepareForTest注解中,导致Jacoco无法统计单元测试的覆盖率。

2.4. 方法3:工具方法法(推荐)

其实,随机数生成,还有很多工具方法,我们可以利用工具方法RandomUtils.nextInt代替构造方法。

2.4.1. 重构代码

/**
 * 随机负载均衡策略类
 */
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy 
    /**
     * 选择服务节点
     * 
     * @param serverNodeList 服务节点列表
     * @param clientRequest 客户请求
     * @return 服务节点
     */
    @Override
    public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) 
        // 检查节点列表
        if (CollectionUtils.isEmpty(serverNodeList)) 
            return null;
        

        // 计算随机序号
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        int randomIndex = RandomUtils.nextInt(0, totalWeight);

        // 查找对应节点
        for (ServerNode serverNode : serverNodeList) 
            int currentWeight = serverNode.getWeight();
            if (currentWeight > randomIndex) 
                return serverNode;
            
            randomIndex -= currentWeight;
        
        return null;
    

2.4.2. 测试用例

/**
 * 随机负载均衡策略测试类
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest(RandomUtils.class)
public class RandomLoadBalanceStrategyTest 
    /** 定义测试对象 */
    /** 随机负载均衡策略 */
    @InjectMocks
    private RandomLoadBalanceStrategy randomLoadBalanceStrategy;

    /**
     * 测试: 选择服务节点-第一个节点
     */
    @Test
    public void testSelectNodeWithFirstNode() 
        // 模拟依赖方法
        PowerMockito.mockStatic(RandomUtils.class);
        PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);

        // 调用测试方法
        ServerNode serverNode1 = new ServerNode(1L, 10);
        ServerNode serverNode2 = new ServerNode(2L, 20);
        ServerNode serverNode3 = new ServerNode(3L, 30);
        List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
        ClientRequest clientRequest = new ClientRequest();
        ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
        Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);

        // 验证依赖方法
        PowerMockito.verifyStatic(RandomUtils.class);
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        RandomUtils.nextInt(0, totalWeight);
    

2.5. 方法4:注入对象法(推荐)

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

2.5.1. 重构代码

/**
 * 随机负载均衡策略类
 */
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy 
    /** 注入依赖对象 */
    /** 随机数提供者 */
    @Autowired
    private RandomProvider randomProvider;

    /**
     * 选择服务节点
     * 
     * @param serverNodeList 服务节点列表
     * @param clientRequest 客户请求
     * @return 服务节点
     */
    @Override
    public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) 
        // 检查节点列表
        if (CollectionUtils.isEmpty(serverNodeList)) 
            return null;
        

        // 计算随机序号
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        int randomIndex = randomProvider.nextInt(totalWeight);

        // 查找对应节点
        for (ServerNode serverNode : serverNodeList) 
            int currentWeight = serverNode.getWeight();
            if (currentWeight > randomIndex) 
                return serverNode;
            
            randomIndex -= currentWeight;
        
        return null;
    

2.5.2. 测试用例

/**
 * 随机负载均衡策略测试类
 */
@RunWith(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest 
    /** 模拟依赖方法 */
    /** 随机数提供者 */
    @Mock
    private RandomProvider randomProvider;

    /** 定义测试对象 */
    /** 随机负载均衡策略 */
    @InjectMocks
    private RandomLoadBalanceStrategy randomLoadBalanceStrategy;

    /**
     * 测试: 选择服务节点-第一个节点
     */
    @Test
    public void testSelectNodeWithFirstNode() 
        // 模拟依赖方法
        Mockito.doReturn(9).when(randomProvider).nextInt(Mockito.anyInt());

        // 调用测试方法
        ServerNode serverNode1 = new ServerNode(1L, 10);
        ServerNode serverNode2 = new ServerNode(2L, 20);
        ServerNode serverNode3 = new ServerNode(3L, 30);
        List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
        ClientRequest clientRequest = new ClientRequest();
        ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
        Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);

        // 验证依赖方法
        int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
        Mockito.verify(randomProvider).nextInt(totalWeight);
    

3. 如何测试虚基类和子类

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

3.1. 案例代码

这里,以Diamond属性配置加载为例说明。

3.1.1. 虚基类定义

首先,定义一个通用的虚基类,定义了需要子类实现的虚方法,实现了通用的配置解析方法。

/**
 * 虚属性回调类
 *
 * @param <T> 配置类型
 */
@Slf4j
public 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();

3.1.2. 子类实现

其次,定义了具体配置的子类,简单地实现了基类定义的虚方法。

/**
 * 例子配置回调类
 */
@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;
    

3.2. 方法1:联合测试法(不推荐)

最简单的测试方法,就是通过子类对虚基类进行联合测试,这样同时把子类和虚基类都测试了。

/**
 * 例子配置回调测试类
 */
@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));
    

3.3. 方法2:独立测试法(推荐)

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

3.3.1. 基类测试

虚基类的单元测试,专注于虚基类的通用配置解析。

/**
 * 虚属性回调测试类
 */
@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();
    

3.3.2. 子类测试

子类的单元测试,专注于对虚基类定义虚方法的实现,避免了每个子类都要针对虚基类的通用配置解析进行测试。

/**
 * 例子配置回调测试类
 */
@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());
    

4. 如何测试策略模式的策略服务

4.1. 案例代码

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

4.1.1. 策略接口

首先,定义一个负载均衡策略接口。

/**
 * 负载均衡策略接口
 */
public interface LoadBalanceStrategy 
    /**
     * 支持策略类型
     * 
     * @return 策略类型
     */
    LoadBalanceStrategyType supportType();

    /**
     * 选择服务节点
     * 
     * @param serverNodeList 服务节点列表
     * @param clientRequest 客户请求
     * @return 服务节点
     */
    ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest);

4.1.2. 策略服务

其次,实现一个负载均衡策略服务,根据负载均衡策略类型选择对应的负载均衡策略来执行。

/**
 * 负载均衡服务类
 */
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);
    

4.1.3. 策略实现

最后,实现一个随机负载均衡策略实现类。

/**
 * 随机负载均衡策略类
 */
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;
    

4.2. 方法1:联合测试法(不推荐)

很多时候,策略模式是用来优化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);
    

策略模式的联合测试法主要有以下问题:

  1. 策略服务依赖于策略实现,需要了解策略实现的具体逻辑,才能写出策略服务的单元测试;
  2. 对于策略服务来说,该单元测试并不关心策略服务的实现,这是黑盒测试而不是白盒测试。

如果我们对策略服务进行以下破坏,该单元测试并不能发现问题:

  1. strategyMap没有根据strategyList生成;
  2. strategyMap.get(strategyType)为空时,初始化一个RandomLoadBalanceStrategy。
/**
 * 负载均衡服务类
 */
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);
    

4.3. 方法2:独立测试法(推荐)

现在,先假设策略实现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);
    

其实,不只是策略模式,很多模式下都不建议联合测试,而是推荐采用独立的单元测试。因为单元测试是白盒测试——一种专注于自身代码逻辑的测试。

5. 如何测试Lambda表达式

在有些单元测试中,Lambda表达式并不一定被执行,所以导致Lambda表达式没有被测试。

5.1. 案例代码

这里,以从ODPS中查询用户交易订单为例说明。

5.1.1. 被测代码

交易订单查询服务,其中有一段转化订单的Lambda表达式。

/**
 * 交易订单服务
 */
@Service
public class TradeOrderService 
    /** 注入依赖对象 */
    /** 交易ODPS服务 */
    @Autowired
    private TradeOdpsService tradeOdpsService;

    /**
     * 查询交易订单
     * 
     * @param userId 用户标识
     * @param maxCount 最大数量
     * @return 交易订单列表
     */
    public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) 
        String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
        String sql = String.format(format, userId, maxCount);
        return tradeOdpsService.executeQuery(sql, record -> 
            TradeOrderVO tradeOrder = new TradeOrderVO();
            tradeOrder.setId(record.getBigint("id"));
            // ...
            return tradeOrder;
        );
    

5.1.2. 依赖代码

封装了通用的ODPS查询方法。

/**
 * 交易ODPS服务类
 */
@Slf4j
@Service
public class TradeOdpsService 
    /** 注入依赖对象 */
    /** 交易ODPS */
    @Resource(name = "tradeOdps")
    private Odps tradeOdps;

    /**
     * 执行查询
     *
     * @param <T> 模板类型
     * @param sql SQL语句
     * @param dataParser 数据解析器
     * @return 查询结果列表
     */
    public <T> List<T> executeQuery(String sql, Function<Record, T> dataParser) 
        try 
            // 打印提示信息
            log.info("开始执行ODPS数据查询...");

            // 执行ODPS查询
            Instance instance = SQLTask.run(tradeOdps, sql);
            instance.waitForSuccess();

            // 获取查询结果
            List<Record> recordList = SQLTask.getResult(instance);
            if (CollectionUtils.isEmpty(recordList)) 
                log.info("完成执行ODPS数据查询: totalSize=0");
                return Collections.emptyList();
            

            // 依次读取数据
            List<T> dataList = new ArrayList<>();
            for (Record record : recordList) 
                T data = dataParser.apply(record);
                if (Objects.nonNull(data)) 
                    dataList.add(data);
                
            

            // 打印提示信息
            log.info("完成执行ODPS数据查询: totalSize=", dataList.size());

            // 返回查询结果
            return dataList;
         catch (OdpsException e) 
            log.warn("执行ODPS数据查询异常: sql=", sql, e);
            throw new BusinessException("执行ODPS数据查询异常", e);
        
    

5.2. 方法1:直接测试法(不推荐)

按照通用的单元测试方法进行测试,发现Lambda表达式没有被测试到。

/**
 * 交易订单服务测试类
 */
@RunWith(MockitoJUnitRunner.class)
public class TradeOrderServiceTest 
    /** 定义静态常量 */
    /** 资源路径 */
    private static final String RESOURCE_PATH = "testTradeOrderService/";

    /** 模拟依赖对象 */
    /** 交易ODPS服务 */
    @Mock
    private TradeOdpsService tradeOdpsService;

    /** 定义测试对象 */
    /** 交易订单服务 */
    @InjectMocks
    private TradeOrderService tradeOrderService;

    /**
     * 测试: 查询交易订单-正常
     */
    @Test
    public void testQueryTradeOrderWithNormal() 
        // 模拟依赖方法
        // 模拟依赖方法: tradeOdpsService.executeQuery
        List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
        Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());

        // 调用测试方法
        Long userId = 12345L;
        Integer maxCount = 100;
        Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));

        // 验证依赖方法
        // 验证依赖方法: tradeOdpsService.executeQuery
        String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
        String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
        Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
    

5.3. 方法2:联合测试法(不推荐)

有人建议,可以把TradeOrderService(交易订单服务)和TradeOdpsService(交易ODPS服务)联合测试,这样就可以保证Lambda表达式被测试到。

/**
 * 交易订单服务测试类
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest(SQLTask.class)
public class TradeOrderServiceTest 
    /** 定义静态常量 */
    /** 资源路径 */
    private static final String RESOURCE_PATH = "testTradeOrderService/";

    /** 模拟依赖对象 */
    /** 交易ODPS */
    @Mock(name = "tradeOdps")
    private Odps tradeOdps;

    /** 定义测试对象 */
    /** 交易ODPS服务 */
    @InjectMocks
    private TradeOdpsService tradeOdpsService = Mockito.spy(TradeOdpsService.class);
    /** 交易订单服务 */
    @InjectMocks
    private TradeOrderService tradeOrderService;

    /**
     * 测试: 查询交易订单-正常
     * 
     * @throws OdpsException ODPS异常
     */
    @Test
    public void testQueryTradeOrderWithNormal() throws OdpsException 
        // 模拟依赖方法
        PowerMockito.mockStatic(SQLTask.class);
        // 模拟依赖方法: SQLTask.run
        Instance instance = Mockito.mock(Instance.class);
        PowerMockito.when(SQLTask.run(Mockito.eq(tradeOdps), Mockito.anyString())).thenReturn(instance);
        // 模拟依赖方法: SQLTask.getResult
        Record record1 = PowerMockito.mock(Record.class);
        Record record2 = PowerMockito.mock(Record.class);
        List<Record> recordList = Arrays.asList(record1, record2);
        PowerMockito.when(SQLTask.getResult(instance)).thenReturn(recordList);
        // 模拟依赖方法: record.getString
        Mockito.doReturn(1L).when(record1).getBigint("id");
        Mockito.doReturn(2L).when(record2).getBigint("id");

        // 调用测试方法
        Long userId = 12345L;
        Integer maxCount = 100;
        List<TradeOrderVO> tradeOrderList = tradeOrderService.queryTradeOrder(userId, maxCount);
        String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
        String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json");
        Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList));

        // 验证依赖方法
        PowerMockito.verifyStatic(SQLTask.class);
        // 验证依赖方法: SQLTask.run
        text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
        SQLTask.run(tradeOdps, text);
        // 验证依赖方法: SQLTask.getResult
        SQLTask.getResult(instance);
        // 验证依赖方法: instance.waitForSuccess
        Mockito.verify(instance).waitForSuccess();
        // 验证依赖方法: record.getString
        Mockito.verify(record1).getBigint("id");
        Mockito.verify(record2).getBigint("id");
    

主要问题:需要了解TradeOdpsService.executeQuery(执行查询)方法的逻辑并构建单元测试用例,导致TradeOrderService.queryTradeOrder(查询交易订单)方法的单测测试用例非常复杂。

5.3. 方法3:重构测试法(推荐)

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

5.3.1. 重构代码

提取Lambda表达式为convertTradeOrder(转化交易订单)方法。

/**
 * 交易订单服务类
 */
@Service
public class TradeOrderService 
    /** 注入依赖对象 */
    /** 交易ODPS服务 */
    @Autowired
    private TradeOdpsService tradeOdpsService;

    /**
     * 查询交易订单
     * 
     * @param userId 用户标识
     * @param maxCount 最大数量
     * @return 交易订单列表
     */
    public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) 
        String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
        String sql = String.format(format, userId, maxCount);
        return tradeOdpsService.executeQuery(sql, TradeOrderService2::convertTradeOrder);
    

    /**
     * 转化交易订单
     * 
     * @param record ODPS记录
     * @return 交易订单
     */
    private static TradeOrderVO convertTradeOrder(Record record) 
        TradeOrderVO tradeOrder = new TradeOrderVO();
        tradeOrder.setId(record.getBigint("id"));
        // ...
        return tradeOrder;
    

5.3.2. 测试用例

针对queryTradeOrder(查询交易订单)方法和convertTradeOrder(转化交易订单)方法分别进行单元测试。

/**
 * 交易订单服务测试类
 */
@RunWith(MockitoJUnitRunner.class)
public class TradeOrderServiceTest 
    /** 定义静态常量 */
    /** 资源路径 */
    private static final String RESOURCE_PATH = "testTradeOrderService/";

    /** 模拟依赖对象 */
    /** 交易ODPS服务 */
    @Mock
    private TradeOdpsService tradeOdpsService;

    /** 定义测试对象 */
    /** 交易订单服务 */
    @InjectMocks
    private TradeOrderService tradeOrderService;

    /**
     * 测试: 查询交易订单-正常
     */
    @Test
    public void testQueryTradeOrderWithNormal() 
        // 模拟依赖方法
        // 模拟依赖方法: tradeOdpsService.executeQuery
        List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
        Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());

        // 调用测试方法
        Long userId = 12345L;
        Integer maxCount = 100;
        Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));

        // 验证依赖方法
        // 验证依赖方法: tradeOdpsService.executeQuery
        String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
        String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
        Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
    

    /**
     * 测试: 转化交易订单
     * 
     * @throws Exception 异常信息
     */
    @Test
    public void testConvertTradeOrder() throws Exception 
        // 模拟依赖方法
        Long id = 12345L;
        Record record = Mockito.mock(Record.class);
        Mockito.doReturn(id).when(record).getBigint("id");

        // 调用测试方法
        TradeOrderVO tradeOrder = Whitebox.invokeMethod(TradeOrderService2.class, "convertTradeOrder", record);
        Assert.assertEquals("订单标识不一致", id, tradeOrder.getId());

        // 验证依赖方法
        Mockito.verify(record).getBigint("id");
    

6. 如何测试链式调用

在日常编码过程中,很多人都喜欢使用链式调用,这样可以让代码变得更简洁。

6.1. 案例代码

这里,通过修改后的添加跨域支持代码来举例说明(原方法没有返回值)。

/**
 * 跨域辅助类
 */
public class CorsHelper 
    /** 定义静态常量 */
    /** 最大生命周期 */
    private static final long MAX_AGE = 3600L;

    /**
     * 添加跨域支持
     * 
     * @param registry 跨域注册器
     * @return 跨域注册
     */
    public static CorsRegistration addCorsMapping(CorsRegistry registry) 
        return registry.addMapping("/**")
            .allowedOrigins("*")
            .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
            .allowCredentials(true)
            .maxAge(MAX_AGE)
            .allowedHeaders("*");
    

6.2. 方法1:普通测试法(不推荐)

正常情况下,每一个依赖对象及其调用方法都要mock,编写的代码如下:

/**
 * 跨域辅助测试类
 */
public class CorsHelperTest 

    /**
     * 测试: 添加跨域支持
     */
    @Test
    public void testAddCorsMapping() 
        // 模拟依赖方法
        CorsRegistry registry = Mockito.mock(CorsRegistry.class);
        CorsRegistration registration = Mockito.mock(CorsRegistration.class);
        Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
        Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any());
        Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any());
        Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean());
        Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong());
        Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any());

        // 调用测试方法
        Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));

        // 验证依赖方法
        Mockito.verify(registry).addMapping("/**");
        Mockito.verify(registration).allowedOrigins("*");
        Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
        Mockito.verify(registration).allowCredentials(true);
        Mockito.verify(registration).maxAge(3600L);
        Mockito.verify(registration).allowedHeaders("*");
    

6.3. 方法2:利用RETURNS_DEEP_STUBS参数法(推荐)

对于链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_DEEP_STUBS参数,实现链式调用返回对象的自动mock。利用Mockito.RETURNS_DEEP_STUBS参数编写的测试用例如下:

/**
 * 跨域辅助测试类
 */
public class CorsHelperTest 
    /**
     * 测试: 添加跨域支持
     */
    @Test
    public void testAddCorsMapping() 
        // 模拟依赖方法
        CorsRegistry registry = Mockito.mock(CorsRegistry.class, Answers.RETURNS_DEEP_STUBS);
        CorsRegistration registration = Mockito.mock(CorsRegistration.class);
        Mockito.when(registry.addMapping(Mockito.anyString())
            .allowedOrigins(Mockito.any())
            .allowedMethods(Mockito.any())
            .allowCredentials(Mockito.anyBoolean())
            .maxAge(Mockito.anyLong())
            .allowedHeaders(Mockito.any()))
            .thenReturn(registration);

        // 调用测试方法
        Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));

        // 验证依赖方法
        Mockito.verify(registry.addMapping("/**")
            .allowedOrigins("*")
            .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
            .allowCredentials(true)
            .maxAge(3600L))
            .allowedHeaders("*");
    

代码说明:

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

6.4. 方法3:利用RETURNS_SELF参数法(推荐)

对于相同返回值的链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_SELF参数,实现链式调用返回对象的自动mock,而且还能返回同一mock对象。利用Mockito.RETURNS_SELF参数编写的测试用例如下:

/**
 * 跨域辅助测试类
 */
public class CorsHelperTest 
    /**
     * 测试: 添加跨域支持
     */
    @Test
    public void testAddCorsMapping() 
        // 模拟依赖方法
        CorsRegistry registry = Mockito.mock(CorsRegistry.class);
        CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF);
        Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());

        // 调用测试方法
        Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));

        // 验证依赖方法
        Mockito.verify(registry).addMapping("/**");
        Mockito.verify(registration).allowedOrigins("*");
        Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
        Mockito.verify(registration).allowCredentials(true);
        Mockito.verify(registration).maxAge(3600L);
        Mockito.verify(registration).allowedHeaders("*");
    

代码说明:

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

方法对比:

  1. 普通测试法:mock调用方法语句较多;
  2. 利用RETURNS_DEEP_STUBS参数法:mock调用方法语句较少,适合于链式调用返回不同值;
  3. 利用RETURNS_SELF参数法:mock调用方法语句最少,适合于链式调用返回相同值。

7. 如何测试相同参数返回不同值

在有些场景下,存在相同参数多次调用返回不同值的情况,比如:读取文本文件的readLine方法。

7.1. 案例代码

这里,以ODPS的RecordReader为例,读取每一行数据记录。

/**
 * 读取数据
 *
 * @param <T> 模板类型
 * @param recordReader 记录读取器
 * @param dataParser 数据解析器
 * @return 数据列表
 * @throws IOException IO异常
 */
public static <T> List<T> readData(RecordReader recordReader, Function<Record, T> dataParser) throws IOException 
    Record record;
    List<T> dataList = new ArrayList<>();
    while (Objects.nonNull(record = recordReader.read())) 
        T data = dataParser.apply(record);
        if (Objects.nonNull(data)) 
            dataList.add(data);
        
    
    return dataList;

7.2. 测试用例

为了mock相同参数返回不同值,需要使用到Mockito.doReturn的可变数组功能。

/**
 * 测试: 读取数据-正常
 * 
 * @throws IOException IO异常
 */
@Test
public void testReadDataWithNormal() throws IOException 
    // 模拟依赖方法
    // 模拟依赖方法: recordReader.read
    Record record1 = Mockito.mock(Record.class);
    Record record2 = Mockito.mock(Record.class);
    TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
    Mockito.doReturn(record1, record2, null).when(recordReader).read();
    // 模拟依赖方法: dataParser.apply
    Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
    Object object1 = new Object();
    Object object2 = new Object();
    Mockito.doReturn(object1).when(dataParser).apply(record1);
    Mockito.doReturn(object2).when(dataParser).apply(record2);

    // 调用测试方法
    List<Object> dataList = OdpsHelper.readData(recordReader, dataParser);
    Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList);

    // 验证依赖方法
    // 验证依赖方法: recordReader.read
    Mockito.verify(recordReader, Mockito.times(3)).read();
    // 验证依赖方法: dataParser.apply
    Mockito.verify(dataParser).apply(record1);
    Mockito.verify(dataParser).apply(record2);

8. 如何测试已变更的方法参数值

在单元测试中,我们通常通过ArgumentCaptor进行方法参数捕获并验证。但是,在有些情况下,我们捕获的可能是已经变更的方法参数,所以无法对这些方法参数值进行验证。

8.1. 案例代码

这里,以分批读取并保存ODPS数据为例说明。其中,dataList在每次存储后,都进行了一次清除操作。

/**
 * 读取数据
 *
 * @param <T> 模板类型
 * @param recordReader 记录读取器
 * @param batchSize 批量大小
 * @param dataParser 数据解析器
 * @param dataStorage 数据存储器
 * @throws IOException IO异常
 */
public static <T> void readData(RecordReader recordReader, int batchSize,
    Function<Record, T> dataParser, Consumer<List<T>> dataStorage) throws IOException 
    // 依次读取数据
    Record record;
    List<T> dataList = new ArrayList<>(batchSize);
    while (Objects.nonNull(record = recordReader.read())) 
        // 解析添加数据
        T data = dataParser.apply(record);
        if (Objects.nonNull(data)) 
            dataList.add(data);
        

        // 批量存储数据
        if (dataList.size() == batchSize) 
            dataStorage.accept(dataList);
            dataList.clear();
        
    

    // 存储剩余数据
    if (CollectionUtils.isNotEmpty(dataList)) 
        dataStorage.accept(dataList);
        dataList.clear();
    

8.2. 问题测试

通常情况下,我们利用ArgumentCaptor编写的

以上是关于Java单元测试典型案例集锦的主要内容,如果未能解决你的问题,请参考以下文章

java单元测试案例怎么写,全网最新

java单元测试案例怎么写,隔壁都馋哭了

Java高级技术 单元测试(JUnit4)

java中springboot集成junit编写单元测试(实战+坑)

java怎么做单元测试,紧急!

如何使用Spring + EasyMock做Java单元测试