测试驱动的开发系列 第二部分:用JUnit测试Java类

类别:Java 点击:0 评论:0 推荐:

测试驱动的开发系列 第二部分:用JUnit测试Java类

Test_Driven Development Series Part I:Testing Java Classes with JUnit
By Wellie Chao

January 2004


一、简介(Introduction)

看到这儿你应该已经知道为什么测试如此重要了。如果还没有的话,请先阅读这个分为五个部分的系列论文中的第一篇。第一篇是为什么测试有益于企业级软件的概念性介绍。现在绝大多数的软件是分层实现的:表示层、逻辑层(业务逻辑就在这儿)和数据层。逻辑层是程序的重点,它包括程序所有的规则和行为。人们经常认为测试就是试用一个产品。对汽车来说这非常容易:打火、发动。对桌面应用也很容易:启动程序,四处点点鼠标、敲敲键盘,就可以测试你要测的功能。但是,你怎么测试一个由很多Java class组成的Jar文件呢?


二、测试Java类(Testing Java Classes)

当然,本质上说也可以算是启动Java类,然后点击按钮。测试一个Java类的途径就是在另一个Java类中调用这个类的方法。下面的例子是一个Java源文件。把程序列表1的内容存为FactorCalculator.java。

程序列表1 (FactorCalculator.java, taken from FactorCalculator.java.v1):

import java.util.List;
import java.util.ArrayList;

public class FactorCalculator {
  public int[] factor(int number) {
    List factorList = new ArrayList();
    while(isDivisor(number, 2)) {
      factorList.add(new Integer(2));
      number /= 2;
    }
    int upperBound = (int)Math.sqrt(number) + 1;
    for(int i = 3; i <= upperBound; i += 2) {
      while(isDivisor(number, i)) {
        factorList.add(new Integer(i));
        number /= i;
      }
    }
    if (number != 1) {
      factorList.add(new Integer(number));
      number = 1;
    }
    int[] intArray = new int[factorList.size()];
    for(int i = 0; i < factorList.size(); i++) {
      intArray[i] = ((Integer)factorList.get(i)).intValue();
    }
    return intArray;
  }
  public boolean isPrime(int number) {
    boolean isPrime = true;
    int upperBound = (int)Math.sqrt(number) + 1;
    if (number == 2) {
      isPrime = true;
    } else if (isDivisor(number, 2)) {
      isPrime = false;
    } else {
      for(int i = 3; i <= upperBound; i += 2) {
        if (isDivisor(number, i)) {
          isPrime = false;
          break;
        }
      }
    }
    return isPrime;
  }
  public boolean isDivisor(int compositeNumber, int potentialDivisor) {
    return (compositeNumber % potentialDivisor == 0);
  }
}


(省略180字,译者注)

FactorCalculator类提供的接口很简单:

 factor: 分解一个数获取素因子的方法。
 isPrime: 判断一个数是否素数的方法。
 isDivisor: 判断一个数是否能被另一个数整除的方法。
这些public方法可以构成一个数学库的API。

要测试FactorCalculator,你可以创建一个有main方法可以从命令行调用的Java类。程序列表2就是这样一个测试类。

