OceaBase开发者大会落地上海!4月20日共同探索数据库前沿趋势!报名戳 了解详情
写点什么

编码那些事:代码覆盖的 15 种典型情景

  • 2012-10-09
  • 本文字数:4515 字

    阅读完需:约 15 分钟

篇首语:《编码那些事》是 InfoQ 中文站新推出的一个专栏,目的是为国内社区的开发者提供一个讨论软件开发过程点点滴滴的平台,欢迎感兴趣的读者投稿至 editors@cn.infoq.com

代码覆盖(Code Coverage)为何物?相信程序员特别是测试人员不陌生,很多人都喜欢用代码覆盖来驱动测试的开展和完善。确实代码覆盖可以找出测试疏漏和代码问题,但是单纯的代码覆盖率高低并不能直接反映代码质量的好坏。大多我们的努力方向都是找出那些没有覆盖到的代码,然后补充用例,完善测试。而摆在我们面前的问题是:是否我们已经充分认识到哪些不需要、不能、必须被覆盖?只有对代码覆盖的各种情景了然于胸,才能不盲目乐观于代码覆盖率之高,悲观于代码覆盖率之低。在实践中(本文面向主要 Java 语言,基于 emma 工具),梳理可知,对于代码覆盖我们可能都会遇到以下 15 种典型情景:

1. 代码覆盖

即代码所有路径被经过,这种需要注意的是:不应该覆盖而被覆盖的情况。例如某种特殊异常就是不期望遇到的,但是遇到了,异常处理的代码也覆盖了,这时,我们应该追溯异常产生的根本原因,而不因覆盖了就直接忽略。

提示:不仅要关注未覆盖的代码,也要关注覆盖的,特别是偶然覆盖的代码。

2. 废弃的功能

一些功能点随着产品版本不断更新,可能会被取消,这部分功能可以直接移除,保留只会让代码看起来越冗余。如果某天需要参考或找回删除的那些代码,CVS/SVN 工具就搞定了。

提示:不用的功能不需要覆盖,要及时删除,不通过保留或者注释的方式残留在代码中。

3. 工具类(助手类)、常量类等的私有构造器

工具类和常量类共有的特征是对外开放的都是静态方法,调用方法的时候,无需创建实例,所以推荐实践是创建一个 private 的构造器方法。这导致类的构造器代码无法覆盖(不考虑反射等方式)。

相反,如果某天发现对于这样的类覆盖率为 100%,那检查下是否代码写的不规范: 用默认构造器,然后通过实例来调用静态方法。

例 1:工具类

复制代码
public final class StringUtil {
public static String concatWithSpace(String... strings) {
return concat(MarkConstants.SPACE, strings);
}
public static String concatWithSemicolon(String... strings) {
return concat(MarkConstants.SEMICOLON, strings);
}
private StringUtil() {
}
}

例 2:常量类

复制代码
public final class MarkConstants {
/**
* {@value}
*/
public static final String SEMICOLON = ";";
private MarkConstants() {
}
}

提示:工具类(助手类)、常量类等的私有构造器不能被覆盖

4. 日志级别配置

日志级别不同,覆盖率高低也不同。在产品部署中,很少将日志的级别设成 debug,因为日志占用磁盘空间会增长很快。只在做一些问题跟踪、调试时才会调高日志级别。

所以环境使用不同的日志级别,也会导致一些日志代码没有覆盖。如以下示例程序,不打开 debug 级别无法覆盖部分代码:

复制代码
public static String formatPath(String path) {
ValidationUtil.checkString(path);
String returnPath = path.trim();
if (!returnPath.startsWith(SPLIT))
returnPath = SPLIT + returnPath;
if (returnPath.endsWith(SPLIT))
returnPath = returnPath.substring(0, returnPath.length() - 1);
if (LOGGER.isDebugEnabled())
LOGGER.debug(String
.format("[util]convert [%s] to [%s]", path, returnPath));
return returnPath;
}

那么这部分代码需要覆盖嘛?需要。 假设代码误写成:

复制代码
LOGGER.debug(String.format("[util]convert [%] to [%s]", path, returnPath));

某天日志级别设为 debug,就会发现报错。类似的还有日志中经常输出某个对象信息,但是该对象可能是 null,从而抛出空指针异常。

提示:日志也是代码的一部分,需要通过调整日志级别来覆盖。

5. JVM 等参数

程序的配置参数会直接影响代码路径覆盖,不仅包括业务上的一些配置,也包括依赖平台的参数,例如 JVM 参数除了会影响性能,也会影响代码的覆盖情况,例如断言相关参数:

复制代码
-ea[:<packagename>...|:<classname>] 和 -da[:<packagename>...|:<classname>] </classname></packagename></classname></packagename>

分别是启用和关闭用户断言(-esa/-eda,针对系统断言),在 JAVA 中断言是默认关闭的,所以涉及断言的代码默认无法覆盖。

提示:一些代码路径能否覆盖与 JVM 等参数有关,需要通过调整参数来覆盖

6. main() 方法

