测试驱动的开发系列 第二部分:用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程序员感兴趣的主题。
他荣幸的毕业于哈佛大学,在那儿他攻读经济学和计算机,他现在住在纽约。
本章的翻译我斗胆省略了其中的几句话。分别是关于素(质)数定义和最大公约数定义的,我不认为会有不了解这两个概念的人来看这篇文章,实在是多此一举(老外一向的习惯)。而且对这种基本概念定义非常麻烦,每个字都必须仔细斟酌,不能有漏洞,翻译也需要如此,我也算偷一下懒吧。