改版后的testCar已经面目全非。先让我们了解这些改动都是什么含义,再看如何执行这个测试。
1>;import语句,引入JUnit的类。(没问题吧)
2>;继承 TestCase 。可以暂时将一个TestCase看作是对某个类进行测试的方法的集合。详细介绍请参看JUnit资料
3>;setUp()设定了进行初始化的任务。我们以后会看到setUp会有特别的用处。
4>;testGetWheeels()对预期的值和myCar.getWheels()返回的值进行比较,并打印比较的结果。assertEquals是junit.framework.Assert中所定义的方法,junit.framework.TestCase继承了junit.framework.Assert。
5>;suite()是一个很特殊的静态方法。JUnit的TestRunner会调用suite方法来确定有多少个测试可以执行。上面的例子显示了两种方法:静态的方法是构造一个内部类,并利用构造函数给该测试命名(test name, 如 Car.getWheels ),其覆盖的runTest()方法,指明了该测试需要执行那些方法--testGetWheels()。动态的方法是利用内省(reflection )来实现runTest(),找出需要执行那些测试。此时测试的名字即是测试方法(test method,如testGetWheels)的名字。JUnit会自动找出并调用该类的测试方法。
6>;将TestSuite看作是包裹测试的一个容器。如果将测试比作叶子节点的话,TestSuite是分支节点。实际上TestCase,TestSuite以及TestSuite组成了一个composite Pattern。 JUnit的文档中有一篇专门讲解如何使用Pattern构造Junit框架。有兴趣的朋友可以查看JUnit资料。
如何运行该测试呢?手工的方法是键入如下命令:
[Windows] d:>;java junit.textui.TestRunner testCar
[Unix] % java junit.textui.TestRunner testCar
别担心你要敲的字符量,以后在IDE中,只要点几下鼠标成了。运行结果应该如下所示,表明执行了一个测试,并通过了测试:
.
Time: 0
OK (1 tests)
如果我们将Car.getWheels()中返回的的值修改为3,模拟出错的情形,则会得到如下结果:
.F
Time: 0
There was 1 failure:
1) testGetWheels(testCar)junit.framework.AssertionFailedError: expected:<4>; but was:<3>;
at testCar.testGetWheels(testCar.java:37)
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
注意:Time上的小点表示测试个数,如果测试通过则显示OK。否则在小点的后边标上F,表示该测试失败。注意,在模拟出错的测试中,我们会得到详细的测试报告“expected:<4>; but was:<3>;”,这足以告诉我们问题发生在何处。下面是你调试,测试,调试,测试...的过程,直至得到期望的结果。
Design by Contract(这句话我没法翻译)
Design by Contract本是Bertrand Meyer(Eiffel语言的创始人)开发的一种设计技术。我发现在JUnit中使用Design by Contract会带来意想不到的效果。Design by Contract的核心是断言(assersion)。断言是一个布尔语句,该语句不能为假,如果为假,则表明出现了一个bug。Design by Contract使用三种断言:前置条件(pre-conditions)、后置条件(post-conditions)和不变式(invariants)这里不打算详细讨论Design by Contract的细节,而是希望其在测试中能发挥其作用。
前置条件在执行测试之前可以用于判断是否允许进入测试,即进入测试的条件。如 expectedWheels >; 0, myCar != null。后置条件用于在测试执行后判断测试的结果是否正确。如 expectedWheels==myCar.getWheels()。而不变式在判断交易(Transaction)的一致性(consistency)方面尤为有用。我希望JUnit可以将Design by Contract作为未来版本的一个增强。
Refactoring(这句话我依然没法翻译)
Refactoring本来与测试没有直接的联系,而是与软件熵有关,但既然我们说测试能解决软件熵问题,我们也必须说出解决之道。(仅仅进行测试只能发现软件熵,Refactoring则可解决软件熵带来的问题。)软件熵引出了一个问题:是否需要重新设计整个软件的结构?理论上应该如此,但现实不允许我们这么做。这或者是由于时间的原因,或者是由于费用的原因。重新设计整个软件的结构会给我们带来短期的痛苦。而不停地给软件打补丁甚至是补丁的补丁则会给我们带来长期的痛苦。(不管怎样,我们总处于水深火热之中)
Refactoring是一个术语,用于描述一种技术,利用这种技术我们可以免于重构整个软件所带来的短期痛苦。当你refactor时,你并不改变程序的功能,而是改变程序内部的结构,使其更易理解和使用。如:该变一个方法的名字,将一个成员变量从一个类移到另一个类,将两个类似方法抽象到父类中。所作的每一个步都很小,然而1-2个小时的Refactoring工作可以使你的程序结构更适合目前的情况。Refactoring有一些规则:
1>; 不要在加入新功能的同时refactor已有的代码。在这两者间要有一个清晰的界限。如每天早上1-2个小时的Refactoring,其余时间添加新的功能。
2>; 在你开始Refactoring前,和Refactoring后都要保证测试能顺利通过。否则Refactoring没有任何意义。
3>; 进行小的Refactoring,大的不是Refactoring了。如果你打算重构整个软件,没有必要Refactoring了。
只有在添加新功能和调试bug时才又必要Refactoring。不要等到交付软件的后关头才Refactoring。那样和打补丁的区别不大。Refactoring 用在回归测试中也能显示其威力。要明白,我不反对打补丁,但要记住打补丁是应该后使用的必杀绝招。(打补丁也需要很高的技术,详情参看微软网站)
IDE对JUnit的支持
目前支持JUnit的Java IDE 包括 IDE 方式 个人评价(1-5,满分5)
Forte for Java 3.0 Enterprise Edition plug-in 3
JBuilder 6 Enterprise Edition integrated with IDE 4
Visual Age for Java support N/A
在IDE中如何使用JUnit,是非常具体的事情。不同的IDE有不同的使用方法。一旦理解了JUnit的本质,使用起来十分容易了。所以我们不依赖于具体的IDE,而是集中精力讲述如何利用JUnit编写单元测试代码。心急的人可参看资料。
JUnit简介
既然我们已经对JUnit有了一个大致的了解,我希望能给大家提供一个稍微正式一些的编写JUnit测试文档的手册,明白其中的一些关键术语和概念。但我要声明的是这并不是一本完全的手册,只能认为是一本入门手册。同其他OpenSource的软件有同样的问题,JUnit的文档并没有商业软件文档的那种有规则,简洁和完全。由开发人员编写的文档总是说不太清楚问题,全整的文档需要参考"官方"指南,API手册,邮件讨论组的邮件,甚至包括源代码中及相关的注释。
事实上问题并没有那么复杂,除非你有非常特别的要求,否则,只需参考本文你可以得到所需的大部分信息。
安装
首先你要获取JUnit的软件包,从JUnit下载新的软件包(截至写作本文时,JUnit的新版本是3.7)。将其在适当的目录下解包。这样在安装目录(也是你所选择的解包的目录)下你找到一个名为junit.jar的文件。将这个jar文件加入你的CLASSPATH系统变量。(IDE的设置会有所不同,参看你所喜爱的IDE的配置指南)JUnit安装完了。太easy了!
你一旦安装完JUnit,有可能想试试我们的Car和testCar类,没问题,我已经运行过了,你得到的结果应该和我列出的结果类似。(以防新版JUnit使我的文章过时)
接下来,你可能会先写测试代码,再写工作代码,或者相反,先写工作代码,再写测试代码。我更赞成使用前一种方法:先写测试代码,再写工作代码。因为这样可以使我们编写工作代码时清晰地了解工作类的行为。
要注意编写一定能通过的测试代码(如文中的例子)并没有任何意义,只有测试代码能帮助我们发现bug,测试代码才有其价值。此外测试代码还应该对工作代码进行全面的测试。如给方法调用的参数传入空值、错误值和正确的值,看看方法的行为是否如你所期望的那样。
你现在已经知道了编写测试类的基本步骤:
1>;扩展TestCase类;
2>;覆盖runTest()方法(可选);
3>;写一些testXXXXX()方法;
Fixture
解下来的问题是,如果你要对一个或若干个的类执行多个测试,该怎么办?JUnit对此有特殊的解决办法。
如果需要在一个或若干个的类执行多个测试,这些类成为了测试的context。在JUnit中被称为Fixture(如testCar类中的 myCar 和 expectedWheels )。当你编写测试代码时,你会发现你花费了很多时间配置/初始化相关测试的Fixture。将配置Fixture的代码放入测试类的构造方法中并不可取,因为我们要求执行多个测试,我并不希望某个测试的结果意外地(如果这是你要求的,那另当别论了)影响其他测试的结果。通常若干个测试会使用相同的Fixture,而每个测试又各有自己需要改变的地方。
为此,JUnit提供了两个方法,定义在TestCase类中。
protected void setUp() throws java.lang.Exception
protected void tearDown() throws java.lang.Exception
覆盖setUp()方法,初始化所有测试的Fixture(你甚至可以在setUp中建立网络连接),将每个测试略有不同的地方在testXXX()方法中进行配置。
覆盖tearDown()(我总想起一首叫雨滴的吉他曲),释放你在setUp()中分配的性资源,如数据库连接。
当JUnit执行测试时,它在执行每个testXXXXX()方法前都调用setUp(),而在执行每个testXXXXX()方法后都调用tearDown()方法,由此保证了测试不会相互影响。