当前位置:网站首页 > Java基础 > 正文

java基础程序实例



简介:近期,阿里巴巴CTO线卓越工程小组举办了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。

作者 | 陈昌毅(常意)

来源 | 阿里开发者公众号

前言

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

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

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

1. java基础程序实例 如何测试不可达代码

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

1.1. 案例代码

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

 
 

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

 
 

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

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

 
 

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

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

 
 

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

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

 
 

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

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

2.1. 代码案例

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

 
 

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

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

 
 

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

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

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

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

 
 

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

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

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

2.4.1. 重构代码

 
 

2.4.2. 测试用例

 
 

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

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

2.5.1. 重构代码

 
 

2.5.2. 测试用例

 
 

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

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

3.1. 案例代码

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

3.1.1. 虚基类定义

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

 
 

3.1.2. 子类实现

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

 
 

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

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

 
 

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

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

3.3.1. 基类测试

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

 
 

3.3.2. 子类测试

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

 
 

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

4.1. 案例代码

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

4.1.1. 策略接口

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

 
 

4.1.2. 策略服务

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

 
 

4.1.3. 策略实现

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

 
 

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

很多时候,策略模式是用来优化if-else代码的。所以,采用联合测试法(策略服务和策略实现同时测试),能够最大限度地利用原有的单元测试代码。

 
 

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

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

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

  1. strategyMap没有根据strategyList生成;
  2. strategyMap.get(strategyType)为空时,初始化一个RandomLoadBalanceStrategy。
 
 

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

现在,先假设策略实现RandomLoadBalanceStrategy(随机负载均衡策略)不存在,直接对策略服务LoadBalanceService(负载均衡服务)独立测试,而且是分别对构造方法和selectNode(选择服务节点)方法进行独立测试。其中,测试构造方法是为了保证strategyMap构造逻辑没有问题,测试selectNode(选择服务节点)方法是为了保证选择策略逻辑没有问题。

 
 

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

5. 如何测试Lambda表达式

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

5.1. 案例代码

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

5.1.1. 被测代码

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

 
 

5.1.2. 依赖代码

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

 
 

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

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

 
 

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

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

 
 

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

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

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

5.3.1. 重构代码

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

 
 

5.3.2. 测试用例

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

 
 

6. 如何测试链式调用

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

6.1. 案例代码

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

 
 

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

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

 
 

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

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

 
 

代码说明:

  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参数编写的测试用例如下:

 
 

代码说明:

  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为例,读取每一行数据记录。

 
 

7.2. 测试用例

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

 
 

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

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

8.1. 案例代码

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

 
 

8.2. 问题测试

通常情况下,我们利用ArgumentCaptor编写的测试用例如下:

 
 

执行该单元测试后,会出现以下错误:

 
 

因为,我们捕获的方法参数dataList只是一个对象引用,其数据内容早已被clear方法清除干净了。

8.3. 正确测试

对于这种情况,我们可以利用Mockito.doAnswer来保存这些临时值,最后再进行统一的数据验证。

 
 

9. 如何测试相同返回值的代码分支

在业务代码中,经常会出现不同的代码分支返回相同值的情况。这个时候,仅通过验证返回值是没法判断是否命中了对应的代码分支的。那么,这种情况如何进行单元测试呢?

9.1. 案例代码

这里,以灰度发布服务判定方法为例说明。

 
 

9.2. 普通测试法(不推荐)

这里,只测试了命中渠道白名单的情况。

 
 

在一次代码重构中,把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试是无法检测出来的。

9.3. 验证测试法(推荐)

通过对mock方法的验证,可以相对准确地确定命中的代码分支。

 
 

如果把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试会报出以下错误日志:

 
 

错误日志告诉我们,grayReleaseItem.getChannelWhiteSet方法并没有被调用,所以不可能命中渠道白名单代码分支。

9.4. 日志测试法(推荐)

对于有日志打印的代码,可以通过验证日志方法来确定命中的代码分支,而且这种验证方法是非常简单直白的。如果没有日志打印,我们也可以添加日志打印(可能会涉及日志存储成本的增加)。

 
 

如果把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试会报出以下错误日志:

 
 

错误日志告诉我们,我们期望命中渠道白名单灰度代码分支,实际却命中的是用户白名单灰度代码分支。

10. 如何测试多线程并发编程

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

10.1. 案例代码

这里,以并行获取批量交易订单为例说明。

 
 

10.2. 测试用例

对于多线程并发编程,如果采集mock静态方法的方式进行单元测试,将会使单元测试用例变得非常复杂。通过实践总结,采用注入线程池的方式,将会使单元测试用例变得非常简单。

 
 

11. 附录

11.1. 引入Maven单测包

 
 

11.2. 使用到的工具方法

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

ResourceHelper.getResourceAsString(以字符串方式获取资源)通过Apache的IOUtils.toString方法实现,提供以字符串方式获取资源的功能。

 
 

11.2.2. 写入静态常量字段

FieldHelper.writeStaticFinalField(写入静态常量字段)通过Apache的FieldUtils相关方法实现,提供写入静态常量字段的功能。

 
 

后记

其实在很久之前,有人就希望我整理一个单元测试案例库。我迟迟没有行动,主要原因如下:

  1. 单元测试案例是无穷无尽的,如何系统化地呈现给读者是个大工程;
  2. 单元测试案例必须典型、合理、有意义,如何构建这些案例也很消耗精力。

现在,终于鼓起勇气整理这篇《Java单元测试典型案例集锦》,主要是因为单元测试案例是单元测试重要的一环,也是为了给我的Java单元测试系列文章划上一个完美的句号。

最后,根据本文主题吟诗一首:

《单测案例》单元测试百家说,
案例总结方法多。
芳草满园花满目,
绿肥红瘦自斟酌。
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

  • 上一篇: java se基础试题
  • 下一篇: java基础做时钟
  • 版权声明


    相关文章:

  • java se基础试题2025-04-20 12:18:00
  • java基础类实例2025-04-20 12:18:00
  • java语言基础试题2025-04-20 12:18:00
  • java基础语法第3讲2025-04-20 12:18:00
  • java语法基础语句2025-04-20 12:18:00
  • java基础做时钟2025-04-20 12:18:00
  • java基础的简称2025-04-20 12:18:00
  • java入门基础题2025-04-20 12:18:00
  • java学习基础2025-04-20 12:18:00
  • java基础易错题2025-04-20 12:18:00