写点什么

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

评论

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

如何快速分类整理电脑文件

TroyLiu

文件管理 文件整理 电脑文件 文件分类 快速整理文件

质量基础设施(NQI)“一站式”服务平台开发搭建

源中瑞-龙先生

NQI 质量基础设施“一站式”

新手模拟实现bind

前端树洞

面试 大前端 js this

机器学习- 吴恩达Andrew Ng 编程作业技巧 -John 易筋 ARTS 打卡 Week 50

John(易筋)

ARTS 打卡计划

计算机专业的应届生想进大厂做开发有多难?

Java架构师迁哥

架构解析|网易自研新一代大规模分布式传输网

网易云信

分布式 音视频

用了这么多年 Gitlab,可能还不了解这些知识

郭旭东

DevOps gitlab 极狐GitLabs

一文讲懂服务的优雅重启和更新

万俊峰Kevin

微服务 web开发 Go 语言 优雅停机

助力初创企业加速升级,华为云初创扶持计划微光训练营南京站开营仪式成功举办

科技热闻

一口气了解【2021 阿里云峰会】重磅发布

阿里云视频云

阿里云

深入浅出负载均衡

vivo互联网技术

负载均衡 高可用 服务器 hash

a.docker

xujiangniao

Docker

阿里架构师手肛6个月,总结出17W字操作系统、网络教程(导图+笔记)

Java架构师迁哥

五层验证系统,带你预防区块链业务漏洞

华为云开发者联盟

区块链 智能合约 漏洞 可信 自免疫系统

流程即代码:低代码 & 云研发 IDE —— Uncode

Phodal

ide 云开发 云研发

阿里P9分享:基于JDK 8 源码剖析

Java架构师迁哥

野生程序员高考试卷,试试你能打多少分?

北游学Java

程序员 高考

毕业设计So Easy:基于Java Web学生选课系统

不脱发的程序猿

Java web 毕业设计 学生选课系统

来自Linux老学员的经验分享,新生必看!

学神来啦

Linux 运维 安全 虚拟机

怒肝最新保姆级前端学习路线,速成贴心全面!

程序员鱼皮

CSS JavaScript Vue 大前端 React

拼搏26天刷完了阿里大佬的Java面试合集1000题,拿到了月薪30K的offer

Java 程序员 架构 面试

IDEA使用

xujiangniao

Java IDEA

EBean ORM 框架介绍-2.字段加密、更新日志和历史记录

Barry的异想世界

jpa Ebean 字段加密 更新日志 历史记录

信息流动过程中的聚类问题

Ryan Zheng

云钉一体应用创新:音视频如何带来灵活高效的协同体验

阿里云视频云

阿里云 音视频

去阿里/腾讯/字节面试P7Java岗时,需要掌握哪些技术栈?

Java架构师迁哥

深入讲解RxJava响应式编程框架,背压问题的几种应对模式

小Q

Java 学习 编程 架构 面试

牛掰!阿里首席架构师用7部分讲明白了Java百亿级高并发系统(全彩版小册开源)

Java架构追梦

Java 学习 阿里巴巴 架构 百亿级并发架构设计

10次面试9次被刷?吃透这500道大厂Java高频面试题后,怒斩offer

Java 程序员 架构 面试

阿里分享:全网最详细的一篇SpringCloud总结

Java架构师迁哥

2021上半年1000道大厂高频面试题汇总(Java岗)

Java架构师迁哥

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