这是一篇翻译文章,源链接 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<T> extends Runner implements Filterable, Sortable {
protected abstract List<T> getChildren();
protected abstract Description describeChild(T child);
protected abstract void runChild(T child, RunNotifier notifier);
}
getChildren()
方法里,子类需要返回一个泛型T的列表。然后ParentRunner
让子类为每个child创建一个Description
(方法describeChild
),最后运行每个child(runChild()
)
现在我们来看两个标准的ParentRunner
s
BlockJUnit4ClassRunner
和Suite
BlockJUnit4ClassRunner
是默认的Runner,所以这个Runner经常用来运行单个测试类。如果你看了BlockJUnit4ClassRunner
源码你将会注意到:
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
@Override
protected List<FrameworkMethod> getChildren() {
// 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 {}
当你选择Suite
Runner 配合@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 GitHub
- Spring’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 GitHub
- Mockito provides MockitoJUnitRunner for automatic mock initialization. Source on GitHub
- Oleaster’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);
}
}
}