第7章 美丽测试

Normal 0 7.8 磅 0 2 false false false MicrosoftInternetExplorer4
7章 美丽的测试

Alberto Savoia

许多程序员都有过这样的经历:看一段代码,觉得它不仅实现了功能,而且实现得很漂亮。通常,如果一段代码能优雅、简洁地完成了需要完成的功能,我们就认为这样的代码很漂亮。

那对于漂亮代码的测试,尤其是那种开发者在编写代码的同时编写的(或者应该编写的)测试,情况又是怎样的呢?在这一章,我将专注于讨论测试,因为测试本身也可以是漂亮的。更重要的是,它们能起到非常关键的作用,可以帮你写出更漂亮的代码。

正如我们将要看到的,有些东西,如果把它们组合起来会使测试很漂亮。跟代码不同的是,我无法让自己认为某个单一的测试很漂亮,至少跟我看待一个排序函数,并认为它漂亮的情况不一样。原因是测试天生就带有组合性和试探性。代码中的每一条if语句至少需要两个测试(一个用于条件表达式为真的情况,另一个用于为假的情况)。一条拥有多个条件的if语句,比如:

    if ( a || b || c )

理论上需要8个测试——每一个对应abc不同值的一个可能的组合。如果再考虑循环中的异常,多个输入参数,对外部代码的依赖,不同的软硬件平台等,所需测试的数量和类型将大大增加。

除了最简单的情况,任何代码,不管漂亮与否,都需要一组(而不是一个)测试,这些测试中的每一个都应该专注于检查代码的一个特定的方面,就像球队一样,不同的队员有不同的职责,负责球场的不同区域。

我们已经知道应该以“组”为单位来对测试进行整体评估,现在我们需要进一步了解都有哪些特性能决定一组测试是否漂亮——“漂亮”,一个很少用来修饰“测试”的形容词。

一般来讲,测试的主要目的是逐步建立,不断加强并再次确认我们对于代码的信心:即代码正确并高效地实现了功能。因此对我来讲,最漂亮的测试是那些能将我们的信心最大化的测试,这个信心就是代码的确实现了它被要求的功能,并将一直保持这一点。由于代码不同方面的属性需要不同类型的测试来验证,所以对于“漂亮”的评判准则也不是固定的。本章考查了能使测试漂亮的三种方法。

测试因简单而漂亮

简单的几行测试代码,使我能描述并验证目标代码的基本行为。通过在每次构建时自动运行那些测试,能确保代码在不断开发的过程中始终保持所要求的行为。本章将使用JUnit测试框架来给出一些比较基本的测试例子,这些只需几分钟就能编写的测试,将在项目的整个生命周期中使我们不断受益。

测试因揭示出使代码更优雅,更可维护和更易测试的方法而漂亮

换句话讲,测试能帮我们把代码变得更漂亮。编写测试的过程不仅能帮我们找出实现中的逻辑错误,还能帮我们发现结构和设计上的问题。在这一章,通过尝试编写测试,我将演示我是怎样找到了一种能使我的代码更健壮、更有可读性、结构也更好的方法的。

测试因其深度和广度而漂亮

深入彻底、覆盖无遗的测试会大大增强开发者的信心,这种信心就是代码不仅在一些基本的、手工挑选的情形下,而且在所有的情形下都实现了所需的功能。在这一章,我将演示怎样根据测试理论中的概念来编写和运行这类测试。

由于大多数程序开发者都已经熟悉了诸如冒烟测试(smoke testing)和边界测试(boundary testing)等基本的测试技术,我将花更多的时间来讨论更有效类型的测试和那些很少被讨论和应用的测试技术。

 

 

7.1   讨厌的二分查找

为了演示多种不同的测试技术,同时又保持本章的篇幅合理,需要一个简单、易描述,并能通过几行代码就能实现的例子。同时,这个例子还必须足够生动,拥有一些有趣的挑战测试的特性。最理想的情况是这个例子要有一个悠久的总是被实现出许多bug的历史,从而显出对彻底测试的迫切需要。最后但并非最不重要的一点:如果这个例子本身也被认为是漂亮的代码那就再好不过了。

每当讨论漂亮的代码,就很容易让人联想起Jon Bentley那本经典的由Addison-Wesley出版的《Programming Pearls (中文名《编程珠玑》,译者注)。我就是在读那本书的时候,发现了我要找的代码例子:二分查找。

