JUnit 是 Java? 语言
事实上的 标准单元测试库。JUnit 4 是该库三年以来最具里程碑意义的一次发布。它的新特性主要是经过采用 Java 5 中的标记(annotation)而不是利用子类、反射或命名机制来识别测试,从而简化测试。在本文中,执着的代码测试人员 Elliotte Harold 以 JUnit 4 为例,详细介绍了如何在本身的工做中使用这个新框架。注意,本文假设读者具备 JUnit 的使用经验。
JUnit 由 Kent Beck 和 Erich Gamma 开发,几乎毫无疑问是迄今所开发的最重要的第三方 Java 库。正如 Martin Fowler 所说,“在软件开发领域,历来就没有如此少的代码起到了如此重要的做用”。JUnit 引导并促进了测试的盛行。因为 JUnit,Java 代码变得更健壮,更可靠,bug 也比之前更少。JUnit(它自己的灵感来自 Smalltalk 的 SUnit)衍生了许多 xUnit 工具,将单元测试的优点应用于各类语言。nUnit (.NET)、pyUnit (Python)、CppUnit (C++)、dUnit (Delphi) 以及其余工具,影响了各类平台和语言上的程序员的测试工做。
然而,JUnit 仅仅是一个工具而已。真正的优点来自于 JUnit 所采用的思想和技术,而不是框架自己。单元测试、测试先行的编程和测试驱动的开发并不是都要在 JUnit 中实现,任何比较 GUI 的编程都必须用 Swing 来完成。JUnit 自己的最后一次更新差很少是三年之前了。尽管它被证实比大多数框架更健壮、更持久,可是也发现了 bug;而更重要的是,Java 不断在发展。Java 语言如今支持泛型、枚举、可变长度参数列表和注释,这些特性为可重用的框架设计带来了新的可能。
JUnit 的停滞不前并无被那些想要废弃它的程序员所战胜。挑战者包括 Bill Venners 的 Artima SuiteRunner 以及 Cedric Beust 的 TestNG 等。这些库有一些可圈可点的特性,可是都没有达到 JUnit 的知名度和市场占有份额。它们都没有在诸如 Ant、Maven 或 Eclipse 之类的产品中具备普遍的开箱即用支持。因此 Beck 和 Gamma 着手开发了一个新版本的 JUnit,它利用 Java 5 的新特性(尤为是注释)的优点,使得单元测试比起用最初的 JUnit 来讲更加简单。用 Beck 的话来讲,“JUnit 4 的主题是经过进一步简化 JUnit,鼓励更多的开发人员编写更多的测试。”JUnit 4 尽管保持了与现有 JUnit 3.8 测试套件的向后兼容,可是它仍然承诺是自 JUnit 1.0 以来 Java 单元测试方面最重大的改进。
注意:该框架的改进是至关前沿的。尽管 JUnit 4 的大轮廓很清晰,可是其细节仍然能够改变。这意味着本文是对 JUnit 4 抢先看,而不是它的最终效果。
之前全部版本的 JUnit 都使用命名约定和反射来定位测试。例如,下面的代码测试 1+1 等于 2:
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
public void testAddition() {
int z = x + y;
assertEquals(2, z);
}
} |
而在 JUnit 4 中,测试是由
@Test
注释来识别的,以下所示:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void testAddition() {
int z = x + y;
assertEquals(2, z);
}
} |
使用注释的优势是再也不须要将全部的方法命名为
testFoo()
、
testBar()
,等等。例如,下面的方法也能够工做:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void additionTest() {
int z = x + y;
assertEquals(2, z);
}
} |
下面这个方法也一样可以工做:
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
assertEquals(2, z);
}
} |
这容许您遵循最适合您的应用程序的命名约定。例如,我介绍的一些例子采用的约定是,测试类对其测试方法使用与被测试的类相同的名称。例如,
List.contains()
由
ListTest.contains()
测试,
List.add()
由
ListTest.addAll()
测试,等等。
TestCase
类仍然能够工做,可是您再也不须要扩展它了。只要您用
@Test
来注释测试方法,就能够将测试方法放到任何类中。可是您须要导入
junit.Assert
类以访问各类 assert 方法,以下所示:
import org.junit.Assert;
public class AdditionTest {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
Assert.assertEquals(2, z);
}
} |
您也可使用 JDK 5 中新特性(static import),使得与之前版本同样简单:
import static org.junit.Assert.assertEquals;
public class AdditionTest {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
assertEquals(2, z);
}
} |
这种方法使得测试受保护的方法很是容易,由于测试案例类如今能够扩展包含受保护方法的类了。
JUnit 3 测试运行程序(test runner)会在运行每一个测试以前自动调用
setUp()
方法。该方法通常会初始化字段,打开日志记录,重置环境变量,等等。例如,下面是摘自 XOM 的
XSLTransformTest
中的
setUp()
方法:
protected void setUp() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
} |
在 JUnit 4 中,您仍然能够在每一个测试方法运行以前初始化字段和配置环境。然而,完成这些操做的方法再也不须要叫作
setUp()
,只要用
@Before
注释来指示便可,以下所示:
@Before protected void initialize() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
} |
甚至能够用
@Before
来注释多个方法,这些方法都在每一个测试以前运行:
@Before protected void findTestDataDirectory() {
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
}
@Before protected void redirectStderr() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
} |
清除方法与此相似。在 JUnit 3 中,您使用
tearDown()
方法,该方法相似于我在 XOM 中为消耗大量内存的测试所使用的方法:
protected void tearDown() {
doc = null;
System.gc();
} |
对于 JUnit 4,我能够给它取一个更天然的名称,并用
@After
注释它:
@After protected void disposeDocument() {
doc = null;
System.gc();
} |
与
@Before
同样,也能够用
@After
来注释多个清除方法,这些方法都在每一个测试以后运行。
最后,您再也不须要在超类中显式调用初始化和清除方法,只要它们不被覆盖便可,测试运行程序将根据须要自动为您调用这些方法。超类中的
@Before
方法在子类中的
@Before
方法以前被调用(这反映了构造函数调用的顺序)。
@After
方法以反方向运行:子类中的方法在超类中的方法以前被调用。不然,多个
@Before
或
@After
方法的相对顺序就得不到保证。
JUnit 4 也引入了一个 JUnit 3 中没有的新特性:类范围的
setUp()
和
tearDown()
方法。任何用
@BeforeClass
注释的方法都将在该类中的测试方法运行以前恰好运行一次,而任何用
@AfterClass
注释的方法都将在该类中的全部测试都运行以后恰好运行一次。
例如,假设类中的每一个测试都使用一个数据库链接、一个网络链接、一个很是大的数据结构,或者还有一些对于初始化和事情安排来讲比较昂贵的其余资源。不要在每一个测试以前都从新建立它,您能够建立它一次,并还原它一次。该方法将使得有些测试案例运行起来快得多。例如,当我测试调用第三方库的代码中的错误处理时,我一般喜欢在测试开始以前重定向
System.err
,以便输出不被预期的错误消息打乱。而后我在测试结束后还原它,以下所示:
// This class tests a lot of error conditions, which
// Xalan annoyingly logs to System.err. This hides System.err
// before each test and restores it after each test.
private PrintStream systemErr;
@BeforeClass protected void redirectStderr() {
systemErr = System.err; // Hold on to the original value
System.setErr(new PrintStream(new ByteArrayOutputStream()));
}
@AfterClass protected void tearDown() {
// restore the original value
System.setErr(systemErr);
} |
没有必要在每一个测试以前和以后都这样作。可是必定要当心对待这个特性。它有可能会违反测试的独立性,并引入非预期的混乱。若是一个测试在某种程度上改变了
@BeforeClass
所初始化的一个对象,那么它有可能会影响其余测试的结果。它有可能在测试套件中引入顺序依赖,并隐藏 bug。与任何优化同样,只在剖析和基准测试证实您具备实际的问题以后才实现这一点。这就是说,我看到了不止一个测试套件运行时间如此之长,以致不能像它所须要的那样常常运行,尤为是那些须要创建不少网络和数据库链接的测试。(例如,LimeWire 测试套件运行时间超过两小时。)要加快这些测试套件,以便程序员能够更加常常地运行它们,您能够作的就是减小 bug。
异常测试是 JUnit 4 中的最大改进。旧式的异常测试是在抛出异常的代码中放入
try
块,而后在
try
块的末尾加入一个
fail()
语句。例如,该方法测试被零除抛出一个
ArithmeticException
:
public void testDivisionByZero() {
try {
int n = 2 / 0;
fail("Divided by zero!");
}
catch (ArithmeticException success) {
assertNotNull(success.getMessage());
}
} |
该方法不只难看,并且试图挑战代码覆盖工具,由于无论测试是经过仍是失败,总有一些代码不被执行。在 JUnit 4 中,您如今能够编写抛出异常的代码,并使用注释来声明该异常是预期的:
@Test(expected=ArithmeticException.class)
public void divideByZero() {
int n = 2 / 0;
} |
若是该异常没有抛出(或者抛出了一个不一样的异常),那么测试就将失败。可是若是您想要测试异常的详细消息或其余属性,则仍然须要使用旧式的
try-catch
样式。
也许您有一个测试运行的时间很是地长。不是说这个测试应该运行得更快,而是说它所作的工做从根本上比较复杂或缓慢。须要访问远程网络服务器的测试一般都属于这一类。若是您不在作可能会中断该类测试的事情,那么您可能想要跳过运行时间长的测试方法,以缩短编译-测试-调试周期。或者也许是一个由于超出您的控制范围的缘由而失败的测试。例如,W3C XInclude 测试套件测试 Java 还不支持的一些 Unicode 编码的自动识别。没必要总是被迫盯住那些红色波浪线,这类测试能够被注释为
@Ignore
,以下所示:
// Java doesn't yet support
// the UTF-32BE and UTF32LE encodings
@Ignore public void testUTF32BE()
throws ParsingException, IOException, XIncludeException {
File input = new File(
"data/xinclude/input/UTF32BE.xml"
);
Document doc = builder.build(input);
Document result = XIncluder.resolve(doc);
Document expectedResult = builder.build(
new File(outputDir, "UTF32BE.xml")
);
assertEquals(expectedResult, result);
} |
测试运行程序将不运行这些测试,可是它会指出这些测试被跳过了。例如,当使用文本界面时,会输出一个“I”(表明 ignore),而不是为经过的测试输出所经历的时间,也不是为失败的测试输出“E”:
$ java -classpath .:junit.jar org.junit.runner.JUnitCore
nu.xom.tests.XIncludeTest
JUnit version 4.0rc1
.....I..
Time: 1.149
OK (7 tests) |
可是必定要当心。最初编写这些测试可能有必定的缘由。若是永远忽略这些测试,那么它们指望测试的代码可能会中断,而且这样的中断可能不能被检测到。忽略测试只是一个权宜之计,不是任何问题的真正解决方案。
测试性能是单元测试最为痛苦的方面之一。JUnit 4 没有彻底解决这个问题,可是它对这个问题有所帮助。测试能够用一个超时参数来注释。若是测试运行的时间超过指定的毫秒数,则测试失败。例如,若是测试花费超过半秒时间去查找之前设置的一个文档中的全部元素,那么该测试失败:
@Test(timeout=500) public void retrieveAllElementsInDocument() {
doc.query("//*");
} |
除了简单的基准测试以外,时间测试也对网络测试颇有用。在一个测试试图链接到的远程主机或数据库宕机或变慢时,您能够忽略该测试,以便不阻塞全部其余的测试。好的测试套件执行得足够快,以致程序员能够在每一个测试发生重大变化以后运行这些测试,有可能一天运行几十次。设置一个超时使得这一点更加可行。例如,若是解析 [url]http://www.ibiblio.org/xml[/url] 花费了超过 2 秒,那么下面的测试就会超时:
@Test(timeout=2000)
public void remoteBaseRelativeResolutionWithDirectory()
throws IOException, ParsingException {
builder.build("http://www.ibiblio.org/xml");
} |
JUnit 4 为比较数组添加了两个
assert()
方法:
public static void assertEquals(Object[] expected, Object[] actual)
public static void assertEquals(String message, Object[] expected,
Object[] actual)
|
这两个方法以最直接的方式比较数组:若是数组长度相同,且每一个对应的元素相同,则两个数组相等,不然不相等。数组为空的状况也做了考虑。
JUnit 4 基本上是一个新框架,而不是旧框架的升级版本。JUnit 3 开发人员可能会找到一些原来没有的特性。
最明显的删节就是 GUI 测试运行程序。若是您想在测试经过时看到赏心悦目的绿色波浪线,或者在测试失败时看到使人焦虑的红色波浪线,那么您须要一个具备集成 JUnit 支持的 IDE,好比 Eclipse。无论是 Swing 仍是 AWT 测试运行程序都不会被升级或捆绑到 JUnit 4 中。
下一个惊喜是,失败(assert 方法检测到的预期的错误)与错误(异常指出的非预期的错误)之间再也不有任何差异。尽管 JUnit 3 测试运行程序仍然能够区别这些状况,而 JUnit 4 运行程序将再也不可以区分。
最后,JUnit 4 没有
suite()
方法,这些方法用于从多个测试类构建一个测试套件。相反,可变长参数列表用于容许将不肯定数量的测试传递给测试运行程序。
我对消除了 GUI 测试运行程序并不感到过高兴,可是其余更改彷佛有可能增长 JUnit 的简单性。只要考虑有多少文档和 FAQ 当前专门用于解释这几点,而后考虑对于 JUnit 4,您再也不须要解释这几点了。
当前,尚未 JUnit 4 的库版本。若是您想要体验新的版本,那么您须要从 SourceForge 上的 CVS 知识库获取它。分支(branch)是“Version4”(参见
参考资料)。注意,不少的文档没有升级,仍然是指以旧式的 3.x 方式作事。Java 5 对于编译 JUnit 4 是必需的,由于 JUnit 4 大量用到注释、泛型以及 Java 5 语言级的其余特性。
自 JUnit 3 以来,从命令行运行测试的语法发生了一点变化。您如今使用
org.junit.runner.JUnitCore
类:
$ java -classpath .:junit.jar org.junit.runner.JUnitCore
TestA TestB TestC...
JUnit version 4.0rc1
Time: 0.003
OK (0 tests) |
Beck 和 Gamma 努力维持向前和向后兼容。JUnit 4 测试运行程序能够运行 JUnit 3 测试,不用作任何更改。只要将您想要运行的每一个测试的全限定类名传递给测试运行程序,就像针对 JUnit 4 测试同样。运行程序足够智能,能够分辨出哪一个测试类依赖于哪一个版本的 JUnit,并适当地调用它。
向后兼容要困难一些,可是也能够在 JUnit 3 测试运行程序中运行 JUnit 4 测试。这一点很重要,因此诸如 Eclipse 之类具备集成 JUnit 支持的工具能够处理 JUnit 4,而不须要更新。为了使 JUnit 4 测试能够运行在 JUnit 3 环境中,能够将它们包装在
JUnit4TestAdapter
中。将下面的方法添加到您的 JUnit 4 测试类中应该就足够了:
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(AssertionTest.class);
} |
可是因为 Java 比较多变,因此 JUnit 4 一点都不向后兼容。JUnit 4 彻底依赖于 Java 5 特性。对于 Java 1.4 或更早版本,它将不会编译或运行。
JUnit 4 远没有结束。不少重要的方面没有说起,包括大部分的文档。我不推荐如今就将您的测试套件转换成注释和 JUnit 4。即便如此,开发仍在快速进行,而且 JUnit 4 前景很是看好。尽管 Java 2 程序员在可预见的将来仍然须要使用 JUnit 3.8,可是那些已经转移到 Java 5 的程序员则应该很快考虑使他们的测试套件适合于这个新的框架,以便匹配。