下面便以一次亲身经历来说说设计测试用例的几大要点吧。
  首先说说基本背景。TopCoder是一个对全世界程序开发爱好者开放的平台,在上面可以讨论、交流、竞赛。这里真的是汇聚了世界上的算法高手。TopCoder周赛也是SRM(Single Round Match),平均每十天左右一次,赛前4个小时内报名的注册人员皆可参加,而每个成功报名的人将被随机分配在一个20人左右的Room里。一般情况是75分钟内面对3道难度和分数分别递增的算法题目,使用C++、Java等编程语言解决,比赛过程中提交后会有分数,这个分数是根据从打开题目到提交答案的时间确定的,是一个很复杂的算法,不过通常时间越短分数越高;不过真正的得分要在比赛结束后系统用大量测试用例评判,如果不通过那么该题终得分仍然为0。SRM有意思的一点是75分钟的答题时间结束后,经过短暂的休息,有一个15分钟的Challenge环节;这期间每位选手被允许查看同一个Room内的其他选手的代码,如果觉得有问题,那么可以设计测试用例Challenge该代码。如果结果是代码的返回值与预期确实不同,那么Challenge成功,Challenger获得额外奖励的50分,Defender该题目分数当场被扣掉;不过如果代码的返回值与预期相同,那么Challenge失败,Challenger被扣掉25分,Defender分数不变。Challenge环节应该说是十分刺激的,据说在TopCoder的某次高水平比赛中,一位选手3道题目都没有答出来,但却通过12次成功Challenge他人的代码获得600分而获得了所在Room的第一名,这是多么神奇的事情!
  言归正传,Challenge别人代码的本质是设计有效的测试用例找出软件bug,以下我以某一次有典型意义的SRM的Challenge环节来讲述如何设计出针对软件bug的测试用例。
  题目描述:现有两个字符串originalWord和finalWord,仅由字母'a'或'b'组成,我们把将originalWord中的某个字符由'a'变作'b'或者由'b'变作'a'称为一次move;另有整型数k,问能否通过恰好k次move将originalWord变为finalWord?如果能够实现,返回字符串"POSSIBLE",否则返回"IMPOSSIBLE"。
  用例:
  1)originalWord="aababba",finalWord="bbbbbbb",k=2,由于originalWord中有4个字符与finalWord不同,因此2次move不可能使originalWord变为finalWord,故返回"IMPOSSIBLE";
  2)originalWord="aabb",finalWord="aabb",k=1,由于originalWord与finalWord已经相同,因此1次move反而会使originalWord与finalWord不同,故返回"IMPOSSIBLE";
  3)originalWord="aaa",finalWord="bab",k=4,应返回"POSSIBLE";move的步骤可以是:aaa->baa->bab->aab->bab;
  分析:很明显,题目很水,其实只要统计originalWord与finalWord中不同字符的个数diff,然后将diff与k作比较即可。如果diff大于k,那么一定返回"IMPOSSIBLE",因为需要move的步数不够;如果diff等于k,显然应返回"POSSIBLE";如果diff小于k,那么应考察(k-diff)的奇偶性,如果(k-diff)为偶数,应该返回"POSSIBLE",因为此时多余的move可以通过将某个字符连续move(即由'a'变作'b'后再由'b'变回'a')消化掉,但如果(k-diff)为奇数,则要返回"IMPOSSIBLE",因为后会有一次move变不回原来的字符了。
  通过分析,我们知道只要一次循环和一次判断可搞定该题目,下面给出我写的例程:

  不过,自己写代码是一回事,看别人代码又是另外一回事,下面看一份被成功Challenge的代码:

  这份代码的问题其实一目了然,事实上它与我提供的例程很接近,但作者犯了一个十分愚蠢的错误,那是把结果搞反了!这虽然可能是笔误、不小心等原因造成了,但在实际应用中会归结为对需求的分析解读错误。而对这份代码设计Challenge用例是轻而易举的,因为它连示例中给出的用例都过不了,之前的三组测试用例任何一组都可以成功将之Challenge。对这份代码的Challenge过程告诉我们,认真细致地做需求分析十分重要,而对于测试人员亦是如此,因为正确地理解需求可以有针对性地设计功能测试用例找出待测软件的bug。
  下面我们再来看另一份有问题的代码:

  如果读者对上面我的分析过程能够正确地理解,相信对于这份代码的bug也是不难找到的:问题出在条件判断上面。
  这个条件判断貌似少点什么!没错,分支只判断c是否等于k,而根本没有考虑(k-c)的奇偶问题;换句话说,代码少考虑情况了!那么Challenge变得很容易了,只要把代码中缺少的部分提炼出来即可,例如示例中的第三组测试数据originalWord="aaa",finalWord="bab",k=4,按照这份代码的结果会是"IMPOSSIBLE",而实际上(k-c)是偶数,应该返回"POSSIBLE",这样我们的Challenge又成功了!不过这次与上一次的Challenge是有所不同的,因为这份代码并非所有测试用例都通过不了,例如示例中的第二个测试用例,当originalWord与finalWord相同的时候,代码是可以通过的。因此我们称这种仅能通过部分测试用例的情况为对需求的片面分析或需求项分析不足。
  我们的Challenge过程很好地阐述了如何针对这种情况设计测试用例:将需求项分析不足的地方提取出来,专门设计这方面的用例。当然,真实应用中,我们会设计大量测试用例,但需求项的覆盖问题依然是重中之重,因为如果测试人员在解读需求的时候不能将其完全覆盖,设计的测试用例必然有所遗漏,可能会造成测试失败。所以,测试用例对需求的完全覆盖,即恰当地进行逻辑测试是测试人员必须加以重视的内容。
  正确的代码总是相同的,而错误的代码则各有各的问题,我们再来看另一份代码:
  判断部分似乎真的没什么问题,只是形式和我的例程有所差异,那这份代码是怎么被成功Challenge的呢?细心的读者可能一眼能发现问题所在,前面的循环有问题!结束条件居然是i<length-1,这意味着后一个字符不在循环处理范围内!
  显然这段代码不能完全实现题目中的需求,我们只需设计一个测试用例使得后一个字符左右了程序的输出结果即可。例如,originalWord="aaa",finalWord="aab",k=0,如果没有后一个字符,显然应该返回"POSSIBLE",然而后一个字符的存在使得结果变成了"IMPOSSIBLE",而这一切对于上面的代码而言好像在比较originalWord=finalWord="aa"一样,所以该测试用例成功Challenge了该代码!
  对于这份代码的问题,我们通常叫做边界问题处理不当。边界可能的含义有很多,例如输入变量的范围,例如系统的吞吐量极限等等,在这个问题中,边界的意义是循环的初始和结束条件。很显然,被我们成功Challenge的代码初始条件做的没有问题,但结束条件却是错的,因为后一个字符没有被处理!而我们设计测试用例的时候便顺水推舟,将后一个字符起决定作用的测试用例设计出来,软件的bug暴露了!所以任何测试一定不能放过边界问题的处理,对边界条件设置测试用例势在必行。
  看来Challenge不是一件多难的事,那么来看下面一位选手的代码吧:

  相信一看到这份代码,大家会十分吃惊,居然代码只有两行,这能实现要求的功能吗?话说回来,我当初看到这份代码的时候也觉得看来这两行儿戏般的代码凶多吉少了,于是将之作为重点Challenge对象。不过细细读来,你和我都会发现,没那么简单,这是因为这份代码对需求的实现没有问题,囊括了所有需求项,边界处理也很得当,任何用例都不可能Challenge成功,这份代码是正确的。因此,只要我们的测试遵循原则,尊重需求,没有遗漏,考虑边界,不仅能够找到软件的bug,也可以证明软件实现需求的正确性。
  说了这么多,相信读者对设计有效的测试用例应该有了一定的了解,不过真实系统中遇到的问题远比算法设计题目要复杂得多,设计的测试用例也要涵盖更多内容,例如除了功能测试、逻辑测试和边界测试外,还可能有余量测试、接口测试、强度测试、容错性测试等。不过,万变不离其宗,任何测试都是基于对需求的准确、完整的分析;而测试人员在测试过程中也应该坚持心思缜密的作风,这样软件bug将无处遁形,被我们一网打尽!