TestCase
需要提醒一下,在junit.framework.Assert类中定义了相当多的assert方法,主要有assert(), assert(), assertEquals(), assertNull(), assertSame(), assertTrue(), fail()等方法。如果你需要比较自己定义的类,如Car。assert方法需要你覆盖Object类的equals()方法,以比较两个对象的不同。实践表明:如果你覆盖了Object类的equals()方法,好也覆盖Object类的hashCode()方法。再进一步,连带Object类的toString()方法也一并覆盖。这样可以使测试结果更具可读性。
当你设置好了Fixture后,下一步是编写所需的testXXX()方法。一定要保证testXXX()方法的public属性,否则无法通过内省(reflection)对该测试进行调用。
每个扩展的TestCase类(也是你编写的测试类)会有多个testXXX()方法。一个testXXX()方法是一个测试。要想运行这个测试,你必须定义如何运行该测试。如果你有多个testXXX()方法,你要定义多次。JUnit支持两种运行单个测试的方法:静态的和动态的方法。
静态的方法是覆盖TestCase类的runTest()方法,一般是采用内部类的方式创建一个测试实例:
TestCase test01 = new testCar("test getWheels") {
public void runTest() {
testGetWheels();
}
}
采用静态的方法要注意要给每个测试一个名字(这个名字可以任意起,但你肯定希望这个名字有某种意义),这样你可以区分那个测试失败了。
动态的方法是用内省来实现runTest()以创建一个测试实例。这要求测试的名字是需要调用的测试方法的名字:
TestCase test01 = new testCar("testGetWheels");
JUnit会动态查找并调用指定的测试方法。动态的方法很简洁,但如果你键入了错误的名字会得到一个令人奇怪的NoSuchMethodException异常。动态的方法和静态的方法都很好,你可以按照自己的喜好来选择。(先别着急选择,后面还有一种更酷的方法等着你呢。)
TestSuite
一旦你创建了一些测试实例,下一步是要让他们能一起运行。我们必须定义一个TestSuite。在JUnit中,这要求你在TestCase类中定义一个静态的suite()方法。suite()方法像main()方法一样,JUnit用它来执行测试。在suite()方法中,你将测试实例加到一个TestSuite对象中,并返回这个TestSuite对象。一个TestSuite对象可以运行一组测试。TestSuite和TestCase都实现了Test接口(interface),而Test接口定义了运行测试所需的方法。这允许你用TestCase和TestSuite的组合创建一个TestSuite。这是为什么我们前面说TestCase,TestSuite以及TestSuite组成了一个composite Pattern的原因。例子如下:
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(new testCar("testGetWheels"));
suite.addTest(new testCar("testGetSeats"));
return suite;
}
从JUnit 2.0开始,有一种更简单的动态定义测试实例的方法。你只需将类传递给TestSuite,JUnit会根据测试方法名自动创建相应的测试实例。所以你的测试方法好取名为testXXX()。例子如下:
public static Test suite() {
return new TestSuite(testCar.class);
}
从JUnit的设计我们可看出,JUnit不仅可用于单元测试,也可用于集成测试。关于如何用JUnit进行集成测试请参考相关资料。
为了兼容性的考虑,下面列出使用静态方法的例子:
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(
new testCar("getWheels") {
protected void runTest() { testGetWheels(); }
}
);
suite.addTest(
new testCar("getSeats") {
protected void runTest() { testGetSeats(); }
}
);
return suite;
}
TestRunner
有了TestSuite我们可以运行这些测试了,JUnit提供了三种界面来运行测试
[Text UI] junit.textui.TestRunner
[AWT UI] junit.awtui.TestRunner
[Swing UI] junit.swingui.TestRunner
我们前面已经看过文本界面了,下面让我们来看一看图形界面:
界面很简单,键入类名-testCar。或在启动UI的时候键入类名:
[Windows] d:>;java junit.swingui.TestRunner testCar
[Unix] % java junit.swingui.TestRunner testCar
从图形UI可以更好的运行测试可查单测试结果。还有一个问题需要注意:如果JUnit报告了测试没有成功,JUnit会区分失败(failures)和错误(errors)。失败是一个期望的被assert方法检查到的结果。而错误则是意外的问题引起的,如ArrayIndexOutOfBoundsException。
由于TestRunner十分简单,界面也比较直观,故不多介绍。朋友们可自行参考相关资料。
JUnit佳实践
Martin Fowler(又是这位高人)说过:“当你试图打印输出一些信息或调试一个表达式时,写一些测试代码来替代那些传统的方法。”一开始,你会发现你总是要创建一些新的Fixture,而且测试似乎使你的编程速度慢了下来。然而不久之后,你会发现你重复使用相同的Fixture,而且新的测试通常只涉及添加一个新的测试方法。
你可能会写许多测试代码,但你很快会发现你设想出的测试只有一小部分是真正有用的。你所需要的测试是那些会失败的测试,即那些你认为不会失败的测试,或你认为应该失败却成功的测试。
我们前面提到过测试是一个不会中断的过程。一旦你有了一个测试,你要一直确保其正常工作,以检验你所加入的新的工作代码。不要每隔几天或后才运行测试,每天你都应该运行一下测试代码。这种投资很小,但可以确保你得到可以信赖的工作代码。你的返工率降低了,你会有更多的时间编写工作代码。
不要认为压力大,不写测试代码。相反编写测试代码会使你的压力逐渐减轻,应为通过编写测试代码,你对类的行为有了确切的认识。你会更快地编写出有效率地工作代码。下面是一些具体的编写测试代码的技巧或较好的实践方法:
1. 不要用TestCase的构造函数初始化Fixture,而要用setUp()和tearDown()方法。
2. 不要依赖或假定测试运行的顺序,因为JUnit利用Vector保存测试方法。所以不同的平台会按不同的顺序从Vector中取出测试方法。
3. 避免编写有副作用的TestCase。例如:如果随后的测试依赖于某些特定的交易数据,不要提交交易数据。简单的会滚可以了。
4. 当继承一个测试类时,记得调用父类的setUp()和tearDown()方法。
5. 将测试代码和工作代码放在一起,一边同步编译和更新。(使用Ant中有支持junit的task.)
6. 测试类和测试方法应该有一致的命名方案。如在工作类名前加上test从而形成测试类名。
7. 确保测试与时间无关,不要依赖使用过期的数据进行测试。导致在随后的维护过程中很难重现测试。
8. 如果你编写的软件面向国际市场,编写测试时要考虑国际化的因素。不要仅用母语的Locale进行测试。
9. 尽可能地利用JUnit提供地assert/fail方法以及异常处理的方法,可以使代码更为简洁。
10.测试要尽可能地小,执行速度快。
事实上,JUnit还可用于集成测试,但我并没涉及到,原因有两个:一是因为没有单元测试,集成测试无从谈起。我们接受测试地概念已经很不容易了,如果再引入集成测试会更困难。二是我比较懒,希望将集成测试的任务交给测试人员去做。在JUnit的网站上有一些相关的文章,有空大家可以翻一翻。
JUnit与J2EE
如果大家仔细考虑一下的话,会发现,JUnit有自己的局限性,比如对图形界面的测试,对servlet/JSP以及EJB的测试我们都没有举相关的例子。实际上,JUnit对于GUI界面,servlet/JSP,JavaBean以及EJB都有办法测试。关于GUI的测试比较复杂,适合用一整篇文章来介绍。这里不多说了。
前面我们所做的测试实际上有一个隐含的环境,JVM我们的类需要这个JVM来执行。而在J2EE框架中,servlet/JSP,EJB都要求有自己的运行环境:Web Container和EJB Container。所以,要想对servlet/JSP,EJB进行测试需要将其部署在相应的Container中才能进行测试。由于EJB不涉及UI的问题(除非EJB操作XML数据,此时的测试代码比较难写,有可能需要你比较两棵DOM树是否含有相同的内容)只要部署上去之后可以运行测试代码了。此时setUp()方法显得特别有用,你可以在setUp()方法中利用JNDI查找特定的EJB。而在testXXX()方法中调用并测试这些EJB的方法。
这里所指的JavaBean同样没有UI的问题,比如,我们用JavaBean来访问数据库,或用JavaBean来包裹EJB。如果这类JavaBean没有用到Container的提供的服务,则可直接进行测试,同我们前面所说的一般的类的测试方法一样。如果这类JavaBean用到了Container的提供的服务,则需要将其部署在Container中才能进行测试。方法与EJB类似。
对于servlet/JSP的测试则比较棘手,有人建议在测试代码中构造HttpRequest和HttpResponse,然后进行比较,这要求开发人员对HTTP协议以及servlet/JSP的内部实现有比较深的认识。我认为这招不太现实。也有人提出使用HttpUnit。由于我对Cactus和HttpUnit 了解不多,所以无法做出合适的建议。希望各位先知们能不吝赐教。
正是由于JUnit的开放性和简单易行,才会引出这篇介绍文章。但技术总在不断地更新,而且我对测试并没有非常深入的理解;我可以将一个复杂的概念简化成一句非常容易理解的话。但我的本意只是希望能降低开发人员步入测试领域的门槛,而不是要修改或重新定义一些概念。这一点是特别要强调的。后,如果有些兄弟姐妹能给我指出一些注意事项或我对某些问题的理解有误,我会非常感激的。