程序列表2 (CalculatorTest.java, taken from CalculatorTest.java.v1):
public class CalculatorTest {
  public static void main(String [] argv) {
    FactorCalculator calc = new FactorCalculator();
    int[] intArray;
    intArray = calc.factor(100);
    if (!((intArray.length == 4) && (intArray[0] == 2) && (intArray[1] == 2) && (intArray[2] == 5) && (intArray[3] == 5))) {
      throw new RuntimeException("bad factorization of 100");
    }
    intArray = calc.factor(4);
    if (!((intArray.length == 2) && (intArray[0] == 2) && (intArray[1] == 2))) {
      throw new RuntimeException("bad factorization of 4");
    }
    intArray = calc.factor(3);
    if (!((intArray.length == 1) && (intArray[0] == 3))) {
      throw new RuntimeException("bad factorization of 3");
    }
    intArray = calc.factor(2);
    if (!((intArray.length == 1) && (intArray[0] == 2))) {
      throw new RuntimeException("bad factorization of 2");
    }
    boolean isPrime;
    isPrime = calc.isPrime(2);
    if (!isPrime) {
      throw new RuntimeException("bad isPrime value for 2");
    }
    isPrime = calc.isPrime(3);
    if (!isPrime) {
      throw new RuntimeException("bad isPrime value for 3");
    }
    isPrime = calc.isPrime(4);
    if (isPrime) {
      throw new RuntimeException("bad isPrime value for 4");
    }
    try {
      isPrime = calc.isPrime(1);
      throw new RuntimeException("isPrime should throw exception for numbers less than 2");
    } catch (IllegalArgumentException e) {
      // do nothing because throwing IAE is the proper action
    }
    boolean isDivisor;
    isDivisor = calc.isDivisor(6, 3);
    if (!isDivisor) {
      throw new RuntimeException("bad isDivisor value for (6, 3)");
    }
    isDivisor = calc.isDivisor(5, 2);
    if (isDivisor) {
      throw new RuntimeException("bad isDivisor value for (5, 2)");
    }
    try {
      isDivisor = calc.isDivisor(6, 0);
      throw new RuntimeException("isDivisor should throw exception when potentialDivisor (the second argument) is 0");
    } catch (ArithmeticException e) {
      // do nothing because throwing AE is the proper action
    }
    System.out.println("All tests passed.");
  }
}

注意这两个try-catch块,一个测试isPrime,另一个测试isDivisor。有时候抛出异常才是某段代码的正确行为,测试这样的代码时,你必须捕获这个异常,看它是否你想要的。如果抛出的不是你想要的异常,你应该把它扔给(异常)处理链的下一级。如果这段代码应该有异常,但测试时没有抛出,你就要抛出你自己定义的异常,来通知程序功能错误。你应该使用与下文中要介绍的JUnit测试代码类似的模式,来测试那种本来就应该抛出一个或多个异常的部分代码。

为了节省篇幅,上面的测试代码省略了某些内容,例如当isDivisor方法的第二个参数是负数时会出现什么情况。

用javac *.java命令编译这个类,然后输入java CalculatorTest运行它。

你应该得到一个“不能判断小于2的数是否素数”的运行时异常。大概形式如下,当然显示的行数36可能因为你处理CalculatorTest.java中空格的方式而有所不同:
Exception in thread "main" java.lang.RuntimeException: isPrime should throw exception for numbers less than 2
at CalculatorTest.main(CalculatorTest.java:36)

换句话说,FactorCalculator的功能不正确。在isPrime方法最前面加一个判断就可以解决这个问题,判断参数小于2时就抛出IllegalArgumentException异常。就像下面的一小段代码:
if (number < 2) {
  throw new IllegalArgumentException();
}

把上面的代码放到FactorCalculator的isPrime方法的最前面。为了便于你参考,修改后的isPrime方法列在FactorCalculator.java.v2中,如果你打算直接使用这个文件,先把它改名为FactorCalculator.java。

增加了检查后,重新编译运行CalculatorTest,新的CalculatorTest应该可以通过所有测试。


三、JUnit提供测试框架的优势(JUnit Provides Advantages as a Test Framework)

测试Java类的内部功能就是刚才你做的那些工作了。真正的测试和刚才的简单例子的主要区别是代码库的大小和复杂度。在处理一大堆代码时,你会需要收集情况报告。但上面的例子遇到第一个错误就停止了,它没有收集尽可能多的错误信息,也不能报告那些测试可以通过。如果一个测试不通过,就把整个测试重新编译、运行一遍,那开发过程肯定会非常慢。Bug经常是相互关联的,而且由各部分代码交互的地方引起。一次看到多个错误可以帮你分析和解决bug,对有关联的bug的处理也会加快。

