“想通过更多的测试来改善软件的质量,就跟妄想通过更频繁的称重来减肥一样”
《代码大全(第二版)》
当第一次听到这句话的时候,很震惊,也很难理解:我们不是通过测试找到软件的很多bug,不是通过测试让开发对代码进行了修改吗?为什么作者会有如此说法?
也许上面那句话说得有些绝对,但是,如果我们只把测试定义为软件质量度量的一种方式,把测试工作的目的定义为寻找缺陷,那么上面这句话就是正确的了。传统测试过多侧重于大规模的集成测试,处于的项目后期,太过被动。做为项目质量最后的守门员,它的责任太大,它也担当不起这个角色。因为,永远没有完备的测试!就算QA一天24小时都在写用例、执行用例,也保证不了产品上线之后没有问题!就算我们发现了很多缺陷,开发在做修改的时候,由于缺少更细粒度、更精确的对原有代码功能的保证,可能会引入更多的缺陷!辛辛苦苦发现的缺陷能否对软件最终质量提高起到积极作用,就要打个问号了。这让我想起来了测试领域一句名言------“测试永远只能证明代码有问题,而不能证明代码没有问题”。
那测试对于代码质量的提高真的没有价值了吗?绝对不是!把我们的思想转变一下,把重点转换一下。测试的灵魂不在于去度量软件的质量,而在于按照下面的方式去保证和提高软件的质量:
1. 通过测试,从设计及代码层面去提高软件质量;
2. 通过测试,去加快每一次项目的发布周期;
3. 通过测试,去保证每次改动、每个新功能添加的正确性。
要如何达到这些作用呢?
1. 将测试(特别是能够快速反馈、准确定位的小测试)作为代码所要实现的功能的夹钳(也就是必须保证通过的条件),保证有变更时的代码的正确性;保证代码对外功能不稳定性,也能放心大胆地对代码进行重构、优化。
2. 将可测试性作为测试对代码质量进行提升的途径。将测试作为添加功能或者修改代码时必须保证通过的条件,就能
3. 将测试作为体现业务需求、代码功能的说明文档。代码本身的功能及使用方式,通过测试用例本身就能得到准确而形象的说明。
TDD的核心思想也就是这样子的。《修改代码的艺术》中也反复提到如何通过测试来保证修改,如何通过测试、为了测试去提高新老代码的质量。
“不能被很好测试的代码,一定是设计糟糕、实现糟糕的代码!”,为了可以测试、方便测试而进行的代码可测试性方面的改善,对代码的封装性、可扩展性等方面会带来直接的提升。只有这样的测试,才会对代码质量的提高起到至关重要的作用。
测试人员应该与开发一起,提高代码的可测试性、提高设计的可测试性!在交付高质量软件产品的共同目标下,测试与开发的分工和界限会越来越模糊。
为什么说可测试性与代码质量密切相关,测试对代码质量能产生积极影响呢?可以看看下面两个例子:
例子1:
糟糕的代码:
class Car {
public Car() {
engine = new Engine();
windshield = new Windshield();
}
}
class Engine {
public
Engine() {
sparkplugs.add(new
Sparkplug());
sparkplugs.add(new
Sparkplug());
}
}
当要进行测试时,不管我们是否需要Car类的所包含的所有零件,我们都要花时间去实例化它们,这个过程可能代价会很大。如果某个零件的类还没有实现,我们就不能开展与其无关的关于CAR类的其他测试。
从开发角度来说,这个实现很脆弱:当我们想使用一个新的高性能的sparkplug时,当我们想给car换一种engine时,我们必须对原来的代码进行修改。
好的代码:
class Engine {
public
Engine(Set<Sparkplug> sparkplugs) {
this.sparkplugs = sparkplugs;
}
}
class Car {
public
Car(Engine engine, Windshield windshield) {
this.engine = engine;
this.windshield = windshield;
}
}
可测试性上:非常方便地去进行mock或者fack。如可以做一个fackEngine,让它从来不会启动。这样测试用例就可以方便地模拟出各种情况。
灵活性方面:现在不管是想要一个HighPerformanceSparkplugs的Enging,还是要一个FuelEfficientEngine,都变得很容易。
例子2
糟糕的代码:
class User {
private Preferences
prefs;
public User(File
preferenceFile) {
prefs = parseFile(preferenceFile);
}
public void
doSomething() {
… // using prefs
}
private Preferences
parseFile {. . . }
}
测试方面的问题:1.涉及到了文件系统,测试会变得很慢2.很难去构造测试执行的条件:需要去构造一个格式符合规范的文件3如果文件格式发生变化,必须更新所有相关的测试用例
灵活性方面的问题:不能够支持今后的扩展,当需要改变文件格式时,侵入性地修改User类,当需要支持更多perference版本时,User中要有更多的逻辑判断,等等问题
好的代码:
class User {
private Preferences
prefs;
public User(Preferences
prefs) {
this.prefs = prefs;
}
public void
doSomething() {
… // using prefs
}
}
灵活性方面:1.数据可以很方便地来源于更多的地方,2.对于每个新的数据来源,可以单独生成一个将数据转换为Perference的工具类。这个工具类本身是很好测试的。
测试方面的好处:1,运行更快2.preference对于初始化测试变得代价很小3.测试不再与文件的格式相依赖。
(例子来源:《Flexible Design? Testable Design?You Don't Have to Choose!》Russ Rufer & Tracy Bialik
Google, Inc. GTAC 2010。这份ppt中还有更多的例子来说明代码的可测试性就是代码质量一个非常重要的方面)
由上面这个简单的例子可以看出,可测试性好的代码,能够方便地写出运行快、耦合小、依赖少的测试用例,而代码本身的结构也更加合理,在可扩展性、可维护性上面大大提高。
当我们把测试与开发活动融合在一起,通过测试提高软件的可测试性,那测试就变成软件质量提高的重要因素了!不再是简单的称重。
注:关于测试如何作为修改代码时的保证,如何做为代码功能的描述,大家可以参考TDD方面的资料及《修改代码的艺术》。