一些程序员喜欢临时写一个 main() 方法方便于测试,完成测试后寻思以后还能方便测试就留了下来。导致产品代码中的这些代码无法被覆盖。在产品代码中,应该删除这些,部署的毕竟是产品代码,不是测试代码。

提示:main() 方不需要被覆盖,产品代码不保留测试代码

7. 编码习惯写法

在编码过程中,常常有一些习惯写法,最常见的比如:(1) 覆盖 toString() 方法; (2) 以意义配对形式写一些方法:比如数据连接中 Connect() 搭配 DisConnect(), 枚举中常用的 toString() 搭配 fromString(),这些惯用的写法告诉读者一些涵义,但是不见得所有的方法都必须被调用,例如在产品应用中,我们可能启动起一个周期性的 job,但是本身尚未添加“取消“功能(或本来就不需要停止)。自然也就无法调用: 但是它应该不应该存在? 笔者认为作为完整的功能应该存在。 类似的还有异常定义的时候,会定义很多重载的方法,虽然不见得每个都调用,但是不定某天就会被调用。

提示:编码习惯写法造成的未覆盖代码需要被覆盖,是代码的一部分。

8. 项目的使用方式

下面两种使用方式会造成代码不能全部被覆盖:

  1. 客户端 Jar 方式:部分代码作为客户端 Jar 包形式提供给他人使用;
  2. 分布式系统交互:分布式系统之间存在交互时,例如从系统 1 复制文件到系统 2,如果始终按照从 1 到 2 的顺序,又仅仅统计系统 1 的代码覆盖肯定不能覆盖全部。 需要覆盖的代码虽出于一处,但是使用方式不同也会导致在不合并覆盖数据情况下代码未覆盖。

提示:项目使用方式造成的代码覆盖统计数据分散需要通过合并数据来覆盖。

9. 常用最佳实践

一些很难覆盖的最佳实践:例如对于一些资源(IO,lock)的释放,可能直接 try…catch 然后记录异常,这些异常一般很难发生。

复制代码
public static void close(InputStream inputStream)
{
try {
inputStream.close();
} catch (Exception e) {
LOGGER.warn("fail to close inputstream");
}
}

提示:常用最佳实践可以不覆盖。

10. 被拒绝的馈赠

在接口 / 抽象类定义的时候,有时候定义的一些方法子类并没有都实现,即常说的被拒绝的馈赠(Refused Bequest),这种问题如果是为了短期扩展需求多加了一些方法也可接受,否则还是需要重新继承体系设计。

提示:子类未使用“馈赠”,无需覆盖,需重新审视继承体系结构。

11. 代码覆盖工具未做合并

做代码覆盖时,往往工具本身不支持“合并”的功能,这导致以下问题存在:

时间上:

  1. 例如对于拥有 cache 的系统: 系统经过一段时间运行后,重新测试得到的代码覆盖往往不包括 cache miss 的情况。
  2. 手工测试问题:每次统计都需要重新完成全部手工测试,否则将丢失数据。

空间上:

  1. 负载均衡:现在大多系统应用都采用负载均衡技术,如果测试时间不够长且只统计一台系统的代码覆盖情况,往往不全面。

提示:代码覆盖本身要支持“合并”功能,对多个系统、不同时间的数据进行合并,才能覆盖的完整全面。

12. 系统逻辑重复

这里可分为两种情况:

  1. 组件之间重复:上层系统可能会对数据合法性做检验,但是下层系统出于系统的独立性目标,也可能对数据做二次校验,但是作为一个完整系统进行 end-to-end 测试时,就无法覆盖二次校验的代码;针对这种情况,需要拆开成独立组件进行测试。
  2. 组件内部重复:同一组件内多层重复逻辑确实可以纠正代码,例如在对于某个数据做多次同一类型校验,这种问题常出现于多人协同编码又缺乏沟通的情况中。

提示:逻辑重复导致的部分未覆盖要分辨是组件之间还是组件内部冗余,组件之间则需要覆盖,组件内部则要修改代码。

13. 代码写法

有时候某些代码的写法,也会导致无法覆盖,例如对于代码调用顺序:多个类调用读取配置文件,而稍晚些调用的再次判断配置文件是否初始化,自然为已初始化。再如对于单例,某些代码写成这样:

复制代码
private static SingleInstance INSTANCE = new SingleInstance();
public static SingleInstance getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleInstance();
return INSTANCE;
}

提示:代码写法造成的未覆盖,需要审查下是代码问题。

14. 隐式的分支

当代码中含有隐式的分支时,往往很难 100% 覆盖,例如上文提到的断言 assert,貌似只有一句,但是即使启用断言仍然无法 100% 覆盖,例如下例还是显示黄色:分支未覆盖。

复制代码
public class AssertCodeCoverage {
public void verify(Boolean b) {
assert b;
}
}

究其原因,查看编译后的 class 可知,存在第三条指令:判断是否启用断言。在实际应用中,要么启用要么关闭,所以不可能覆盖所有分支。只能说启用断言,或许能提高指令覆盖率,下图为启用及关闭断言的覆盖率对比:

