追求代码质量: 不要被覆盖报告所迷惑
测试覆盖工具对单元测试具有重要的意义,但是经常被误用。这个月,andrew glover 会在他的新系列 —— 追求代码质量 中向您介绍值得参考的专家意见。第一部分深入地介绍覆盖报告中数字的真实含义。然后他会提出您可以尽早并经常地利用覆盖来确保代码质量的三个方法。
您还记得以前大多数开发人员是如何追求代码质量的吗。在那时,有技巧地放置 main() 方法被视为灵活且适当的测试方法。经历了漫长的道路以后,现在自动测试已经成为高质量代码开发的基本保证,对此我很感谢。但是这还不是我所要感谢的全部。java? 开发人员现在拥有很多通过代码度量、静态分析等方法来度量代码质量的工具。我们甚至已经设法将重构分类成一系列便利的模式!
所有的这些新的工具使得确保代码质量比以前简单得多,不过您还需要知道如何使用它们。在这个系列中,我将重点阐述有关保证代码质量的一些有时看上去有点神秘的东西。除了带您一起熟悉有关代码质量保证的众多工具和技术之外,我还将为您说明:
定义并有效度量最影响质量的代码方面。 设定质量保证目标并照此规划您的开发过程。 确定哪个代码质量工具和技术可以满足您的需要。 实现最佳实践(清除不好的),使确保代码质量及早并经常地 成为开发实践中轻松且有效的方面。 在这个月,我将首先看看 java 开发人员中最流行也是最容易的质量保证工具包:测试覆盖度量。
谨防上当
这是一个晚上鏖战后的早晨,大家都站在饮水机边上。开发人员和管理人员们了解到一些经过良好测试的类可以达到超过 90% 的覆盖率,正在高兴地互换着 nfl 风格的点心。团队的集体信心空前高涨。从远处可以听到 “放任地重构吧” 的声音,似乎缺陷已成为遥远的记忆,响应性也已微不足道。但是一个很小的反对声在说:
女士们,先生们,不要被覆盖报告所愚弄。
现在,不要误解我的意思:并不是说使用测试覆盖工具是愚蠢的。对单元测试范例,它是很重要的。不过更重要的是您如何理解所得到的信息。许多开发团队会在这儿犯第一个错。
高覆盖率只是表示执行了很多的代码,并不意味着这些代码被很好地 执行。如果您关注的是代码的质量,就必须精确地理解测试覆盖工具能做什么,不能做什么。然后您才能知道如何使用这些工具去获取有用的信息。而不是像许多开发人员那样,只是满足于高覆盖率。
测试覆盖度量
测试覆盖工具通常可以很容易地添加到确定的单元测试过程中,而且结果可靠。下载一个可用的工具,对您的 ant 和 maven 构建脚本作一些小的改动,您和您的同事就有了在饮水机边上谈论的一种新报告:测试覆盖报告。当 foo 和 bar 这样的程序包令人惊奇地显示高 覆盖率时,您可以得到不小的安慰。如果您相信至少您的部分代码可以保证是 “没有 bug” 的,您会觉得很安心。但是这样做是一个错误。
存在不同类型的覆盖度量,但是绝大多数的工具会关注行覆盖,也叫做语句覆盖。此外,有些工具会报告分支覆盖。通过用一个测试工具执行代码库并捕获整个测试过程中与被 “触及” 的代码对应的数据,就可以获得测试覆盖度量。然后这些数据被合成为覆盖报告。在 java 世界中,这个测试工具通常是 junit 以及名为 cobertura、emma 或 clover 等的覆盖工具。
行覆盖只是指出代码的哪些行被执行。如果一个方法有 10 行代码,其中的 8 行在测试中被执行,那么这个方法的行覆盖率是 80%。这个过程在总体层次上也工作得很好:如果一个类有 100 行代码,其中的 45 行被触及,那么这个类的行覆盖率就是 45%。同样,如果一个代码库包含 10000 个非注释性的代码行,在特定的测试运行中有 3500 行被执行,那么这段代码的行覆盖率就是 35%。
报告分支覆盖 的工具试图度量决策点(比如包含逻辑 and 或 or 的条件块)的覆盖率。与行覆盖一样,如果在特定方法中有两个分支,并且两个分支在测试中都被覆盖,那么您可以说这个方法有 100% 的分支覆盖率。
问题是,这些度量有什么用?很明显,很容易获得所有这些信息,不过您需要知道如何使用它们。一些例子可以阐明我的观点。
代码覆盖在活动
我在清单 1 中创建了一个简单的类以具体表述类层次的概念。一个给定的类可以有一连串的父类,例如 vector,它的父类是 abstractlist,abstractlist 的父类又是 abstractcollection,abstractcollection 的父类又是 object:
清单 1. 表现类层次的类
package com.vanward.adana.hierarchy;
import java.util.arraylist;
import java.util.collection;
import java.util.iterator;
public class hierarchy {
private collection classes;
private class baseclass;
public hierarchy() {
super();
this.classes = new arraylist();
}
public void addclass(final class clzz){
this.classes.add(clzz);
}
/**
* @return an array of class names as strings
*/
public string[] gethierarchyclassnames(){
final string[] names = new string[this.classes.size()];
int x = 0;
for(iterator iter = this.classes.iterator(); iter.hasnext();){
class clzz = (class)iter.next();
names[x++] = clzz.getname();
}
return names;
}
public class getbaseclass() {
return baseclass;
}
public void setbaseclass(final class baseclass) {
this.baseclass = baseclass;
}
}
正如您看到的,清单 1 中的 hierarchy 类具有一个 baseclass 实例以及它的父类的集合。清单 2 中的 hierarchybuilder 通过两个复制 buildhierarchy 的重载的 static 方法创建了 hierarchy 类。
清单 2. 类层次生成器
package com.vanward.adana.hierarchy;
public class hierarchybuilder {
private hierarchybuilder() {
super();
}
public static hierarchy buildhierarchy(final string clzzname)
throws classnotfoundexception{
final class clzz = class.forname(clzzname, false,
hierarchybuilder.class.getclassloader());
return buildhierarchy(clzz);
}
public static hierarchy buildhierarchy(class clzz){
if(clzz == null){
throw new runtimeexception("class parameter can not be null");
}
final hierarchy hier = new hierarchy();
hier.setbaseclass(clzz);
final class superclass = clzz.getsuperclass();
if(superclass !=
null && superclass.getname().equals("java.lang.object")){
return hier;
}else{
while((clzz.getsuperclass() != null) &&
(!clzz.getsuperclass().getname().equals("java.lang.object"))){
clzz = clzz.getsuperclass();
hier.addclass(clzz);
}
return hier;
}
}
}
现在是测试时间!
有关测试覆盖的文章怎么能缺少测试案例呢?在清单 3 中,我定义了一个简单的有三个测试案例的 junit 测试类,它将试图执行 hierarchy 类和 hierarchybuilder 类:
清单 3. 测试 hierarchybuilder!
package test.com.vanward.adana.hierarchy;
import com.vanward.adana.hierarchy.hierarchy;
import com.vanward.adana.hierarchy.hierarchybuilder;
import junit.framework.testcase;
public class hierarchybuildertest extends testcase {
public void testbuildhierarchyvaluenotnull() {
hierarchy hier = hierarchybuilder.buildhierarchy(hierarchybuildertest.class);
assertnotnull("object was null", hier);
}
public void testbuildhierarchyname() {
hierarchy hier = hierarchybuilder.buildhierarchy(hierarchybuildertest.class);
assertequals("should be junit.framework.assert",
"junit.framework.assert",
hier.gethierarchyclassnames()[1]);
}
public void testbuildhierarchynameagain() {
hierarchy hier = hierarchybuilder.buildhierarchy(hierarchybuildertest.class);
assertequals("should be junit.framework.testcase",
"junit.framework.testcase",
hier.gethierarchyclassnames()[0]);
}
}
因为我是一个狂热的测试人员,我自然希望运行一些覆盖测试。对于 java 开发人员可用的代码覆盖工具中,我比较喜欢用 cobertura,因为它的报告很友好。而且,corbertura 是开放源码项目,它派生出了 jcoverage 项目的前身。




