技术开发 频道

单元测试和编码协作

【IT168 技术文章】

        在缺陷和错误发生的时候就发现并纠正它们对任何快速、控制成本的软件项目来说是很关键的。由代码编写者实施的单元测试(Unit Test)是最常用的方法。本文的作者Panagiotis Louridas总结了他在最流行的Java单元测试OSS(开放源代码软件)工具——JUnit上的经验。

        自1947年9月9日一只飞蛾被发现困在Harvard Mark Ⅱ中开始,调试问题就一直困扰着程序员。(这第一个“真实的Bug”,至今还存放在Smithsonian的美国国家历史博物馆)。计算机先驱Manrice Wilkes在谈到他意识到调试的严重性的时候说道:“我生命中余下的时间,将有很大的一部分将被用于从我自己所写的代码中查找错误。”他的这个解释在很多的计算机专家身上得到了验证。
        单元测试是一个检测bug的方法。首先,单元测试适用于单独的代码片断。例如函数,方法。除非这些最小的代码片断是正确的,否则,整个软件的“大厦“将会倒塌。其次,单元测试让代码的编写者也同时来参与代码的测试。通过测试能够查出bug,但程序员除了编码不愿花费时间在其他任何事上。
        JUnit是一个开放源代码的Java类库,目的是让单元测试更简单、有趣。事实上JUnit是如此的有趣以至于程序员会爱上编写测试代码。Kent Beck和Erich Gamma从Beck的SmallTalk测试框架中获得了灵感并建立了JUnit项目。最初的想法是让编码和测试能同时进行。程序员写若干行的代码,然后为这几行的代码编写测试。如果没有通过测试,就改正程序中的bug;如果正常通过测试,就继续编码和测试。所有的测试码随着代码的变化而变化,一旦运行代码发生变化,通过回归测试的方法能保证整个软件不会因为这些变化而遭到破坏。
        编码和测试的整合是极限编程XP(eXtreme Programming)和敏捷软件开发活动的中心环节。在敏捷软件开发过程中,软件通过不断增加实现功能来构建,简言之就是活动的突发催生软件功能的完善。)为了达到这个目的则必须保证所有已经完成的代码都是正确的,只有这样才能有自信在这些代码的基础上进行后续开发。

    一、JUnit简介
        在单元测试中,我们经常编写这样的代码:按实际需求提供输入的代码和以输出结果形式展示的代码的运行情况。程序员长期使用的一个简单的方法是:编写一系列的if条件语句与预期的结果来进行比较。在JUnit中,我们并不需要这些if语句,而是写断言(assertion)。断言是这样一个方法:预先标志出期望的结果,并将得到的结果与之进行比较,如果匹配,则断言成功,否则断言失败。
        一般的说,测试需要建立“测试框架”:例如初始化变量,创建对象等。在JUnit中,准备工作被称为装配(setup)。setup和assertions是相互独立的,所以相同的测试框架可适用于若干独立的测试。setup过程在测试以前执行。
        而当一个测试结束的时候,可能需要对测试框架进行一些清理活动。在JUnit中,清理活动被称为拆卸(TearDown)。它保证每一个测试不会留下任何的影响。如果接下来有另一个测试活动开始了,那么这个测试的setup过程就能被正确得执行。setup和teardown被成为一个测试的固定环节
        单独的测试被称为测试用例(Test Cases),测试用例几乎不存在于真空中。如果一个项目经历了若干个单元测试,就能积累一定数量的测试用例集。程序员往往将这些测试一起运行。如果测试一起运行,就将测试联合成为测试组(test suites)。这种情况下,程序员将多个测试用例组合成为一个测试组(test suites),作为一个单独的整体来运行。

    二、一个例子
        用实践来理解JUnit往往更简单。设想我们需要一个Complex类(当然不是从其他地方得到现成的)。这个Complex类应当包含常规的复数运算和操作,我们会对这些方法进行测试。
        首先,新建一个TestCase子类。在这个子类中,为fixture的每一部分(setup和teardown)新建实例变量。然后重写setUp()(注意保留)方法以初始化变量,重写tearDown()方法以清理所有测试过程中使用的资源从而避免任何的副作用。在这个例子中,所有的操作并没有副作用,所以我们并不需要重写tearDown()方法。Figure 1(a)中展示了如何创建一个TestCase子类并重写setUp()方法。
