理解JUnit的Runner架构
几星期前,我开始创建一个小型的JUnit Runner(Oleaster),它允许你在JUnit中用Jasmine的方式写单元测试用例。从中我学习到写单元测试用例其实很简单。在这篇博客中,我将向你展示JUnit Runners内部是怎么工作的,还有你可以如何自定义Runner来更改JUnit的测试用例执行过程。
什么是JUnit的Runner
JUnit Runner是一个继承了JUnit抽象类Runner
的类。Runners用于执行测试类。可以通过@Runwith注解来设置一个Runner要执行的测试。
@RunWith(MyTestRunner.class)
public class MyTestClass {
@Test
public void myTest() {
..
}
}
JUnit测试的启动是通过使用JUnitCore
类实现的。你也可以通过使用命令行,或者使用JUnitCore各种run()方法中的一个(这种方式就是IDE开发工具在你点击run test按钮的时候做的工作)来启动。
JUnitCore.runClasses(MyTestClass.class);
然后JUnitCore使用反射来为传递的测试类找到一个合适的Runner。其中的一个步骤就是查找测试类上的@RunWith注解。如果没有找到其他的Runner,将使用默认的Runner(BlockJUnit4ClassRunner)。Runner将被实例化,测试类会被传递给Runner。然后Runner就会实例化传进来的测试类,然后运行。
Runner怎么工作的?
让我们先来看一下JUnit Runner的类体系:
Runner是一个实现Describable接口的简单类,它有两个抽象方法:
public abstract class Runner implements Describable {
public abstract Description getDescription();
public abstract void run(RunNotifier notifier);
}
getDescription()方法继承自Describable接口,并且会返回一个Description
类型对象。描述(Descriptions)包含了后续将被导出然后被各种工具使用的信息。举个例子,你的IDE集成开发工具也许会使用这些信息来展示测试结果。
run()是一个非常宽泛的方法,它将用于runs something(比如一个测试类或者一个测试簇suite)。我认为,你不会想要去直接扩展Runner类,因为它太宽泛了。
在ParentRunner类中,事情就具体一点了。ParentRunner是一个抽象类,给有多个children的Runner继承用。测试被按照体系(hierarchical)顺序(想象成一棵树)组织和执行,理解这一点很重要。
举个例子,你可能会想运行一个测试簇suite,它包含其它测试簇。然后这些簇每一个都包含多个测试类,每个测试类可以包含多个被测试的方法。
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);
}
ParentRunner的子类需要在getChildren()方法中返回范型T的列表。ParentRunner然后要求每个子类为它的每个child创建一个描述Description(describeChild()),最后运行每一个child(runChild())。
现在让我们来看看两个标准的ParentRunner:BlockJUnit4ClassRunner
和Suite
。
BlockJUnit4ClassRunner是在没指定其它Runner的情况下的默认Runner。如果你运行一个测试类,通常使用的就是BlockJUnit4ClassRunner。如果你看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()方法中,Runner会通过反射扫描所有标注了@Test注解的方法,被扫描到的方法会被包装成FrameworkMethod类型对象返回。describeChildren()方法根据方法名称创建一个Description对象。runChild()方法负责最后运行测试方法。
BlockJUnit4ClassRunner内部使用了很多protected方法,你可以根据自己的需求,重写其中的protected方法。
Suite Runner被用来创建测试簇(test suite),Suites是测试类或者别的测试簇的集合。一个简单的簇的定义类似如下的:
@RunWith(Suite.class)
@Suite.SuiteClasses({
MyJUnitTestClass1.class,
MyJUnitTestClass2.class,
MyOtherTestSuite.class
})
public class MyTestSuite {}
一个测试簇是通过选择有@RunWith注解注释的Suite Runner来创建的。如果你查看Suite的实现,你会看到它其实很简单。Suite做的唯一的一件事就是实例化@SuiteClasses注解中的Runner类。然后,getChildren()返回一个Runner列表,runChildren()把执行委托给相应的Runner。
例子
有了上面的信息,你创建自己的JUnit Runner应该不难了。如果你想查看一些自定义Runner的例子,可以参考下面的例子:
- Fabio Strozzi创建的非常简单明了的GuiceJUnitRunner project。它可以让你在JUnit测试中注入Guice组件。
- Spring的SpringJUnit4ClassRunner可以帮助你测试Spring框架应用。它可以让你在测试类中使用依赖注入,也可以创建支持事务的测试方法。
- Mockito提供了MockitoJUnitRunner,用于自动地初始化mock。
- Oleaster的Java8 Jasmine runner。
结论
JUnit Runners可高度自定义,给了你足够的选择去改变测试执行过程。最酷的事情就是你可以改变测试执行过程,但仍然可以使用IDE工具集成的JUnit功能。
如果你只想做小小的定制化改动,不妨看看BlockJUnit4Class runner的protected方法,有很大机会找到一个可以重写的方法来实现你的改动。
如果你对Oleaster感兴趣,可以看这篇博客: An alternative approach of writing JUnit tests。