在缺陷和错误发生的时候发现并纠正它们对任何快速、控制成本的软件项目来说是很关键的。由代码编写者实施的单元测试(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()方法。