在使用JUnit重写这个测试之前,你需要了解下述术语和测试概念:

 1、单元测试(Unit test):单元测试是指一小段代码——绝大多数情况下都只有一个Java类——测试一个软件或者库非常有限的一个部分。单元测试检验的代码都很小,例如一个或几个类。通常是测试EJB组件和普通Java类库,不管这些类在服务器端(容器)环境中还是独立运行。与列表中提到的另一个概念功能测试比起来,单元测试的主要区别在于,单元测试的重点是最终用户一般看不到的内部组件,而功能测试的重点是“点击按钮”。在JUnit中,单元测试可能是TestCase类中的一个方法,也可能是整个TestCase类。从大小上讲一两页的代码对单元测试应该是合适的。如果单元测试达到十页就太夸张了,分成若干个粒度更细的测试会比较好。

 2、功能测试(Functional test):功能测试就是站在最终用户的角度验证程序的功能是否正确。功能测试和黑盒测试是同一个意思。
 3、黑盒测试(Black box test):黑盒测试就是只根据对外发布的接口或公共约定,而忽略程序内部实现进行的测试。这通常意味着你只知道应该输入什么,只测试预期的输出,不知道程序如何生成这些输出,性能、边界影响等其它外部特征也不在你的考虑范围内,除非代码的这些方面特性是公共约定的一部分。如果你开发的软件库中有提供给其它开发者(你的最终用户)使用的API,黑盒测试就显得尤为重要。这个重要性不仅仅是指软件库能按公共接口说的做,软件库避免公共接口中禁止或省略的内容也很重要。如果你开发一个程序,遵守公共接口也是很重要的,因为它使你和同事之间能更有效的合作。
 4、白盒测试(White box test):白盒测试是在知道代码如何实现的情况下测试一段代码的功能。当你要测试公共约定中没有指定,但很重要的行为,例如性能问题时,白盒测试就派上用场了。在测试某些特别复杂的算法或业务逻辑时,也需要白盒测试。这种情况下,通过白盒测试你可以把注意力集中在可能出现错误的地方,这在黑盒测试中由于缺乏对内部情况的了解很难做到。
 5、容器内测试(In-container test):容器内测试在servlet或EJB容器内部进行,因此能更直接的和要测试的代码通信。Jakarta Cactus项目实现了一个免费的测试工具Cactus,它让你和要测试的代码在同一个容器内执行,不管这个容器是servlet容器还是EJB容器。容器内测试对黑盒功能测试没什么用,它的作用体现在单元测试上。
 6、测试用例(Test case):Test case在JUnit中就是一组相关的测试。Test case表现为继承junit.framework.TestCase的类。Test case通常有多个方法,每个方法测试程序一方面的行为。Test case中的测试方法习惯用test作前缀命名,但并不是必须这样做,只要不与其它方法产生冲突就可以。
 7、测试集(test suite):Test suite是一组test case或test suites。它表现为继承junit.framework.TestSuite的类。没有任何限制要求test suite只包括test case或只包括test suite,它可以既有test case,又有test suite。一个test suite的子test suite也可以包括test case和test suite,因此允许嵌套测试。只要你愿意,你可以建立几组test case,每一组测试程序的一个明确的小方面。一组可以形成一个test suite。然后你可以把它们综合到一个主test suite中,最后用这个主test suite测试整个程序,一个功能点一个功能点的测试。
 8、测试启动器(Test runner):Test runner是启动测试过程的JUnit类。你调用test runner,它依次执行你预订的测试。有几种办法可以定义要test runner执行的测试。这些办法在下面的第五部分“指定要运行的测试”中介绍。JUnit有三种不同的test runners:text、AWT和Swing,类名分别是junit.textui.TestRunner、junit.awtui.TestRunner和junit.swingui.TestRunner。


四、编写JUnit测试(Writing a Test with JUnit)

