|
3#
楼主 |
发表于 2007-10-9 17:08:22
|
只看该作者
测试驱动开发
[用TestCase做试验]
如果你在开始某个特性或产品的开发之前对某个领域不太熟悉或一无所知,或者对自己在该领域里的能力一无所知,那么你一定会选择做试验,在有单元测试作工具的情况下,建议你用TestCase做试验,这看起来就像你在写一个验证功能是否实现的TestCase一样,而事实上也一样,只不过你所验证的不是代码本身,而是这些代码所依赖的环境。
[TestCase之间应该尽量独立]
保证单独运行一个TestCase是有意义的。
[不仅测试必须要通过的代码,还要测试必须不能通过的代码]
这是一个小技巧,也是不同于设计思路的东西。像越界的值或者乱码,或者类型不符的变量,这些输入都可能会导致某个异常的抛出,或者导致一个标示“illegal parameters”的返回值,这两种情况你都应该测试。当然我们无法枚举所有错误的输入或外部环境,这就像我们无法枚举所有正确的输入和外部环境一样,只要TestCase能说明问题就可以了。
[编写代码的第一步,是在TestCase中用Ctrl-C]
这是一个高级技巧,呃,是的,我是这个意思,我不是说这个技巧难以掌握,而是说这个技巧当且仅当你已经是一个TDD高手时,你才能体会到它的魅力。多次使用TDD的人都有这样的体会,既然我的TestCase已经写的很好了,很能说明问题,为什么我的代码不能从TestCase拷贝一些东西来呢。当然,这要求你的TestCase已经具有很好的表达能力,比如断言f(5)=125的方式显然没有断言f(5)=5^(5-2)表达更多的内容。
[测试用例包应该尽量设计成可以自动运行的]
如果产品是需要交付源代码的,那我们应该允许用户对代码进行修改或扩充后在自己的环境下run整个测试用例包。既然通常情况下的产品是可以自动运行的,那为什么同样作为交付用户的制品,测试用例包就不是自动运行的呢?即使产品不需要交付源代码,测试用例包也应该设计成可以自动运行的,这为测试部门或下一版本的开发人员提供了极大的便利。
[只亮一盏红灯]
大师的建议,前面已经提到了,仅仅是建议。
[用TestCase描述你发现的bug]
如果你在另一个部门的同事使用了你的代码,并且,他发现了一个bug,你猜他会怎么做?他会立即走到你的工位边上,大声斥责说:“你有bug!”吗?如果他胆敢这样对你,对不起,你一定要冷静下来,不要当面回骂他,相反你可以微微一笑,然后心平气和的对他说:“哦,是吗?那么好吧,给我一个TestCase证明一下。”现在局势已经倒向你这一边了,如果他还没有准备好回答你这致命的一击,我猜他会感到非常羞愧,并在内心责怪自己太莽撞。事实上,如果他的TestCase没有过多的要求你的代码(而是按你们事前的契约),并且亮了红灯,那么就可以确定是你的bug,反之,对方则无理了。用TestCase描述bug的另一个好处是,不会因为以后的修改而再次暴露这个bug,它已经成为你发布每一个版本之前所必须检查的内容了。
{关于单元测试}
单元测试的目标是
Keep the bar green to keep the code clean
这句话的含义是,事实上我们只做两件事情:让代码奏效(Keep the bar green)和让代码洁净(Keep the code clean),前者是把事情做对,后者是把事情做好,两者既是TDD中的两顶帽子,又是xUnit架构中的因果关系。
单元测试作为软件测试的一个类别,并非是xUnit架构创造的,而是很早就有了。但是xUnit架构使得单元测试变得直接、简单、高效和规范,这也是单元测试最近几年飞速发展成为衡量一个开发工具和环境的主要指标之一的原因。正如Martin Fowler所说:“软件工程有史以来从没有如此众多的人大大收益于如此简单的代码!”而且多数语言和平台的xUnit架构都是大同小异,有的仅是语言不同,其中最有代表性的是JUnit和NUnit,后者是前者的创新和扩展。一个单元测试框架xUnit应该:1)使每个TestCase独立运行;2)使每个TestCase可以独立检测和报告错误;3)易于在每次运行之前选择TestCase。下面是我枚举出的xUnit框架的概念,这些概念构成了当前业界单元测试理论和工具的核心:
[测试方法/TestMethod]
测试的最小单位,直接表示为代码。
[测试用例/TestCase]
由多个测试方法组成,是一个完整的对象,是很多TestRunner执行的最小单位。
[测试容器/TestSuite]
由多个测试用例构成,意在把相同含义的测试用例手动安排在一起,TestSuite可以呈树状结构因而便于管理。在实现时,TestSuite形式上往往也是一个TestCase或TestFixture。
[断言/Assertion]
断言一般有三类,分别是比较断言(如assertEquals),条件断言(如isTrue),和断言工具(如fail)。
[测试设备/TestFixture]
为每个测试用例安排一个SetUp方法和一个TearDown方法,前者用于在执行该测试用例或该用例中的每个测试方法前调用以初始化某些内容,后者在执行该测试用例或该用例中的每个方法之后调用,通常用来消除测试对系统所做的修改。
[期望异常/Expected Exception]
期望该测试方法抛出某种指定的异常,作为一个“断言”内容,同时也防止因为合情合理的异常而意外的终止了测试过程。
[种类/Category]
为测试用例分类,实际使用时一般有TestSuite就不再使用Category,有Category就不再使用TestSuite。
[忽略/Ignored]
设定该测试用例或测试方法被忽略,也就是不执行的意思。有些被抛弃的TestCase不愿删除,可以定为Ignored。
[测试执行器/TestRunner]
执行测试的工具,表示以何种方式执行测试,别误会,这可不是在代码中规定的,完全是与测试内容无关的行为。比如文本方式,AWT方式,swing方式,或者Eclipse的一个视图等等。
{实例:Fibonacci数列}
下面的Sample展示TDDer是如何编写一个旨在产生Fibonacci数列的方法。
(1)首先写一个TC,断言fib(1) = 1;fib(2) = 1;这表示该数列的第一个元素和第二个元素都是1。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
}
(2)上面这段代码不能编译通过,Great!——是的,我是说Great!当然,如果你正在用的是Eclipse那你不需要编译,Eclipse会告诉你不存在fib方法,单击mark会问你要不要新建一个fib方法,Oh,当然!为了让上面那个TC能通过,我们这样写:
public int fib( int n ) {
return 1;
}
(3)现在那个TC亮了绿灯,wow!应该庆祝一下了。接下来要增加TC的难度了,测第三个元素。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(2, fib(3));
}
不过这样写还不太好看,不如这样写:
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
}
(4)新增加的断言导致了红灯,为了扭转这一局势我们这样修改fib方法,其中部分代码是从上面的代码中Ctrl-C/Ctrl-V来的:
public int fib( int n ) {
if ( n == 3 ) return fib(1)+fib(2);
return 1;
}
(5)天哪,这真是个贱人写的代码!是啊,不是吗?因为TC就是产品的蓝本,产品只要恰好满足TC就ok。所以事情发展到这个地步不是fib方法的错,而是TC的错,于是TC还要进一步要求:
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
assertEquals(fib(2)+fib(3), fib(4));
}
(6)上有政策下有对策。
public int fib( int n ) {
if ( n == 3 ) return fib(1)+fib(2);
if ( n == 4 ) return fib(2)+fib(3);
return 1;
}
(7)好了,不玩了。现在已经不是贱不贱的问题了,现在的问题是代码出现了冗余,所以我们要做的是——重构:
public int fib( int n ) {
if ( n == 1 || n == 2 ) return 1;
else return fib( n - 1 ) + fib( n - 2 );
}
(8)好,现在你已经fib方法已经写完了吗?错了,一个危险的错误,你忘了错误的输入了。我们令0表示Fibonacci中没有这一项。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
assertEquals(fib(2)+fib(3), fib(4));
assertEquals(0, fib(0));
assertEquals(0, fib(-1));
}
then change the method fib to make the bar grean:
public int fib( int n ) {
if ( n <= 0 ) return 0;
if ( n == 1 || n == 2 ) return 1;
else return fib( n - 1 ) + fib( n - 2 );
}
(9)下班前最后一件事情,把TC也重构一下:
public void testFab() {
int cases[][] = {
{0, 0}, {-1, 0}, //the wrong parameters
{1, 1}, {2, 1}}; //the first 2 elements
for (int i = 0; i < cases.length; i++)
assertEquals( cases[1], fib(cases[0]) );
/*the rest elements*/
for (int i = 3; i < 20; i++)
assertEquals(fib(i-1)+fib(i-2), fib(i));
}
(10)打完收工。
[ 本帖最后由 mowuhen 于 2007-10-9 17:09 编辑 ] |
|