这是一篇翻译文章,源链接 https://www.mscharhag.com/java/understanding-junits-runner-architecture 2014.8.15 更新(2020) 这个博文描述的是JUnit 4 runner如何工作和你怎么创建你自己的Junit 4runner。请注意JUnit 5已经发布了好几年了(2017年已经发布)。如果你还在使用JUnit 4,也许你应该考虑将项目升级到JUnit 5,如果你对JUnit 5 感兴趣,可以看我这篇博文:creating custom extensions for JUnit 5.
几周前我开始创建一个小小的JUnit Runner。我习得创建一个自定义JUnit Runners是非常简单的。这篇文章里,我将展示JUnit 内部是怎么工作的,如何自定义Runners来修改JUnit的测试执行。
所以,什么是JUnit Runner?
一个JUnit Runner是一个继承了JUnit的抽象类Runner的类.Runners是用来运行测试类的。可以通过@RunWith注解来指定一个Runner去运行测试。
@RunWith(MyTestRunner.class)
public class MyTestClass {
@Test
public void myTest() {
..
}
}
JUnit测试是通过使用类JUnitCore开始的。可以通过命令行或者使用它的其中一个run()方法来运行测试(IDE的run按钮运行测试就是通过此方法实现的)
JUnitCore.runClasses(MyTestClass.class);
然后JUnitCore使用反射为传入的测试类找到一个合适的Runner。一个步骤便是测试类的@RunWith注解。如果没有找到注解将会用默认runner(BlockJUnit4ClassRunner) .这个Runner将会实例化并且测试类会传递给这个Runner。接下来的工作便是Runner初始化、运行传入的测试类。
JUnit Runners如何工作?
我们来看一张标准的JUnit Runners继承图 Runner是一个非常简单的类,它实现了Describale 接口,它(Runner)有两个抽象方法:
public abstract class Runner implements Describable {
public abstract Description getDescription();
public abstract void run(RunNotifier notifier);
}
方法getDescription是继承自Describable,并且返回了一个Description(源码), Description里是包含的是未来将会被导出、被各种工具使用的信息。例如,你的IDE会用这个信息来展示测试结果。 run()是一个非常通用的方法,它runs一些东西(比如一个测试类或者测试suite)。我想通常你不会去继承Runner这个类(因为它太抽象了*【译者注:generous,范围太大不好继承,太抽象】*)
ParentRunner里有了好转,变得具体了一些。ParentRunner是那些有多个叶子的Runners的抽象基类。理解这一点很重要,测试被结构化并且已一个层次顺序执行(想下树)。 比如,你有个test suite,它包含了其他test suites.这些test suites也许会包含多个测试类。并且最后每个测试类包含多个测试方法。 ParentRunner有以下三个抽象方法:
public abstract class ParentRunner
protected abstract List
protected abstract Description describeChild(T child);
protected abstract void runChild(T child, RunNotifier notifier);
}
getChildren()方法里,子类需要返回一个泛型T的列表。然后ParentRunner让子类为每个child创建一个Description(方法describeChild),最后运行每个child(runChild()) 现在我们来看两个标准的ParentRunners BlockJUnit4ClassRunner和Suite BlockJUnit4ClassRunner是默认的Runner,所以这个Runner经常用来运行单个测试类。如果你看了BlockJUnit4ClassRunner源码你将会注意到:
public class BlockJUnit4ClassRunner extends ParentRunner
@Override
protected List
// scan test class for methonds annotated with @Test
}
@Override
protected Description describeChild(FrameworkMethod method) {
// create Description based on method name
}
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
if (/* method not annotated with @Ignore */) {
// run methods annotated with @Before
// run test method
// run methods annotated with @After
}
}
}
当然这应被极大简化了(【译者注:就需要简化来帮助我们理清主干】),但是它展示了BlockJUnit4ClassRunner的核心工作。 泛型参数FrameworkMethod基本上是对java.lang.reflect.Method包装,提供了一些方便的方法。在函数getChildren()中,测试类通过反射被扫面来勋在注解了@Test的方法。被找到的方法将会被包裹进FrameworkMethod对象中然后返回。方法describeChildren()以方法名创建了一个Description,方法runChild()最终运行测试函数。BlockJUnit4ClassRunner内部使用了许多protected方法。取决于你想做什么,你可以检查BlockJUnit4ClassRunner的方法来覆盖重写。可以在GitHhub上看一下源码BlockJUnit4ClassRunner on GitHub.
Suite Runner被用来创建test suites。Suites是测试的集合(或者其他suites),一个简单的suite定义如下:
@RunWith(Suite.class)
@Suite.SuiteClasses({
MyJUnitTestClass1.class,
MyJUnitTestClass2.class,
MyOtherTestSuite.class
})
public class MyTestSuite {}
当你选择SuiteRunner 配合@RunWith注解时,一个test suite就被创建 了。如果你看了Suite的实现,你会发现它很简单。它只做一个件事:用@SuiteClasses注解里定义的类来创建Runner实例。所以getChildren()返回一系列Runners和runChild委派给对应runner的去执行。
自定义JUnit runners的例子
以上面提供的信息,创建一个你自己的JUnit Runner应该不难(至少我希望如此)。如果你想看一些自定义Runner实现的例子,以下有些参考:
Fabio Strozzi created a very simple and straightforward GuiceJUnitRunner project. It gives you the option to inject Guice components in JUnit tests. Source on GitHubSpring’s SpringJUnit4ClassRunner helps you test Spring framework applications. It allows you to use dependency injection in test classes or to create transactional test methods. Source on GitHubMockito provides MockitoJUnitRunner for automatic mock initialization. Source on GitHubOleaster’s Java 8 Jasmine runner. Source on GitHub (shameless self promotion)
总结
JUnit Runners是高度可自定义的,留给了你修改测试执行过程的空间。修改这个测试流程、集成到IDE、构建服务器这是非常酷的
如果你想做些小的改动,最好看一下BlockJUnit4Class runner的protected方法。你应该能找到一个正确的可覆盖的方法。
如果你对Olaester感兴趣,可以看我这篇博文 An alternative approach of writing JUnit tests.
译者自己的一个Runner
public class BlockedParameterized extends Parameterized {
/**
* Only called reflectively. Do not use programmatically.
*
* @param klass
*/
private Class clazz;
public BlockedParameterized(Class> klass) throws Throwable {
super(klass);
this.clazz = klass;
}
@Override
protected void runChild(Runner runner, final RunNotifier notifier) {
synchronized (clazz){
runner.run(notifier);
}
}
}