JUnit4 已经在业界运用很长时间了。相对于 JUnit3 ,版本 4 做了很大量的改进。主要是针对使用 annotation使得用户有更多客户化的定制,为了提供更多更灵活的测试手段。早在版本3的时候JUnit就完美地符合了一个单元测试框架的3个要求:
- 每个单元测试必须独立于其他的单元测试
- 每个单元测试中产生的错误必须被记录下来
- 用户能够轻松指定要执行的单元测试
在版本4中间这些优良特性被保留,而且加入了更多的测试灵活性。
JUnit 的代码不算巨大,但其以高密度的设计模式和灵活性使大家对 JUnit 框架评价很高。作为 JUnit3 的后续版本它仍旧延续了高密度的设计模式的风格,而且代码更为精巧。利用 Eclipse 的插件CodePro Analytix 来测量 JUnit4 的结果是:抽象类和接口的比例超过 22% ,平均每个方法的大小约为 5 行。
已经有很多文章描述如何使用 JUnit4 的新特性。此文不打算介绍这些新特性,只是分析和介绍 JUnit4 的设计框架,其版本为 4.8.2 。希望读者对JUnit至少有初步的理解,这样才能比较容易理解这篇文章。
JUnit4的输入输出:
作为一个程序,首先要知道它的输入和输出是什么。
1. 输入:JUnit4的输入是待测的类。这里需要说明的是这里的输入是类的Class对象,而非一个java对象。因为程序的入口是 JUnitCore. runClasses(Class<?>…) ,也就是说参数是个Class对象,而非这个待测的类的实例。
2. 输出:测试的结果,包括成功和失败的case的原因,以及测试花费的时间等。
JUni4的总体结构:
总的来说可以分为2大步骤:
1. 首先对输入的Class利用 org.junit.runners.model.RunnerBuilder 进行分类解析,把它组织成JUnit4内部对象 org.junit.runner.Runner 。
2. 然后调 Runner.run( RunNotifier ) 来执行testcase,测试过程中的事件由 RunNotifier 来处理。
组织Runner:
JUnit4使用了Builder模式来生成 Runner 。在这个过程里面, RunnerBuilder 的子类扮演具体Builder角色。一种 RunnerBuilder 负责一种 Runner 的生成,而每个 Runner 代表一个待测类。
这个需要指出的是JUnit4利用 AllDefaultPossibilitie sBuilder 类作为Director的角色,它本身就是一个 RunnerBuilder ,它通过对不同的class的特征来分配对应的 RunnerBui lder 。
下面是不同特征的待测class和对应的 RunnerBuilder ,以及其生成的 Runner 的表:
JUnit4的输入输出:
作为一个程序,首先要知道它的输入和输出是什么。
1. 输入:JUnit4的输入是待测的类。这里需要说明的是这里的输入是类的Class对象,而非一个java对象。因为程序的入口是 JUnitCore. runClasses(Class<?>…) ,也就是说参数是个Class对象,而非这个待测的类的实例。
2. 输出:测试的结果,包括成功和失败的case的原因,以及测试花费的时间等。
JUni4的总体结构:
总的来说可以分为2大步骤:
1. 首先对输入的Class利用 org.junit.runners.model.RunnerBuilder 进行分类解析,把它组织成JUnit4内部对象 org.junit.runner.Runner 。
2. 然后调 Runner.run( RunNotifier ) 来执行testcase,测试过程中的事件由 RunNotifier 来处理。
组织Runner:
JUnit4使用了Builder模式来生成 Runner 。在这个过程里面, RunnerBuilder 的子类扮演具体Builder角色。一种 RunnerBuilder 负责一种 Runner 的生成,而每个 Runner 代表一个待测类。
这个需要指出的是JUnit4利用 AllDefaultPossibilitie sBuilder 类作为Director的角色,它本身就是一个 RunnerBuilder ,它通过对不同的class的特征来分配对应的 RunnerBui lder 。
下面是不同特征的待测class和对应的 RunnerBuilder ,以及其生成的 Runner 的表:
Runner的继承结构:
最后所有生成的Runner都被封装到 org.junit. runners.Suite , Suite 还是使用了Composite模式,它本身就是一个 Runner ,并且内部包含了 Runner 集合。也就是说 Suite 里面可以嵌套 Suite 和其他的 Runner 。
需要特别说明的是 AnnotatedBuilder ,它是处理带有 @RunWith 的待测类,为其生成 Runner 。这提供了客户订制 Runner 的强大功能。有时你需要做一些特别的功能你就可以继承 Runner ,然后在待测类上标注 @RunWith(MyRunner. class) 。这个时候 AnnotatedBuilder 会通过反射生成 MyRunne r的实例。你在定义自己的 Runner 的时候就可以对测试的行为“为所欲为”,这个真的很酷。
JUnit4还带了 Categories , Enclosed 和 Parameterized 这些 Runner ,它们都实现了很强大的功能。
说了这么多不如举个例子来说明,对于程序员来说有什么比用代码说明更清楚的:)
假设我们的待测类有JUnit3的类 Version3Test , JUnit4的类 Version4Test ,Ignore的类 IgnoreTest ,以及含有静态 suite() 方法的类 SuiteTest 。我们把 Version3Test 和 Version3Test 放到 Suite1 。 然后为了展示递归结构把 Suite1 再放入 Suite2 ,顺便把 IgnoreTest 和 SuiteTest 也放入 Suit e2 。
代码如下:
- public class Version3Test extends TestCase{
- public void testIn3X(){}
- }
- public class Version4Test {
- @Test public void testIn4X(){}
- }
- @RunWith(Suite.class)
- @Suite.SuiteClasses({
- Version3Test.class, Version4Test.class
- })
- public class Suite1 {}
- @Ignore
- public class IgnoreTest {
- public void testInIgnore(){}
- }
- public class SuiteTest {
- public static Test suite(){ return new TestSuite(); }
- }
- @RunWith(Suite.class)
- @Suite.SuiteClasses({//Put all together
- Suite1.class, IgnoreTest.class, SuiteTest.class
- }) public class Suite2 {}
如果是在Eclipse下面运行 Suite2 ,你就可以看到这样的结构:
各种Runner的说明:
- JUnit38ClassRunner:它的run方法其实就是把3版本的运行机制搬到4版本。你可以在代码里面看到Test.run(TestResult) 这样的代码。
- SuiteMethod:它继承于JUnit38ClassRunner 。这也是个巧妙的设计:SuiteMethod会在构造方法里面利用反射执行静态suite() 方法。suite() 方法的返回值就是Test 对象,这样就把问题变为版本3的测试问题。
- IgnoredClassRunner:这几乎不用说了。既然希望忽略的测试,那么这个Runner的run就是没有执行测试内容。
- Suite:它是Composite结构的根,它包含了所有子Runner的集合。
- BlockJUnit4ClassRunner:这是全新的设计。对于提供了丰富订制化的测试来说,原先junit.framework.TestCase.runBare() 方法里面的Template模式已经完全不够用了。因此在BlockJUnit4ClassRunner里面引入了Statement 类的概念,实际上这是个Decorator模式。Statement里面只有一个方法evaluate() 。一个方法evaluate() 代表一个操作:譬如@Before 是一个Statement ,@BeforeClass 也是一个Statement ,@Tes t里面定义的expected 和timeout 都是Statement 。当然被测试的方法本身也是一个Statement 。一个典型的带有@Before ,@After 以及@Test(expected=Exception.class, timeout=X) 的 测试方法最终组成的Statement装饰链表结构就是:RunAfters -> RunBefores -> FailOnTimeout -> ExpectedException -> InvokeMethod。
自己写一个Runner:
接下来做点好玩的东西:自己写一个Runner。在正常情况下,testcase的测试方法的执行顺序就是定义测试方法的顺序。现在我们自己写一个按照测试方法名字升序执行的Runner。在
BlockJUnit4ClassRunner 里面它其实会支持dummy的sort功能,如果我们重新实现这个sort就可以很便捷地自定义排序行为。 虽然这有悖单元测试互相独立的原则,for fun:)
- /**
- * The Runner provides sort functionality by lexicographic test method name.
- *
- * @author 卢声远<michaellufhl@yahoo.com.cn>
- */
- public class SortRunner extends BlockJUnit4ClassRunner {
- /**
- * Create a Sorter.
- * @param klass
- * @throws InitializationError
- */
- public SortRunner(Class<?> klass) throws InitializationError {
- super(klass);
- Sorter sorter = new Sorter(new Comparator<Description>() {
- public int compare(Description o1, Description o2) {
- return o1.getMethodName().compareTo(o2.getMethodName());
- }});
- sort(sorter);
- }
- }
- @RunWith(SortRunner.class)
- public class Version4Test {
- @Test public void c(){}
- @Test public void a(){}
- @Test public void b(){}
- }
当执行结束后,你会发现执行顺序是a->b->c,而不是默认的c->b->a。
关于@Rule:
为了增强订制行为的高灵活度,JUnit4提供了
@Rule 机制。Rule变量必须实现
org.junit.rules.MethodRule 接口,用户可以通过自定义@Rule来做些类似Interceptor的操作。而且JUnit4同时支持多个
MethodRule 。MethodRule接口只有一个方法
Statement apply(Statement base, FrameworkMethod method, Object target) 。参数base就是已经生成的Statement,通过apply方法用户可以修饰已有的Statement base。@Rule机制是如此强大以至于JUnit4已经打算在后续版本把ExpectingException,timeout,Before和After的特性用Rule机制来实现。
下面继续做点有趣的事情:定义一个可以订制执行次数的Rule:
- @Retention(RetentionPolicy.RUNTIME)
- @interface RunCount {
- int count() default 1;
- }
- /**
- * Define a repeatable Rule.
- * The repeat times is defined in annotation RunCount(count=X).
- *
- * @author 卢声远<michaellufhl@yahoo.com.cn>
- */
- class RepeatRule implements MethodRule {
- public final Statement apply(final Statement base,
- final FrameworkMethod method, Object target) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- int count =1;//Default
- RunCount rCount= method.getAnnotation(RunCount.class) ;
- if (rCount != null)
- count=rCount.count();
- while(count–>0){
- base.evaluate();
- }
- }
- };
- }
- }
- public class RepeatTest {
- @Rule
- public RepeatRule rule = new RepeatRule();
- @Test
- @RunCount(count=2)//Run twice
- public void testIt(){}
- }
你可以看到testIt()执行了2次,而且参数Statement base其实已经是包含了 @Before 和 @After 等修饰的Statement。
最后:
JUnit4 的结构相比 3 版本变化是巨大的。代码非常精致,但同时代码也相当复杂,充满 callback 机制和大量的内部类。还有些比较 cool 的特性的代码在包 org.junit.experimental 下面。譬如 里面有各种各样特性的 Runner ,有兴趣大家可以看一下。源代码可以到: https://github.com/downloads/KentBeck/junit/junit-4.8.2.jar 下载。
参考资料:
- JUnit A Cook’s Tour
- JUnit 4 in 60 Seconds
- JUnit in Action : Vincent Massol, Ted Husted
作者:卢声远<michaellufhl@yahoo.com.cn>