Techniques for building resilient, relocatable, multithreaded JUnit tests
一项灵活的、可重定位的多线程JUnit测试技术
作者 Andy Schneider
译者 雷云飞 javawebstart Barret gstian [AKA]
校对 gstian [AKA]
Summary
摘要
Extreme Programming's rise in popularity among the Java community has prompted more development teams to use JUnit: a simple test framework for building and executing unit tests. Like any toolkit, JUnit can be used effectively and ineffectively. In this article, Andy Schneider discusses good and bad ways to use JUnit and provides practical recommendations for its use by development teams. In addition, he explains simple mechanisms to support:
Java社区里面流行的编程热的不断升温使越来越多的开发团队使用 JUnit进行测试。JUnit 是一种构造和进行单元测试的简便的测试框架。就象所有的工具包一样,JUnit 可以被高效的使用,也可以被低效的使用。在这篇文章种,Andy Schneider讨论了JUnit 的高效和低效的使用方法,并且为开发团队提供了实用的JUnit使用建议。另外,他提供了几种简单的机制来解释两种方法的差别:
Automatic construction of composite tests
组合测试的自动构件
Multithreaded test cases
多线程测试用例
This article assumes some familiarity with JUnit. (4,000 words)
阅读本篇文章,需要您对JUnit略知一二。
JUnit is a typical toolkit: if used with care and with recognition of its idiosyncrasies, JUnit will help to develop good, robust tests. Used blindly, it may produce a pile of spaghetti instead of a test suite. This article presents some guidelines that can help you avoid the pasta nightmare. The guidelines sometimes contradict themselves and each other -- this is deliberate. In my experience, there are rarely hard and fast rules in development, and guidelines that claim to be are misleading.
JUnit是一个有特色的工具包:熟知它的特性的情况下并细心的使用,它在你开发优良的健壮的测试上市有帮助的。如果被盲目的使用,它可能就像一堆意大利面条,而不是测试集。本文给出了一些可以帮助你避免这些生面团恶梦的指导方针。这些指导方针有时看起来会相互矛盾----这是故意的。以我的经验,在开发中很少有硬性而方便的规则。任何声称是这种规则的指导方针都是误导。
We'll also closely examine two useful additions to the developer's toolkit:
我们同时还将深入检查开发者的工具包里的两个有用的附加物:
A mechanism for automatically creating test suites from classfiles in part of a filesystem
一种可以从部分文件系统里面自动创建测试集的机制
A new TestCase that better supports tests in multiple threads
一种更好支持多线程的新测试用例。
When faced with unit testing, many teams end up producing some kind of testing framework. JUnit, available as open source, eliminates this onerous task by providing a ready-made framework for unit testing. JUnit, best used as an integral part of a development testing regime, provides a mechanism that developers can use to consistently write and execute tests. So, what are the JUnit best practices?
当面对单元测试时,许多团队都会自己去完成某种测试框架。JUnit做为一种开放软件,通过为单元测试提供一种现成的测试框架,来消除这种繁重的任务。JUnit作为一个开发测试体制整体中的一部分给开发者提供了一种可以一致地编写和执行测试的机制。既然如此,那么,什么是JUnit的最佳实践?
Do not use the test-case constructor to set up a test case
不要使用测试用例构造器来创建一个测试用例
Setting up a test case in the constructor is not a good idea. Consider:
使用构造器来建立一个测试用例并不是个好主意,例如:
public class SomeTest extends TestCase
public SomeTest (String testName) {
super (testName);
// Perform test set-up
}
}
Imagine that while performing the setup, the setup code throws an IllegalStateException. In response, JUnit would throw an AssertionFailedError, indicating that the test case could not be instantiated. Here is an example of the resulting stack trace:
想象一下当执行安装时,代码抛出一个IllegalStateException异常。做为回应,JUnit也会抛出一个AssertionFailedError异常来指示测试用例无法实例化。下面是一个堆栈跟踪结果示例:
junit.framework.AssertionFailedError: Cannot instantiate test case: test1 at
junit.framework.Assert.fail(Assert.java:143) at
junit.framework.TestSuite$1.runTest(TestSuite.java:178) at
junit.framework.TestCase.runBare(TestCase.java:129) at
junit.framework.TestResult$1.protect(TestResult.java:100) at
junit.framework.TestResult.runProtected(TestResult.java:117) at
junit.framework.TestResult.run(TestResult.java:103) at
junit.framework.TestCase.run(TestCase.java:120) at
junit.framework.TestSuite.run(TestSuite.java, Compiled Code) at
junit.ui.TestRunner$12.run(TestRunner.java:429)
This stack trace proves rather uninformative; it only indicates that the test case could not be instantiated. It doesn't detail the original error's location or place of origin. This lack of information makes it hard to deduce the exception's underlying cause.
这个堆栈跟踪没有提供多少有价值的信息。它只是表明测试用例不能被实例化。它并没有初始化时产生错误的错误位置和错误来源的详细信息。信息的缺乏使得推断该异常出现的原因变得困难。
Instead of setting up the data in the constructor, perform test setup by overriding setUp(). Any exception thrown within setUp() is reported correctly. Compare this stack trace with the previous example:
放弃在构造器中创建数据,通过重载setUp()来执行测试创建,。任何在setUp()中产生的异常都会被准确的报告。与前一个例子对照,比较下面的堆栈跟踪:
java.lang.IllegalStateException: Oops at bp.DTC.setUp(DTC.java:34) at
junit.framework.TestCase.runBare(TestCase.java:127) at
junit.framework.TestResult$1.protect(TestResult.java:100) at
junit.framework.TestResult.runProtected(TestResult.java:117) at
junit.framework.TestResult.run(TestResult.java:103)
...
This stack trace is much more informative; it shows which exception was thrown (IllegalStateException) and from where. That makes it far easier to explain the test setup's failure.
这个堆栈跟踪含有更多的信息量。它表明了异常类型(IllegalStateException), 以及产生位置。这使得可以更容易解释为何测试建立失败。
Don't assume the order in which tests within a test case run
不要推测一个测试用例运行中各测试的执行顺序
You should not assume that tests will be called in any particular order. Consider the following code segment:
你不应该认为各测试用例会按照任何特定顺序被调用。考虑下面的代码片断:
public class SomeTestCase extends TestCase {
public SomeTestCase (String testName) {
super (testName);
}
public void testDoThisFirst () {
...
}
public void testDoThisSecond () {
}
}
In this example, it is not certain that JUnit will run these tests in any specific order when using reflection. Running the tests on different platforms and Java VMs may therefore yield different results, unless your tests are designed to run in any order. Avoiding temporal coupling will make the test case more robust, since changes in the order will not affect other tests. If the tests are coupled, the errors that result from a minor update may prove difficult to find.
在这个例子中,当使用映射时,JUnit将按照何种顺序执行这些测试并不能确定。在不同的平台及Java VM上,可能产生不同的结果,除非你的测试被事先设计为按某种顺序执行。由于执行顺序的改变不会影响其它测试,避免这种短暂的耦合使得你的测试用例更加健壮。如果测试耦合在一起,由于一个小变动引起的错误也许会难于发现。
In situations where ordering tests makes sense -- when it is more efficient for tests to operate on some shared data that establish a fresh state as each test runs -- use a static suite() method like this one to ensure the ordering:
在某些情况下,测试的顺序还是有意义的----例如,测试们可以使用一些共享数据时来提高效率。这些共享数据对于每个测试运行时都会建立一个新的状态。----可以使用一个静态的 suite() 方法来保证执行顺序,如下:
public static Test suite() {
suite.addTest(new SomeTestCase ("testDoThisFirst");
suite.addTest(new SomeTestCase ("testDoThisSecond");
return suite;
}
Test cases that have side effects exhibit two problems:
带有副作用的测试用例会出现下面两个问题:
They can affect data that other test cases rely upon
它们会影响其他测试用例所依赖的数据
You cannot repeat tests without manual intervention
你不能在没有手工干预的情况下重复测试
In the first situation, the individual test case may operate correctly. However, if incorporated into a TestSuite that runs every test case on the system, it may cause other test cases to fail. That failure mode can be difficult to diagnose, and the error may be located far from the test failure.
在第一种情况下,独立的测试用例也许可以正确的执行,然而,当它们被置入一个执行 该系统中所有测试的测试集时,可能导致其他测试用例失败。但这种失败的做法很难 诊断出来,错误也许离失败的地方很远。
In the second situation, a test case may have updated some system state so that it cannot run again without manual intervention, which may consist of deleting test data from the database (for example). Think carefully before introducing manual intervention. First, the manual intervention will need to be documented. Second, the tests could no longer be run in an unattended mode, removing your ability to run tests overnight or as part of some automated periodic test run.
在第二种情况下,一个测试用例可能运行后更改了系统状态,以至于它不能在没有手工干预 的情况下被再次执行。例如,这有可能是从数据库中删除了测试数据造成的。在手工 干预之前,仔细的考虑下面两点:首先,手工干预应该被记录在文档当中,其次,这种测试不 能在无人监控的情况下被执行,应该去掉它们通宵执行测试或者作为自动运行的周期性测试的一部分的能力.
Call a superclass's setUp() and tearDown() methods when subclassing
子类化的时候,调用父类的 setUp() 方法和 tearDown() 方法
When you consider:
考虑如下情况:
public class SomeTestCase extends AnotherTestCase {
// A connection to a database
private Database theDatabase;
public SomeTestCase (String testName) {
super (testName);
}
public void testFeatureX () {
...
}
public void setUp () {
// Clear out the database
theDatabase.clear ();
}
}
Can you spot the deliberate mistake? setUp() should call super.setUp() to ensure that the environment defined in AnotherTestCase initializes. Of course, there are exceptions: if you design the base class to work with arbitrary test data, there won't be a problem.
你能发现隐藏其中的那个需要深思的错误吗?setUp()方法应该调用父类的setUp()方法以保证能够初始化在父类AnotherTestCase 中定义的测试环境。当然,这也并不是绝对的--如果父类设计成可以通用的基类的话,那么,以上就不是一个问题。
Do not load data from hard-coded locations on a filesystem
不要从文件系统里那些代码已固定的位置加载数据
Tests often need to load data from some location in the filesystem. Consider the following:
测试经常要从文件系统中读入数据,如下:
public void setUp () {
FileInputStream inp ("C://TestData//dataSet1.dat");
...
}
The code above relies on the data set being in the C:/TestData path. That assumption is incorrect in two situations:
上面这段代码依赖于C:/TestDate中的数据。在下面2种情况下,上面的假设会出现如下问题:
A tester does not have room to store the test data on C: and stores it on another disk
可能在C盘没有足够的空间存储测试数据,而把它存在其它的磁盘上
The tests run on another platform, such as Unix
测试案例可能运行在另外的平台上,比如Unix
One solution might be:
以下是一种解决方案:
public void setUp () {
FileInputStream inp ("dataSet1.dat");
...
}
However, that solution depends on the test running from the same directory as the test data. If several different test cases assume this, it is difficult to integrate them into one test suite without continually changing the current directory.
但是,上面的解决方案是把测试数据放在运行测试案例的目录中,如果把这样的几个测试案例集成起来作为测试集来运行的话,只有测试集在运行过程中不断的改变当前目录才行。
To solve the problem, access the dataset using either Class.getResource() or Class.getResourceAsStream(). Using them, however, means that resources load from a location relative to the class's origin.
要解决这个问题,可以使用Class.getResource()或者Class.getResourceAsStream()这种访问资源的形式来访问数据,它们都是从类的相对路径来访问资源的。
Test data should, if possible, be stored with the source code in a configuration management (CM) system. However, if you're using the aforementioned resource mechanism, you'll need to write a script that moves all the test data from the CM system into the classpath of the system under test. A less ungainly approach is to store the test data in the source tree along with the source files. With this approach, you need a location-independent mechanism to locate the test data within the source tree. One such mechanism is a class. If a class can be mapped to a specific source directory, you could write code like this:
如果可能的话,测试数据应该和源程序一起存放到配置管理系统中。如果使用前面提到的访问资源的形式,就要自己写脚本来把所有的测试数据从配置管理系统中取出来加入要测试案例的classpath中。还有一种方法是把测试数据和源程序存放在一起,使用和位置无关的方法来查找测试数据。以类为例,如果类能够被映射到一个特定的目录,相应的代码可以如下:
InputStream inp = SourceResourceLoader.getResourceAsStream (this.getClass (), "dataSet1.dat");
Now you must only determine how to map from a class to the directory that contains the relevant source file. You can identify the root of the source tree (assuming it has a single root) by a system property. The class's package name can then identify the directory where the source file lies. The resource loads from that directory. For Unix and NT, the mapping is straightforward: replace every instance of '.' with File.separatorChar.
现在要考虑的是怎么把一个类映射到包含相应源文件的目录。可以通过系统属性来设置源文件根目录。类的包名可以标志源文件的存放位置。在Unix和NT上,这种映射是直接的:把'.'替换成为File.separatorChar就可以了。
Keep tests in the same location as the source code
把测试案例和源文件放在一起
If the test source is kept in the same location as the tested classes, both test and class will compile during a build. This forces you to keep the tests and classes synchronized during development. Indeed, unit tests not considered part of the normal build quickly become dated and useless.
如果测试案例和要测试的代码放在一起,那么可以同时对这两者编译。这样可以保证在开发过程中测试和代码保持同步。实际上,不在正常的版本中的单元测试马上会变的过时、无用。
Name tests properly
正确命名测试案例
Name the test case TestClassUnderTest. For example, the test case for the class MessageLog should be TestMessageLog. That makes it simple to work out what class a test case tests. Test methods' names within the test case should describe what they test:
给测试案例起名TestClassUnderTest。例如,类MessageLog的测试案例的名字应该是TestMessageLog。这样可以很容易的看出是对哪个类进行测试。同时,测试案例的方法应该清楚的表明要测什么:
testLoggingEmptyMessage()
testLoggingNullMessage()
testLoggingWarningMessage()
testLoggingErrorMessage()
Proper naming helps code readers understand each test's purpose.
正确的命名可以帮助别人理解每个测试的目的。
Ensure that tests are time-independent
保证测试是和时间无关的
Where possible, avoid using data that may expire; such data should be either manually or programmatically refreshed. It is often simpler to instrument the class under test, with a mechanism for changing its notion of today. The test can then operate in a time-independent manner without having to refresh the data.
只要可能,避免使用可能过期的数据;这样的数据要么手工,要么由程序来刷新。在测试下建立一个类经常需要更简化些,要用一种可以与现在的思想保持同步的机制.这样,测试案例就可以和时间无关,不需要刷新数据。
Consider locale when writing tests
写测试时考虑地址的影响
Consider a test that uses dates. One approach to creating dates would be:
考虑使用日期的一个测试案例。一种创建日期的方法:
Date date = DateFormat.getInstance ().parse ("dd/mm/yyyy");
There is no guarantee in the JUnit API documentation as to the order your tests will be called in, because JUnit employs a Vector to store tests. However, you can expect the above tests to be executed in the order they were added to the test suite.
在JUnit API 文档中并没有保证你的测试被调用的顺序,因为JUnit使用V一个区段来存放测试。 然而,你可以保证上面的测试按照它们被加入测试集的顺序被执行。
Avoid writing test cases with side effects
避免写带有副作用的测试用例
Unfortunately, that code doesn't work on a machine with a different locale. Therefore, it would be far better to write:
不幸的是,这段代码在不同的机器上不能正常的运行。因此,可以换用下面较好的方式:
Calendar cal = Calendar.getInstance ();
Cal.set (yyyy, mm-1, dd);
Date date = Calendar.getTime ();
The second approach is far more resilient to locale changes.
第二中方法可以更灵活的适应地址的改变。
Utilize JUnit's assert/fail methods and exception handling for clean test code
利用JUnit's的assert/fail方法和异常机制创建干净的代码
Many JUnit novices make the mistake of generating elaborate try and catch blocks to catch unexpected exceptions and flag a test failure. Here is a trivial example of this:
许多初学者可能会精心设计一些异常捕捉来捕捉异常,并标志测试出现错误。如下:
public void exampleTest () {
try {
// do some test
} catch (SomeApplicationException e) {
fail ("Caught SomeApplicationException exception");
}
}
JUnit automatically catches exceptions. It considers uncaught exceptions to be errors, which means the above example has redundant code in it.
JUnit可以自动的捕捉异常,把没有截获的异常看作错误,所以,上面的代码有冗余代码。
Here's a far simpler way to achieve the same result:
以下以一种更简洁的方式实现上面的例子:
public void exampleTest () throws SomeApplicationException {
// do some test
}
In this example, the redundant code has been removed, making the test easier to read and maintain (since there is less code).
在此,除去了冗余的代码,使得测试易读易维护(因为代码很少)。
Use the wide variety of assert methods to express your intention in a simpler fashion. Instead of writing:
使用广泛的多样性的有效方法来表达你的意图。不应该:
assert (creds == 3);
Write:
而是:
assertEquals ("The number of credentials should be 3", 3, creds);
The above example is much more useful to a code reader. And if the assertion fails, it provides the tester with more information. JUnit also supports floating point comparisons:
上面的代码让人很容易的读懂,并且即使上面的维护失败,可以提供给测试者更多的信息。JUnit同样支持浮点数的比较:
assertEquals ("some message", result, expected, delta);
When you compare floating point numbers, this useful function saves you from repeatedly writing code to compute the difference between the result and the expected value.
当比较浮点类型的数据时,可以不必再写同样功能的代码。
Use assertSame() to test for two references that point to the same object. Use assertEquals() to test for two objects that are equal.
要测试两个引用是否指向同一个对象,使用assertSame()方法;要测试两个对象是否相等,使用assertEquals()方法
Document tests in javadoc
javadoc下的文档测试
Test plans documented in a word processor tend to be error-prone and tedious to create. Also, word-processor-based documentation must be kept synchronized with the unit tests, adding another layer of complexity to the process. If possible, a better solution would be to include the test plans in the tests' javadoc, ensuring that all test plan data reside in one place.
在一个字处理器里创建需要归档的测试计划易于出现错误且单调乏味。另外,基于字处理器的文件必须与单元测试保持同步,这给处理过程增加了额外一层的复杂性。如果可能,更好的解决方法是将测试计划包括在测试的javadoc,确保所有的测试计划数据保存在一个地方。
Avoid visual inspection
避免目视检查
Testing servlets, user interfaces, and other systems that produce complex output is often left to visual inspection. Visual inspection -- a human inspecting output data for errors -- requires patience, the ability to process large quantities of information, and great attention to detail: attributes not often found in the average human being. Below are some basic techniques that will help reduce the visual inspection component of your test cycle.
测试servlets,用户界面,和其他产生复杂输出的系统通常采用目视检查。目视检查--一个人为了发现错误检查输出的数据--需要耐心,处理大量信息的能力,以及对细节的洞察力:这些通常不会是一般人身上所具备的。以下是一些基本的技术可以用来帮助减少你的测试周期中目视检查。
Swing
Swing
When testing a Swing-based UI, you can write tests to ensure that:
当测试一个基于Swing的用户界面时,你可以写一些测试以保证:
All the components reside in the correct panels
所有的构件都在适当的面板里
You've configured the layout managers correctly
确保正确配置了版式管理器
Text widgets have the correct fonts
Text widgets(文字集)里有正确的字体
A more thorough treatment of this can be found in the worked example of testing a GUI, referenced in the Resources section.
这方面更详细的处理方式可以在一个测试某GUI的成功例子中找到,参考资料章节。
XML
XML
When testing classes that process XML, it pays to write a routine that compares two XML DOMs for equality. You can then programmatically define the correct DOM in advance and compare it with the actual output from your processing methods.
当测试处理XML的类时,写一个程序比较两个XML DOM是否相等。这样你可以预先精确地定义正确的DOM并且与使用你的处理方法得出的实际结果相比较。