(a)
       import junit.framework.TestCase;
       import junit.framework.Test;
       import junit.framework.TestSuite;
       public class ComplexTest extends TestCase {
              private Complex a;
              private Complex b;
              protected void setUp() {
                     a = new Complex(1, -1);
                     b = new Complex(2, 5);
              }
       }
       (b)
       public void testComplexEquality()
              Complex expected = new Complex(1, -1);
              assertEquals(expected, a);
       }
       public void testComplexAddition() {
              Complex expected = new Complex(3, 4);
              assertEquals(expected, a.add(b));
       }
       public void testComplexMultiplication() {
              Complex expected = new Complex(1*2 - (-1)*5,1*5 + (-1)*2);
              assertEquals(expected, a.multiply(b));
       }
(c)
       public static Testsuite() {
              return new TestSuite(ComplexTest.class);
      }
Figure 1 一个复数类例子:(a)JUnit测试的setup,(b)JUnit测试的实例,(c)在一个类中为所有实例动态得创建一个测试组(test suite)
 
        要创建测试用例,我们要在ComplexTest类中添加包含我们要执行的assertion的方法。测试方法名以“test”开始,并且必须是public修饰的(public 类型)。只有这样JUnit才能通过Java的反射机制来调用它。我们接下来要测试的是对象的判等在我们的Complex类中重写了java.lang.Object的equals()方法,加法和乘法。
    Figure 1(b)展示了上述几个测试方法的实现。
        我们可以用任何的Java原始类中都定义的assertEquals方法。(The assertEquals method is overloaded and defined for all wuhuif primitives, apart from wuhuif objects.这里的assertEquals()方法重写和定义了所有Java原始类中的定义。)也可以用assertTrue和assertFalse来测试条件;用assertNull和assertNotNull来判断是否为空的引用;assertSame和assertNotSame来测试两个对象是否指向同一个引用。
        接下来就要开始运行测试了。最简单的方法就是让JUnit通过反射机制来找到预定义的测试用例。当然也可以通过编写一些其他的代码来静态指定要使用的测试用例。要用反射机制来动态指定要运行的测试用例,我们只需要像Figure 1(c)一样在ComplexTest类中添加一个Testsuite()方法。

    三、运行测试
        运行测试时,我们不需要为Complex类添加任何一行代码。事实上,在以测试驱动的开发过程中,是先编写测试,然后写具体的类的实现。当然一开始就编写类的代码也不会有任何的问题。JUnit的相关信息可以在http://www.junit.org/上找到。这个站点中有很多文档,包括Beck和Gamma原创的介绍JUnit的文章。JUnit的安装非常简单,只需要解压就完成了。在编译的过程中,需要确保ClassPath中包含了JUnit所在的目录。假设你将程序放在/usr/java/JUnit下,编译的命令就是javac –classpath “.:/usr/java/JUnit/JUnit.jar” ComplexTest.java。
        要运行测试,可以通过一下命令启动JUnit的图形界面:java -classpath “.:/usr/java/JUnit/JUnit.jar” JUnit.swingui.TestRunner 然后输入包含测试组的类名。接下来JUnit就会进行测试,如果所有的测试都通过了,窗口上就会出现一个绿色的滚动条,如Figure2所示(图略)。由此也就有了JUnit的箴言:“Keep the bargreen to keep the code clean.”,意思为,保持条为绿色,也就得到了良好的代码。如果出现了错误,条就会变称红色,同时会出现一个关于失败测试的详细信息。如果你更喜欢文本界面,可以通过如下命令调用:
wuhuif -classpath “.:/usr/java/JUnit/JUnit.jar” JUnit.textui. TestRunner ComplexTest,在中端窗口中就会出现运行结果。
        现在,你可以为Complex类添加更多的方法,比如一个复数的除法。与此同时,你也要为ComplexTest类添加一个测试用例,运行测试,然后纠正任何存在的错误,继续编码和测试。

    四、JUnit的遗赠和竞争
        JUnit的成功激励了很多促进特殊代码测试的扩展的诞生。这些扩展的目标是对数据库代码、EJB,Servlets等进行测试。JUnit也能很好的和一些开发环境(工具)相结合。JUnit Ant Task允许Ant调用JUnit的测试任务(要了解更多关于Ant,见Nicholas Serrano 和 Ismael Ciordia 2004年11、12月份的专栏)。JUnit的官方网站上包括了很多JUnit的扩展和与之结合的开发环境的介绍和链接。
        如果你最喜欢开发语言不是Java,其他的语言也提供了JUnit类似的接口和框架,xUNIT的符号代表了整个家族:CppUnit负责C++的单元测试;perlUnit是Perl的单元测试接口。PyUnit是Python语言的单元测试标准框架;Test::Unit则控制了整个Ruby社区;Nunit是用C#完成的,为.Net平台提供了开源的单元测试支持。
        TestNG(www.beust.com/testng)是近来兴起的一个JUnit的替代品,吸引了很多开发者的注意,ParaSoft的Jtest(www.parasoft.com/jtest)是一个流行并且经济的测试工具。在表格中把上述工具和JUnit进行了比较。
        在表格中,我们可以看到Jtest提供了额外的一些特性,而这些特性你可能会认为只有那些比开源软件大100倍的商业软件才能拥有。不管怎样,这写额外的特性反映了一些不同的理念。本质上,JUnit是一个很简单的类库。事实上,如果你愿意,你可以从头开始写。JUnit在设计的时候,被设计为只用来做一件事情,但是一定要做好它。这个理念和Unix的工具的理念非常类似:让开发人员可以放入工具箱中,从而得到额外的功能。几个常见的JUnit例子有:JTestCase (http://jtestcase.sourceforge.net/)
 JUnitDoclet(http://www.junitdoclet.org/)
 JUnitPerf (www.clarkware.com/software/JUnitPerf.html)
 MockObjects(http://www.mockobjects.com/)
 EasyMock (http://www.easymock.org/)。
        而另一方面,商业的测试框架则提供了尽可能多的功能和综合环境。

    五、测试覆盖
        在编码和测试的过程中,你必须确认所有的代码都被测试到了。JUnit能帮助编写测试,然而选择一个完整的测试集却依赖于每一个程序员。在测试中有很多种测试覆盖。例如语句覆盖(也被称为线性覆盖),结果覆盖(也被称为分支覆盖或路径覆盖)等等。如果你正在为一个开源项目工作,就能得到Clover(http://www.cenqua.com/clover/)这是一个商业的测试覆盖工具,但遵循了非商业的许可证。另外一个商业的测试覆盖工具Jcoverage (http://www.jcoverage.com/)拥有一个遵循GPL的版本,但在功能上有所限制。GroboUtils (http://groboutils.sourceforge.net/)包含了一个覆盖分析的包。Emma (http://emma.sourceforge.net/)是另外一个易用的工具。两个JUnit特有的工具包是NoUnit (http://nounit.sourceforge.net/using.html) 和 Jester (http://jester.sourceforge.net/)。GNU的编译器网站也包含了一个叫GCov的工具。

    六、开发过程种的单元测试
        援引Glenford Myers关于测试的经典书中的一句话:不要丢弃测试用例,除非程序被丢弃("Avoid throwaway test cases unless the program is truly a throwaway program."见JUnit Resources栏)。你必须让你的代码和测试保持同步,做到这一点,无论什么时候代码改变了,回归测试都很简单。
        用心计划的测试集合能创建非常有效的项目文档。看一个类如何运行的最好办法就是看它的运行,而单元测试用例包含的代码片断则正好能满足这一点。
        一个综合的测试集合并不会浪费运行资源,因为测试代码不会干扰程序。但是测试会消耗一定的编译时的资源,不断增加的测试用例能拖慢编译的过程,要避免这个状况,开发人员可以按需要定制编译过程来完成系统的构建。比如可以用Ant来定义一组测试任务,这些任务能在每天结束的时候,在代码提交以前执行。

    当然,单元测试只是测试环节中的很小一部分。除单元测试以外,我们还有将某一功能相关的代码一起执行测试,这就是功能测试,功能测试下面以后,就是整个系统的测试(系统测试);在交付以前,最终用户会看软件是否达到了预定的需求(接受测试)等等。但是为软件的最小部分测试添加很多的舒适和乐趣会给后面的测试打下结实的基础。

0
相关文章