编写JUnit测试,只要扩展junit.framework.TestCase类就可以了。你的TestCase子类将按你希望的顺序调用test cases,包括可能的测试前设置和测试后清除。设置在setUp方法中进行。清除在tearDown方法中进行。你可以,但不是必须,重载这两个方法做你想做的事。

下面是对上面的例子用JUnit进行重写的test case:

程序列表3 (CalculatorTest.java, taken from CalculatorTest.java.v2):

import junit.framework.TestCase;

public class CalculatorTest extends TestCase {

  private FactorCalculator calc;

  public CalculatorTest(String name) {
    super(name);
  }

  protected void setUp() {
    calc = new FactorCalculator();
  }

  public void testFactor() {
    int numToFactor;
    int[] factorArray;
    int[] correctFactorArray;

    numToFactor = 100;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2, 2, 5, 5};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 4;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2, 2};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 3;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {3};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));

    numToFactor = 2;
    factorArray = calc.factor(numToFactor);
    correctFactorArray = new int[] {2};
    assertTrue("bad factorization of " + numToFactor, isSameFactorArray(factorArray, correctFactorArray));
  }

  // presumes both factor arrays are in numeric order
  private boolean isSameFactorArray(int[] factorArray1, int[] factorArray2) {
    boolean isSame = false;
    if (factorArray1.length == factorArray2.length) {
      isSame = true;
      for(int i = 0; i < factorArray1.length; i++) {
        if (factorArray1[i] != factorArray2[i]) {
          isSame = false;
          break;
        }
      }
    }
    return isSame;
  }

  public void testIsPrime() {
    int numToCheck;
    boolean isPrime;

    numToCheck = 2;
    isPrime = calc.isPrime(numToCheck);
    assertTrue("bad isPrime value for " + numToCheck, isPrime);

    numToCheck = 3;
    isPrime = calc.isPrime(numToCheck);
    assertTrue("bad isPrime value for " + numToCheck, isPrime);

    numToCheck = 4;
    isPrime = calc.isPrime(numToCheck);
    assertFalse("bad isPrime value for " + numToCheck, isPrime);

    try {
      numToCheck = 1;
      isPrime = calc.isPrime(numToCheck);
      fail("isPrime should throw exception for numbers less than 2");
    } catch (IllegalArgumentException e) {
      // do nothing because throwing IAE is the proper action
    }
  }

  public void testIsDivisor() {
    int numToCheck;
    int potentialDivisor;
    boolean isDivisor;

    numToCheck = 6;
    potentialDivisor = 3;
    isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
    assertTrue("bad isDivisor value for (" + numToCheck + ", " + potentialDivisor + ")", isDivisor);

    numToCheck = 5;
    potentialDivisor = 2;
    isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
    assertFalse("bad isDivisor value for (" + numToCheck + ", " + potentialDivisor + ")", isDivisor);

    try {
      numToCheck = 6;
      potentialDivisor = 0;
      isDivisor = calc.isDivisor(numToCheck, potentialDivisor);
      fail("isDivisor should throw an exception when potentialDivisor is 0 but did not");
    } catch (ArithmeticException e) {
      // do nothing because throwing AE is the proper action
    }
  }
}


通过方法名assertXxx你可以让JUnit知道你想要的结果,其中Xxx是True、Fase、Equals或者其它条件。JUnit记录assertXxx方法的通过/失败状态,并在执行完所有测试后反馈给你。这儿是一些JUnit中有签名和操作描述的断言(assert)方法:

assertTrue(String errorMessage, boolean booleanExpression): 检查booleanExpression值是否为true。如果不是,把errorMessage添加到错误报告的显示列表中。
assertFalse(String errorMessage, boolean booleanExpression): 检查booleanExpression值是否为false。如果不是,把errorMessage添加到错误报告的显示列表中。
assertEquals(String errorMessage, Object a, Object b): 检查对象a是否等于对象b,通过equals方法,如果不是,把errorMessage添加到错误报告的显示列表中。对象a是期望值,对象b是要测试的程序实际返回的值。
assertNull(String errorMessage, Object o): 检查对象o是否为null。如果不是,把errorMessage添加到错误报告的显示列表中。