让我们快速复习一下,二分查找是一个简单而又高效的算法(但我们即将看到,要正确实现它也是有点难度的),这个算法用来确定一个预先排好顺序的数组x[0..n-1]中是否含有某个目标元素t。如果数组包含t,程序返回它在数组中的位置,否则返回-1

Jon Bentley是这样向学生们描述该算法的:

在一个包含t的数组内,二分查找通过对范围的跟综来解决问题。开始时,范围就是整个数组。通过将范围中间的元素与t比较并丢弃一半范围,范围就被缩小。这个过程一直持续,直到在t被发现,或者那个能够包含t的范围已成为空。

他又说到:

大多数程序员认为,有了上面的描述,写出代码是很简单的事情。他们错了。能使你相信这一点的惟一方法是现在就合上书,去亲手写写代码试试看。

Second Bentley的建议。如果你从来没有写过二分查找,或者有好几年没写过了,我建议你在继续读下去之前亲手写一下;它会使你对后面的内容有更深的体会。

二分查找是一个非常好的例子,因为它是如此简单,却又如此容易被写错。在《Programming Pearls》一书中,Jon Bentley记述了他是怎样在多年的时间里先后让上百个专业程序员实现二分查找的,而且每次都是在他给出一个算法的基本描述之后。他很慷慨,每次给他们两个小时的时间来实现它,而且允许他们使用他们自己选择的高级语言(包括伪代码)。令人惊讶的是,大约只有10%的专业程序员正确地实现了二分查找。

更让人惊讶的是,Donald Knuth在他的《Sorting and Searching[1]一书中指出,尽管第一个二分查找算法早在1946年就被发表,但第一个没有bug的二分查找算法却是在12年后才被发表出来。

[]见《计算机程序设计艺术,第3卷:排序和查找(第二版)》,Addison-Wesley1998。(国内由清华大学出版社出版影印版-译者注)。

然而,最让人惊讶的是,Jon Bentley正式发表的并被证明过的算法,也就是被实现或改编过成千上万次的那个,最终还是有问题的,问题发生在数组足够大,而且实现算法的语言采用固定精度算术运算的时候。

Java语言中,这个bug导致一个ArrayIndexOutOfBoundsException异常被抛出,而在C语言中,你会得到一个无法预测的越界的数组下标。你可以在Joshua Blochblog上找到更多关于这个bug的信息:

http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html

以下就是带有这个著名的bugJava实现:

    public static int buggyBinarySearch(int[] a, int target) {

        int low = 0;

        int high = a.length - 1;

 

        while (low <= high) {

            int mid = (low + high) / 2;

            int midVal = a[mid];

 

            if (midVal < target)

                low = mid + 1;

            else if (midVal > target)

                high = mid - 1;

            else

                return mid;

        }

        return -1;

    }

Bug位于这一行:

    int mid = (low + high) / 2;

如果lowhigh的和大于Integer.MAX_VALUE(在Java中是231 -1),计算就会发生溢出,使它成为一个负数,然后被2除时结果当然仍是负数。

推荐的解决方案是修改计算中间值的方法来防止整数溢出。方法之一是用减法——而不是加法——来实现:

    int mid = low + ((high - low) / 2);

或者,如果你想炫耀一下自己掌握的位移运算的知识,那个blog(还有Sun微系统公司的官方bug report[1])建议使用无符号位移运算,这种方法或许更快,但对大多数Java程序员(包括我)来说,可能也比较晦涩。

[1]   http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5045582

        int mid = (low + high) >>> 1;

想一下,二分查找算法的思想是多么的简单,而这么多年又有多少人的多少智力花在它上面,这就充分说明了即使是最简单的代码也需要测试,而且需要很多。Joshua Bloch在它的blog中对这个bug作了非常漂亮的陈述:

这个bug给我上的最重要的一课就是要懂得谦逊:哪怕是最简单的一段代码,要写正确也并非易事,更别提我们现实世界中的系统:它们跑在大段大段的复杂代码上。

下面是我要测试的二分查找的实现。理论上讲,对于中间值的计算方法的修正,应该是解决了这段令人讨厌的代码的最后一个bug,一个在好几十年的时间里,连一些最好的程序员都抓不到的bug

    public static int binarySearch(int[] a, int target) {

        int low = 0;

        int high = a.length - 1;

 

        while (low <= high) {

            int mid = (low + high) >>> 1;

            int midVal = a[mid];

 

            if (midVal < target)

                low = mid + 1;

            else if (midVal > target)

                high = mid - 1;

            else

                return mid;

        }

        return -1;

    }

