阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

编码那些事:代码覆盖的 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:1112005

评论

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

【量化】实战获取资产组合理论模型的数据源

恒生LIGHT云社区

资源 量化投资 量化

java开发之java开发环境的快速构建

@零度

Java java开发环境

Vue.js 的九个性能优化技巧

编程江湖

Vue 大前端

uni-app技术分享| uniapp实现直播旁路推流

anyRTC开发者

uni-app 音视频 视频直播 视频通话 旁路推流

学习react源码 征服面试官

buchila11

React

管人理事

张老蔫

28天写作

第三天用 Mac,我安装了这些玩意

悟空聊架构

Mac 28天写作 悟空聊架构 12月日更

年度重磅!华为云2021应用构建技术实践精选集,免费下载!

华为云开发者联盟

数据库 大数据 云原生 数字化 华为云

好习惯影响孩子的一生

Tiger

28天写作

Android C++系列:Linux网络(二)通信过程

轻口味

c++ android 28天写作 12月日更

伴鱼基于 Flink 构建数据集成平台的设计与实现

Apache Flink

大数据 flink 编程 后端 实时计算

大厂面试算法题之链表

程序员学长

高效设计一个LRU

bigsai

数据结构 算法 LRU

Redis分布式锁的正确使用

编程江湖

redis java编程

Go语言学习查缺补漏ing Day6

恒生LIGHT云社区

golang 编程语言

API标准化对Dapr的重要性

行云创新

数据分析从零开始实战专栏导航@老表

老表

Python 数据库 数据分析 pandas 数据分析从零开始实战

Perforce用户文章转载:用了P4这一招,九成问题能自救

龙智—DevSecOps解决方案

报错 perforce

Go语言逆向技术:恢复函数名称算法

华为云开发者联盟

二进制 函数 go语言 逆向分析 恢复函数名称

通过接口上传文件到百度网盘

为自己带盐

28天写作 百度网盘 签约计划第二季 12月日更

万众提供素材,万众联合创作

mtfelix

28天写作

dart系列之:浏览器中的舞者,用dart发送HTTP请求

程序那些事

flutter 浏览器 dart 程序那些事 12月日更

前端面试题之模块化开发

@零度

大前端 模块化

搞定react源码 惊艳面试官

buchila11

React

大数据开发之Hadoop家族都有谁

@零度

大数据 hadoop

架构师实战营模块一作业

圈圈gor

「架构实战营」

李飞飞力荐:阿里巴巴高可用数据库解决方案

博文视点Broadview

给弟弟的信第7封|离开大学的喜与悲

大菠萝

28天写作

基于MRS-Hudi构建数据湖的典型应用场景介绍

华为云开发者联盟

数据仓库 数据湖 华为云 Apache Hudi MRS-Hudi

【报名中】我们把你对 ShardingSphere 的好奇,都放在这场 Meetup 中

SphereEx

数据库 开源社区 ShardingSphere Meetup SphereEx

模块一课程作业

李晓笛

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