要查看所有断言方法的完整列表,请参考Assert类的javadoc文档(http://www.junit.org/junit/javadoc/index.htm)。

你可以在整个测试代码中随意使用assertXxx语句,来确认你要测的代码中某个条件结果为true(或者false,视情况而定)。

 

五、指定要运行的测试(Specifying Which Tests to Run)

要运行你的测试,你需要:
一个TestRunner类的实例。
一个测试类(例如本例的MyTestClass类)的实例,它包含你要运行的测试的。这个类必须继承junit.framework.TestCase。
告诉这个TestRunner实例你的MyTestClass实例中哪些测试要运行的途径。

创建TestRunner的实例和指定MyTestClass实例非常容易,你可以通过下面的命令:
java junit.textui.TestRunner MyTestClass

对别的UI可以用其相应的TestRunner代替junit.textui.TestRunner,例如AWT的junit.awtui.TestRunner和Swing的junit.swingui.TestRunner。你还要用你自己的测试类的名字替换MyTestClass。

有两种途径可以让TestRunner知道你要运行MyTestClass类中的哪些测试。一个是显式途径,一个是默认途径。在MyTestClass中,你可以选择是否包含一个public static方法suite,这个方法没有任何参数,返回Test对象。更准确地说,它返回一个实现Test接口的对象,因为Test是接口,不是类。大多数时候你都使用TestSuite和你自己的TestCase子类,TestSuite和TestCase都实现了Test接口。

如果你在MyTestClass方法中省略了suite方法,那么TestRunner通过reflection机制找到MyTestClass类中所有以“test”为前缀命名的方法,并运行它们。这是通知TestRunner要运行哪些测试的默认途径。

如果你在MyTestClass中实现了suite方法,TestRunner调用suite方法,通过suite方法返回的Test对象,TestRunner获悉它要进行的测试。这是显式途径。TestCase和TestSuite类都实现Test接口,意味着你可以只返回一个TestCase,也可以返回一个包含0到多个TestCase/TestSuite的TestSuite,这样就可以进行多个测试和层次测试。


在junit.framework.TestCase中指定要运行的测试

在TestCase中有两种方式可以指定测试方法:一个静态一个动态。静态方法是重TestCase的runTest方法,在其中调用你的测试。例如:

import junit.framework.TestCase;
public class MySimpleTest extends TestCase {
  public MySimpleTest(String name) {
    super(name);
  }
  public void runTest() {
    testTurnLeft();
  }
  public void testTurnLeft() {
    ... code here ...
  }
}

有时最简单最灵活的重载TestCase.runTest的方式是用一个匿名内部类。下面的代码描述了这种方式:
TestCase testCase = new MySimpleTest("myNameForTurnLeft") {
  public void runTest() {
    testTurnLeft();
  }
}

匿名内部类让你在实例化test类的类中重载runTest,这样在不同的地方可以有不同的runTest实现,它们都使用MySimpleTest作为实际的测试方法。如果你在test类的suite方法中初始化它自己,这个初始化test类的类就是它自己。

通过构造器的name参数可以在TestCase中动态指定测试方法。对上面的MySimpleTest类,你可以写成:
TestCase testCase = new MySimpleTest("testTurnLeft");

因为你没有重载runTest,TestCase类的默认实现将通过reflection找到方法testTurnLeft。你可以用任何你喜欢的名字代替“testTurnLeft”。


六、用junit.framework.TestSuite指定多层测试(Specifying a Hierarchy of Tests to Run With junit.framework.TestSuite)

TestSuite类可以把多个测试打包成一组。基本形式如下:
TestSuite testSuite = new TestSuite();
testSuite.addTest(new MySimpleTest("testTurnLeft"));
testSuite.addTest(new CalculatorTest("testIsDivisor"));
testSuite.addTest(new TestSuite(MyThirdTest.class));

前两个addTest方法是直接调用。TestSuite.addTest方法接受实现Test接口的对象作参数。MySimpleTest和CalculatorTest类都是TestCase的子类,而TestCase实现Test接口。通过前两个addTest方法,你只是把两个测试方法添加到TestSuite实例要执行的测试列表中。

第三个addTest调用描述如何通过在TestSuite实例中包含TestSuite实例来创建层次测试。TestSuite类实现Test接口,所以可以作为addTest方法的参数。第三个addTest调用中,新的TestSuite对象包含MyThirdTest类所有的testXxx方法。没有任何限制要求addTest方法中指定的TestSuite实例是单层列表,子TestSuite还可以包含子TestSuite。


七、再论TestSuite.suite()方法(Back to the TestCase.suite() Method)

现在我们对如何指定TestCase和TestSuite要运行的测试,已经很清楚了,让我们再回过头来看看TestRunner需要的TestCase.suite()方法。这儿有一个TestCase.suite()方法的例子,它添加一个TestCase类的一个测试方法,另一个TestCase类的所有测试方法,以及一个子TestSuite所有层次的测试方法。

程序列表4 (a suite method demonstrating many different ways of specifying tests):

public static suite() {
  TestSuite globalTestSuite = new TestSuite();

  TestCase addToCartTestCase = new ShopCartTest("testAddToCart");
  globalTestSuite.addTest(addToCartTestCase);

  TestCase checkOutTestCase = new ShopCartTest("testCheckOut");
  globalTestSuite.addTest(checkOutTestCase);

  TestSuite calcTestSuite = new TestSuite(CalculatorTest.class);
  globalTestSuite.addTest(calcTestSuite);

  TestSuite fileModuleTestSuite = new TestSuite();
  fileModuleTestSuite.addTest(new ImportExportTest("testImport"));
  fileModuleTestSuite.addTest(new TestSuite(SaveFileTest.class));
  globalTestSuite.addTest(fileModuleTestSuite);

  return globalTestSuite;
}

好,你已经了解了如何向TestRunner指定测试的不同方法,你应该开始这些测试了。如果你在CalculatorTest中添加了一个suite方法,把它删掉,因为在下一小节中TestRunner将运行CalculatorTest类中所有的testXxx方法。suite方法在你要做大量测试时非常重要。

八、运行测试(Running the Test)

输入javac -classpath ~/packages/junit3.8.1/junit.jar *.java编译CalculatorTest类。用你机器上junit.jar文件的路径代替“~/packages/junit3.8.1/junit.jar”。输入java -classpath ~/packages/junit3.8.1/junit.jar:. junit.textui.TestRunner CalculatorTest运行测试。这儿的junit.jar路径也需要替换。为了避免每次都要在命令行指定classpath,把JUnit库和当前目录都加到classpath中。Linux下你可以在bash shell中用这两个命令:
CLASSPATH=~/packages/junit3.8.1/junit.jar:.
export CLASSPATH

注意把“~/packages/junit3.8.1/junit.jar”替换为junit.jar文件的正确路径,而且不要忘了后面的冒号和点。Windows下设置环境变量的命令是"set",你可以用它把CLASSPATH设置为类似的值,除了正斜杠改成反斜杠。把“.”加入classpath是为了让JUnit TestRunner能找到当前目录下的CalculatorTest。对本文来说,你应该使用“.”而不是当前路径的硬编码,因为你还要练习其它的例子,这样无论你在做那个例子,你都访问和执行新的当前目录下的类。下面假定你已经正确设置了你的classpath。

运行CalculatorTest中的测试后,你应该看到下面的输出:
...
Time: 0.008

OK (3 tests)

一串点表示JUnit正在运行,JUnit还在统计行显示通过或失败的测试数目。如果某个测试失败了,显示结果可能就不是上面那样,而是:

..F
Time: 0.01
There was 1 failure:
1) testAddition(Test) "expected:<5> but was:<4>"