这个版本的binarySearch看上去是正确的,但它仍可能有问题。或许不是bug,但至少是可以而且应该被修改的地方。这些修改可以使代码不仅更加健壮,而且可读性,可维护性和可测试性都比原来更好。让我们看看是否可以通过测试来发现一些有趣的和意想不到的改善它的机会。


7.2 JUnit简介

谈到漂亮测试,就很容易想到JUnit测试框架。因为我使用Java,通过使用JUnit来构建我的漂亮的测试是一个很自然的决定。但在做之前,考虑到你对JUnit可能尚未熟悉,让我先对它做一个简单介绍吧。

JUnitKent BeckErich Gamma设计的,他们创造JUnit来帮助Java开发者编写和运行自动的和自检验的测试。它有一个很简单,却又很宏伟的目标:就是使得程序开发者更容易去做他们本来就应该做的事情:测试自己的代码。

遗憾的是,我们还要走很长的路才能到达那种大多数程序员都像是被“测试病毒”所感染的阶段(在那种情况下,程序员们试着自己编写测试,并决定把它看作开发中的一个常规的重要组成部分)。然而,自从被引入开发领域,任何其他的东西都没能像JUnit那样使这么多的程序员开始编写测试。不过这也得感谢极限编程和其他敏捷开发方法的巨大帮助,在这些方法中,开发者参加程序测试是必须的[1]Martin FowlerJUnit的影响作了这样的概括:“少量代码对大量的代码起了如此重要的作用,这在软件开发领域中是前所未有的事。”

[1]   能够彰显JUnit的成功及影响力的另一个事实是,如今针对大多数现代编程语言的测试框架都出现了,它们都是从JUnit那里得到的灵感,JUnit的各类扩展也出现了。

JUnit被特地设计得很简单,易学易用。这是JUnit的一个重要的设计准则。Kent BeckErich Gamma花费了大量心思来确保JUnit的易学易用,于是程序员们才会真正使用它。它们自己是这样说的:

我们的第一目标就是要写出一个框架,使我们可以对程序员们真正在其中编写测试抱有希望。这个框架必须使用人们熟悉的工具,这样大家就不用学习很多新东西;必须保证编写一个新的测试所需的工作量降至最低;还必须能够消除重复劳动。[1]

[1]JUnit: A Cook’s Tour》,Kent BeckErich Gammahttp://junit.sourceforge.net/doc/cookstour/cookstour.htm.

JUnit的官方入门文档(the JUnit Cookbook)的长度还不到两页纸:http://junit.sourceforge.net/doc/cookbook/cookbook.htm.

 

以下是从cookbook(来自JUnit4.x版本)中抽取出来的最重要的一段

当你需要测试一样东西时,你只要做:

1.  为一个方法加上@org.junit.Test标注(annotate);

2.  当你需要检查一个值,把org.junit.Assert [1]输入进来,调用assertTrue(),并传递一个Boolean对象,当测试成功时它为true

比如,为了测试同一币种的两个Money对象相加时,结果对象中的值恰好等于那两个Money对象中的值直接相加的结果,你可以这样做:

    @Test

    public void simpleAdd() {

        Money m12CHF= new Money(12, "CHF");

        Money m14CHF= new Money(14, "CHF");

        Money expected= new Money(26, "CHF");

        Money result= m12CHF.add(m14CHF);

        assertTrue(expected.equals(result));

    }

[1]   能够彰显JUnit的成功及影响力的另一个事实是,如今针对大多数现代编程语言的测试框架都出现了,它们都是从JUnit那里得到的灵感,JUnit的各类扩展也出现了。

(为啥上面这段这里又重复了一遍?)

只要你稍微熟悉一点Java语言,那两条操作指导和这个简单的例子就足以使你上手。它们也足以使你理解我将要写的测试。简单得让人觉得漂亮,是不是?好,我们继续。

7.3  将二分查找进行到底

知道了它的历史,我不想被二分查找表面的简单或看似明显的修改所欺骗,尤其是我从来没有在其他代码中用过无符号移位操作符(即>>>)。我将测试这个二分查找的修正版本,就如同我以前从来没有听说过它,也没有实现过它。我不想相信任何人的说辞,测试和证明,说它这一次确实正确。我要通过自己的测试来确信它按照它所应该的方式工作,让他成为一件确凿无疑的事情。

这是我最初的测试策略(或者说测试组)。

· <!--[endif]

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/16502878/viewspace-481923/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/16502878/viewspace-481923/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值