复制代码
public void verify(java.lang.Boolean b);
0 getstatic com.test.coverage.AssertCodeCoverage.$assertionsDisabled : boolean [16]
3 ifne 21
6 aload_1 [b]
7 invokevirtual java.lang.Boolean.booleanValue() : boolean [28]
10 ifne 21
13 new java.lang.AssertionError [33]
16 dup
17 invokespecial java.lang.AssertionError() [35]
20 athrow
21 return
}

0x9a ifne 当栈顶 int 型数值不等于 0 时跳转。因此,从这个角度来说,想覆盖断言,不仅要关闭断言完成测试用例,还要在开启断言情况下完成测试。

提示:隐式的分支(黄色)需要分析未覆盖分支。

15. 不在覆盖范围内

下面两种类型的代码不在代码覆盖统计范围内:

  1. Java 接口,接口里面都是抽象方法的结合,不含有任何代码细节;
  2. 不含有可执行 java 字节码的方法:抽象方法和本地 native 方法。
复制代码
abstract class AbstractService {
abstract String getString(); // 不做统计
String getName() { return getString().trim(); }
}
public class BaseService extends AbstractService {
@Override
String getString() { return "zookeeper"; }
}

提示:不在统计范围内的直接忽视。

小结:

通过对上面 15 种典型情况的概括,相信大家对代码覆盖的常见情景已有大概印像,在实际分析中,可以按照以下规则进行:

  1. 内容:包 -> 类 -> 方法 -> 代码;
  2. 优先级: 核心业务类 -> 普通业务类 -> 工具助手类 -> 常量类;

经过不断的分析和推敲,相信大家得到的不仅是一个较高的代码覆盖率,更是对代码质量的一份信心。

作者介绍:

傅健,思科软件工程师,Java、开源爱好者


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-10-09 07:1112001

评论

发布
暂无评论
发现更多内容

IT外包服务的未来发展趋势

Ogcloud

IT 外包公司 外包项目 IT 运维

直播预告丨大模型如何在健康医疗中挖出大大的花?

京东科技开发者

人工智能 大模型

低代码就是不写代码吗?到底什么是真正的低代码平台?

代码生成器研究

华为云耀云服务器L实例:中小企业小程序开发的高效解决方案

YG科技

如何绕过某讯手游保护系统并从内存中获取Unity3D引擎的Dll文件

雪奈椰子

编程那么难,为什么不弄个大众一学就会的计算机

代码生成器研究

关于 SLO,我们需要了解什么?

观测云

SLA SLO

带键扫的LED专用驱动方案

二哈侠

为什么越来越多的企业选择IT外包服务?

Ogcloud

外包 IT 外包公司 外包项目 IT 运维

什么是真正的低代码?

代码生成器研究

音视频FAQ(一):视频直播卡顿

ZEGO即构

CDN RTC 实时音视频 音视频技术 直播间

项目管理必备的时间轴软件!这10款协同工具有口皆碑。

彭宏豪95

项目管理 效率工具 在线白板 办公软件 绘图软件

华为云耀云服务器L实例:企业建站与小程序开发的新纪元

YG科技

Open AI 砸了所有人的饭碗吗?

代码生成器研究

StarRocks 存算分离最佳实践,让降本增效更简单

StarRocks

数据库 数据分析 StarRocks

ubuntu20.4服务器安装mysql社区版并开放3306端口

百度搜索:蓝易云

MySQL 云计算 Linux ubuntu 云服务器

2023 IoTDB 用户大会倒计时 3 天 | 1 分钟让你了解 IoTDB!

Apache IoTDB

大模型+搜索:一盘跳棋和三位选手

脑极体

AI

低代码可以减少程序员哪些工作?

代码生成器研究

MongoDB助力腾讯游戏 优化游戏开发体验

技术咖和技术渣

SQL 数据操作技巧:SELECT INTO、INSERT INTO SELECT 和 CASE 语句详解

小万哥

MySQL 数据库 程序员 sql 后端开发

华为云耀云服务器L实例:中小企业数字化转型的加速器

轶天下事

华为云耀云服务器L实例:中小企业数字化转型的关键伙伴

轶天下事

企业上云请认准华为云这款服务器,数字化时代安全可靠的引擎

轶天下事

MySQL数据库,RDBMS术语,使用说明和报错解决的详细讲解?

百度搜索:蓝易云

MySQL 云计算 运维 RDBMS 云服务器

小程序开发新选择!华为云耀云服务器L实例快人一步

YG科技

稳定可靠,华为云服务器引领数字化时代

YG科技

加速数智化升级,华为云这款服务器让企业上云更轻松

轶天下事

打工人福音:未来每周只需工作3天?比尔盖茨这样评价AI...

代码生成器研究

面向对象编程的弊端是什么?

代码生成器研究

从零实现一个在线相亲APP(缓解相亲尴尬的神器)

音视频开发_AIZ

编码那些事:代码覆盖的15种典型情景_Java_傅健_InfoQ精选文章