FAILURES!!!
Tests run: 2,  Failures: 1,  Errors: 0


九、其它TestRunner类和执行方法(Different TestRunner Classes and Ways of Executing Them)

有好几个TestRunner你可以使用:text、AWT和Swing。对应的类分别是junit.textui.TestRunner、junit.awtui.TestRunner和junit.swingui.TestRunner。运行它们的命令类似:
java junit.awtui.TestRunner CalculatorTest

--或者--

java junit.swingui.TestRunner CalculatorTest

AWT和Swing版本的TestRunner需要在Windows、OS X或X11等图形环境中使用。它们用交互的图形格式显示运行结果。text UI是最常用的,因为测试一般都用批处理模式运行,这时交互是一种缺点。

当你调用TestRunner,把测试类的名字传给它时,TestRunner加载你的类,使用reflection找到所有以“test”开始的方法。如果你不想在命令行用java调用TestRunner类,你还有另一重办法:直接调用包含test suite的类的main方法。

输入下述内容作为TestCase子类的main方法:
public static void main(String[] argv) {
  junit.textui.TestRunner.run(suite());
}

如果你需要的话,用junit.awtui.TestRunner或junit.swingui.TestRunner代替junit.textui.TestRunner。

方便起见,示例文件中提供了CalculatorTest类的另一个版本CalculatorTest.java.v3,它包含suite方法和前面描述的main方法,当然,你在使用之前需要把它改名为CalculatorTest.java。如果你使用的环境是Linux,或者是Cygwin在Windows下模拟的UNIX,你可以用diff命令查看不同版本之间的差别,这在学习新东西时通常很有用。例如,输入diff CalculatorTest.java.v2 CalculatorTest.java.v3就可以查看CalculatorTest.java.v2和CalculatorTest.java.v3之间的区别。

编译新的CalculatorTest类之后,就可以运行了。这次不必输入java junit.textui.TestRunner CalculatorTest,可以用java CalculatorTest代替。

十、在程序和测试中添加功能(Adding Functionality to Your Application and to Your Test)

现在假如说你要在FactorCalculator中添加功能。测试驱动的开发方式建议你首先增加测试,验证测试失败,(代码还没写当然失败,译者注。)然后再编写新功能的代码,并确保测试通过。现在假如你要增加一个求最大公约数的方法,名字叫“gcd”。(此处省略20字,译者注)可以用下面三条验证:6和4的最大公约数是2。36和18的最大公约数是18。30和75的最大公约数是15。

刚才那些都是正常的例子。除此之外,你还应该测试边缘和错误的情况,例如gcd(2, 1)和gcd(3, -1)。下面的代码就可以做这些测试:

程序列表 5:

public void testGcd() {
  assertEquals("bad value for gcd(6, 4)", 2, calc.gcd(6, 4));
  assertEquals("bad value for gcd(36, 18)", 18, calc.gcd(36, 18));
  assertEquals("bad value for gcd(30, 75)", 15, calc.gcd(30, 75));
  assertEquals("bad value for gcd(2, 1)", 1, calc.gcd(2, 1));
  try {
    calc.gcd(3, -1);
    fail("gcd should throw exception for when either argument is less than 1");
  } catch (IllegalArgumentException e) {
    // do nothing because throwing IAE is the proper action
  }
}

把程序列表5中的代码添加到CalculatorTest.java。如果你不想敲键盘,你可以从示例代码中拷贝CalculatorTest.java.v4,把名字改成CalculatorTest.java。

为了让CalculatorTest能编译,需要在FactorCalculator中添加一个stub,你在CalculatorTest中调用了gcd方法,你就必须在FactorCalculator中定义gcd方法。把下述内容加到FactorCalculator中:

public int gcd(int a, int b) {
  return 1;
}

如果你不想敲键盘,你可以从示例代码中拷贝FactorCalculator.java.v3,把名字改成FactorCalculator.java。

很明显,大部分情况下上面的gcd方法都返回错误结果,但测试驱动的开发方式信奉“先让它出错,然后再纠正它”( "errors first, then correction of errors")为最有效的开发模式。测试代码和其它代码一样,也可能出错。有可能你的测试代码不能发现程序中的错误。如果你在开发程序功能前编写测试代码,你就可以确保测试正确发现错误,因此减少错误被疏漏的机会。

输入javac *.java编译FactorCalculator和CalculatorTest类。在编译和运行时都需要确保JUnit库在classpath中。输入java CalculatorTest。你将看到下面的输出:

....F
Time: 0.01
There was 1 failure:
1) testGcd(CalculatorTest)junit.framework.AssertionFailedError: bad value for gcd(6, 4) expected:<2> but was:<1>
        at CalculatorTest.testGcd(CalculatorTest.java:125)
        ...
        at CalculatorTest.main(CalculatorTest.java:14)

FAILURES!!!
Tests run: 4,  Failures: 1,  Errors: 0

开始测试失败了,这是意料之中的结果,也正是我们想要的。如果它不失败,那就意味着测试的设计或实现出了问题。JUnit会把你给它的信息显示出来,所以应该写有意义的错误信息。现在修复FactorCalculator类,让它通过测试。删除FactorCalculator类gcd方法中的“return 1;”一行,用下面的代码代替:

程序列表6 (functional implementation of gcd method):

int gcd = 1;
int smallerInt = (a < b) ? a : b;
for(int i = smallerInt; i > 1; i--) {
  if (isDivisor(a, i) && isDivisor(b, i)) {
    gcd = i;
    break;
  }
}
return gcd;


如果你不想敲键盘,你可以从示例代码中拷贝FactorCalculator.java.v4,把名字改成FactorCalculator.java。

输入javac FactorCalculator.java重新编译javac FactorCalculator。输入java CalculatorTest重跑测试。你仍然得到错误,因为参数错误时gcd方法没有抛出异常。调用gcd(3, -1)应该产生一个IllegalArgumentException,但事实上没有。把下面的代码加到gcd方法的最前面可以解决这个问题。

if ((a < 1) || (b < 1)) {
  throw new IllegalArgumentException();
}

修改后的FactorCalculator是示例代码中的FactorCalculator.java.v5,你可以更名为FactorCalculator.java。重新编译FactorCalculator.java后运行测试。一切正常,测试通过,状态报告类似:

....
Time: 0.008

OK (4 tests)


十一、总结(Conclusion)

现在你已经知道如何用JUnit进行单元测试了,在你自己的代码中进行试验吧,亲身体会一下程序测试的好处。

现在准备进入测试驱动开发系列的下一章。下一章,也就是五个部分中的第三部分,将带你进入如何在EJB容器中测试服务器端EJB组件的操作细节。


作者
Wellie Chao从1984年开始对软件开发产生兴趣,并在1994年成为职业程序员至今。他领导了几个构建企业应用的软件项目,在Java和Perl语言上有很深的造诣,他出版过几本Java的书籍,其中“Core Java Tools”(Prentice Hall)讲述了用Ant、CVS和JUnit等开源Java工具进行极限编程和测试驱动的开发等主题。他在IBMdeveloperWorks、DevX、TheServerSide和其它地方发表的论文涵盖了开发企业软件的Java程序员感兴趣的主题。
他荣幸的毕业于哈佛大学,在那儿他攻读经济学和计算机,他现在住在纽约。

 

 

本章的翻译我斗胆省略了其中的几句话。分别是关于素(质)数定义和最大公约数定义的,我不认为会有不了解这两个概念的人来看这篇文章,实在是多此一举(老外一向的习惯)。而且对这种基本概念定义非常麻烦,每个字都必须仔细斟酌,不能有漏洞,翻译也需要如此,我也算偷一下懒吧。

本文地址:http://com.8s8s.com/it/it16775.htm