RunListener简述
JUnit4中的RunListener类用来监听测试执行的各个阶段,由RunNotifier通知测试去运行。RunListener与RunNotifier之间的协作应用的是观察者模式,RunListener类充当观察者角色,RunNotifier充当通知者角色,有点类似于JDK中的事件监听器MouseListener在鼠标执行不同的操作时触发相应方法中封装的动作。RunListener监听的动作包括如下测试阶段:
- 所有测试开始前:调用testRunStarted()
- 所有测试结束后:调用testRunFinished()
- 测试套开始前:调用testSuiteStarted()
- 测试套结束后:调用testSuiteFinished()
- 原子测试方法开始前:调用testStarted()
- 原子测试方法结束后:调用testFinished()
- 原子测试被忽略:调用testIgnored()
- 测试执行失败或监听器自身抛出异常:调用testFailure()
- 原子测试方法因调用Assume类中的方法失败(非测试执行失败):调用testAssumptionFailure()
RunListener封装了测试执行过程处于这些阶段时需要做出的响应,另外提供了@ThreadSafe用来表明其所注解的监听器是线程安全的。
JUnit4自身已经提供了如下两个RunListener子类供系统调用,我们还可以根据测试实际需要扩展自己的RunListener。
- TextListener:以打印文本的形式处理测试各阶段的RunListener子类,打印操作由其类成员java.io.PrintStream执行
- SynchronizedRunListener:线程安全的RunListener子类,未使用@ThreadSafe修饰的监听器由该子类封装
Junit4在Result类中内置了一个私有内部类Listener用于实时统计测试执行信息,该内部类继承自RunListener但不对外提供系统调用。
RunListener源码分析
RunListener定义了9个空方法和1个注解,这里空方法的主要作用是分类各种测试事件并定义相应的回调接口,回调接口中的参数用于将数据传递给上层模块。由于各类测试事件发生的时机不同,所以RunListener中分别使用了Description、Result和Failure类封装回调接口需要传递给上层模块的数据。
- 关键代码
JUnit4.12版本增加了@ThreadSafe注解,JUnit4.13版本新增了testSuiteStarted()和testSuiteFinished()两个测试事件。去掉英文注释后的源码如下:
//org.junit.runner.notification
public class RunListener { //所有测试开始前被调用 public void testRunStarted(Description description) throws Exception { } //所有测试结束后被调用 public void testRunFinished(Result result) throws Exception { } //测试套执行前被调用 public void testSuiteStarted(Description description) throws Exception { } //测试套执行后被调用 public void testSuiteFinished(Description description) throws Exception { } //单个原子级测试执行前被调用 public void testStarted(Description description) throws Exception { } //单个原子级测试执行后被调用 public void testFinished(Description description) throws Exception { } //单个原子级测试失败时或监听器抛出异常时被调用,并且把测试失败信息写入到Failure类对象中 public void testFailure(Failure failure) throws Exception { } //原子级测试方法因调用Assume类中的方法失败(非测试执行失败),并将调用失败信息写入到Failure类对象中 public void testAssumptionFailure(Failure failure) { }
//当执行到被@Ignore修饰的原子级测试方法时调用 public void testIgnored(Description description) throws Exception { }
//被@ThreadSafe修饰的监听器是线程安全的 @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ThreadSafe { } }
- 相关类解析
与RunListener相关的类有Description,Result和Failure,三者都实现了序列化接口,作为测试事件回调接口的参数传递给上层模块。本篇只简述这三个类的封装及功能,详细解析将在后续文档中展开。
Description(org.junit.runner)
Description类用来封装待测试对象中的文本描述信息。Description描述的对象可以是单个的测试方法,包含若干个测试方法的单个测试类,或者包含若干个测试类的测试套。
JUnit4使用组合模式来构造待测试对象集合,这些测试对象整体上可以看做一个测试树,每个测试对象都由一个Description类封装且包含如下数据:
private final Collection<Description> fChildren = new ConcurrentLinkedQueue<Description>(); //子节点 private final String fDisplayName; //显示名称 private final Serializable fUniqueId; //唯一标识符 private final Annotation[] fAnnotations; //注解数组 private volatile /* write-once */ Class<?> fTestClass; //待测试类
如果fChildren为空,则该Description为叶子结点;如果fChildren非空,则该Description为复合结点。
Result(org.junit.runner)
Result类用来封装测试执行结果。每个Result对象都包括如下数据:
private final AtomicInteger count; //测试个数 private final AtomicInteger ignoreCount; //忽略的测试个数 private final AtomicInteger assumptionFailureCount; //因调用Assume类方法失败的测试个数 private final CopyOnWriteArrayList<Failure> failures; //测试失败个数 private final AtomicLong runTime; //运行时间 private final AtomicLong startTime; //开始时间
从Result对象中封装的数据可以看出,count,runTime等变量类型为AtomicInteger或AtomicLong,采用的是原子操作,保证变量访问时的线程安全。记录failures使用的是CopyWriteArrayList(写时复制)。
写时复制本身是多线程环境中对共享对象执行修改操作时的优化方法,多线程执行时如果不修改共享对象则不需要使用此项优化,若某个线程需要修改共享对象,则写时复制功能会首先将该共享对象复制一份,然后在新复制对象的地址空间上进行修改,其他的线程仍旧访问之前旧的共享对象,当新复制对象修改完毕后,再将旧共享对象的指针指向新复制对象,此项优化适用于读多写少的场景,也属于读写分离设计的应用。
Result类内部提供了一个私有内部类Listener负责实时统计测试执行信息,该内部类继承自RunListener,由@RunListener.ThreadSafe修饰,所以是线程安全的。
Result类中还封装了一个静态内部类SerializedForm用于扩展序列化,可以根据项目需求将定制的作用域引入到SerializedForm中,从而控制序列化的内容和方式。
Faillure(org.junit.runner.notification)
Failure类用来封装测试执行失败时的信息,JUnit中本身有Failure和Error的概念,在深入Failure之前需要先理清二者的区别。
- Failure:一般用在测试结果断言的场景中,表明测试结果与预期结果不一致,也即测试过程中发现了Bug
- Error:一般是由代码异常引起的,是代码实现自身的错误,说明待测试程序本身不符合提测的标准或健壮性有问题
我们知道在测试执行结果Result类中有一个变量(private final CopyOnWriteArrayList<Failure> failures; //测试失败个数 ),该变量代表的含义是如果测试过程中出现执行失败的情况,则将该失败信息封装为Failure对象并添加到failures列表中。
- 流程分析
因为RunListener的执行流程仅仅是Runner执行测试主流程的一部分,所以此处列出单个测试类执行的典型处理过程(不包括异常处理),并且把与RunListener直接相关的部分以绿色字体显示,便于理解。单个测试类执行的主要过程如下:
- JUnit用例是通过JUnitCore类来执行的,该类可以通过命令行调用,也可以通过IDE调用
- JUnitCore的入口函数为main(),main()方法中调用runMain()方法,runMain方法执行如下操作
- JUnitCommandLineParseResult类进行解析参数,并返回解析结果
- TextListener类封装监听器
- 调用addListener()添加监听器
- 根据解析结果创建测试请求Request类对象,具体过程如下
- 根据待测试类信息及默认的测试策略computer创建Request对象,具体过程如下
- 创建所有可能的RunnerBuilder类对象
- 根据computer对象代表的测试策略及所有可能的RunnerBuilder对象创建出相应的BlockJUnit4ClassRunner类对象
- 将Runner类对象封装为Request类对象并返回
- 根据FilterFactories创建的Filter过滤规则对返回的Request类对象进行过滤,并返回过滤后的Request
- 由该Request类对象获取BlockJUnit4ClassRunner类对象,并将其作为run()方法的入参
- 执行JUnitCore类对象中的run()方法,具体过程如下
- 创建Result类对象
- 创建RunListener监听器
- 添加该新创建的监听器
- 启动测试执行
- 调用runner.run()运行测试,具体过程如下
- 将RunNotifier类对象封装为EachTestNotifier类对象
- 由EachTestNotifier类对象通知测试执行
- classBlock()函数将notifier转换为Statement并对其进行一系列封装处理,具体过程如下
- 调用childrenInvoker将notifier转换为Statement,具体过程如下
- 调用runChildren()处理过滤出的所有FrameworkMethod类对象(如果Runner为Suite的话,此处应为被嵌套的Runner)
- 调用runChild()处理每一个FrameworkMethod类对象
- 调用runLeaf()先执行入参中的调用再执行statement的evaluate()方法,具体如下
- 调用methodBlock()将method转换为Statement,具体如下
- 生成待测试的Test Class对象
- 调用methodInvoker()封装为InvokeMethod类对象(statement)并返回
- 调用possiblyExpectingExceptions()对statement进一步封装
- 调用withPotentialTimeout()对statement进一步封装
- 调用withBefores()对statement进一步封装
- 调用withAfters()对statement进一步封装
- 调用withRules()对statement进一步封装
- 调用withBeforeClasses()对statement进一步封装
- 调用withAfterClasses()对statement进一步封装
- 调用withClassRules()对statement进一步封装
- 调用statement的evaluate()方法执行测试
- 调用invokeExplosively()执行测试,这里是测试方法真正、最终执行的地方
- 结束测试执行,并将结果保存在新创建的Result类对象中
- 移除监听器
- 返回Result类对象
RunListener扩展示例
本次以简单的Calculate类为例扩展RunListener,在所有测试方法执行前后、每个测试方法执行前后监控测试运行过程信息。扩展RunListener功能的一般步骤如下:
- 自定义Listener继承自RunListener
- 自定义Runner中添加该自定义Listener
- JUnit用例@RunWith中引入自定义Runner.class
在扩展RunListener前,先构造待测试类Calculate及相应的JUnit测试类CalculateTest。此处本身就是简略示例,所以代码中涉及到的硬编码信息只是为了看起来更直观易懂,而非处于易维护的考虑。
//待测试类 public class Calculate { public int add(int a, int b) { return Math.addExact(a, b); } public int subtract(int a, int b) { return Math.subtractExact(a, b); } public int multiple(int a, int b) { return Math.multiplyExact(a, b); }
//实际项目中处于健壮性的考虑,Calculate类中的divide()方法实现中应该做相应的异常处理,此处仅仅是为了方便抛出异常,所以直接return a / b. public int divide(int a, int b) { return a / b; } }
//待测试类的JUnit用例 public class CalculateTest { private Calculate cal; @BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println("类测试开始"); } @AfterClass public static void tearDownAfterClass() throws Exception { System.out.println("类测试结束"); } @Before public void setUp() throws Exception { cal = new Calculate(); System.out.println("方法测试开始"); } @After public void tearDown() throws Exception { System.out.println("方法测试结束"); } @Test public void testAdd_Positive() { assertEquals("加法运算出错", 6, cal.add(3, 3)); } @Test(expected=ArithmeticException.class) public void testAdd_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.add(1100000000, 1100000000)); } @Test public void testMinus_Positive() { assertEquals("减法运算出错", 0, cal.subtract(3, 3)); } @Test(expected=ArithmeticException.class) public void testMinus_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.subtract(1100000000, -1100000000)); } @Test public void testMultiple_Positive() { assertEquals("乘法运算出错", 9, cal.multiple(3, 3)); } @Test(expected=ArithmeticException.class) public void testMultiple_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.multiple(1100000000, 2)); } @Test public void testDivide_Poistive() { assertEquals("除法运算出错", 3, cal.divide(9, 3)); } @Test(expected=ArithmeticException.class) public void testDivide_Negetive() { assertEquals("请检查是否存在除0操作", 2, cal.divide(2, 0)); } @Test(timeout=500) public void testTimeout() throws InterruptedException { int sum = 0; for(int i=1; i<=100; i++) { Thread.sleep(1); sum = cal.add(i, sum); } } @Ignore public void testMix() { assertEquals("混合运算出错", 30, cal.divide(cal.add(100, 200), cal.multiple(2, cal.subtract(10, 5)))); } }
从JUnit中打印的信息可以看到,用例比较多时测试执行过程信息容易混淆,调试起来也很难瞬间定位到出错的测试方法,如果我们能在测试执行过程信息中添加相应的测试类名、测试方法名、测试失败详情等信息,执行过程就比较一目了然了,调试效率也会相应提升。基于以上考虑我们可以扩展自己的Listener加入这些特性,扩展后的Listener类和Runner类如下。
import java.util.Date; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; //自定义Listener类,此处的JUnit用例为单一测试类,所以与Suite相关的测试事件不需要覆写 public class MyListener extends RunListener { private long startTime; private long endTime; @Override public void testRunStarted(Description description) throws Exception { startTime = new Date().getTime(); System.out.println("Test Run Started!"); System.out.println("The Test Class is " + description.getClassName() + ". Number of Test Case is " + description.testCount()); System.out.println("==================================================================================="); } @Override public void testRunFinished(Result result) throws Exception { endTime = new Date().getTime(); System.out.println("Test Run Finished!"); System.out.println("Number of Test Case Executed is " + result.getRunCount()); System.out.println("Elipsed Time of this Test Run is " + (endTime - startTime) / 1000); System.out.println("==================================================================================="); } @Override public void testStarted(Description description) throws Exception { System.out.println("Test Method Named " + description.getMethodName() + " Started!"); } @Override public void testFinished(Description description) throws Exception { System.out.println("Test Method Named " + description.getMethodName() + " Ended!"); System.out.println("==================================================================================="); } @Override public void testFailure(Failure failure) throws Exception { System.out.println("Test Method Named " + failure.getDescription().getMethodName() + " Failed!"); System.out.println("Failure Cause is : " + failure.getException()); } @Override public void testAssumptionFailure(Failure failure) { System.out.println("Test Method Named " + failure.getDescription().getMethodName() + " Failed for Assumption!"); } @Override public void testIgnored(Description description) throws Exception { super.testIgnored(description); } }
import org.junit.internal.AssumptionViolatedException; import org.junit.internal.runners.model.EachTestNotifier; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; //扩展Runner,此处的JUnit为单一用例,所以继承自默认Runner public class MyRunner extends BlockJUnit4ClassRunner { public MyRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override public void run(RunNotifier notifier) {
//添加自定义Listener notifier.addListener(new MyListener()); EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription()); notifier.fireTestRunStarted(getDescription()); try { Statement statement = classBlock(notifier); statement.evaluate(); } catch(AssumptionViolatedException av) { testNotifier.addFailedAssumption(av); } catch(StoppedByUserException sbue) { throw sbue; } catch(Throwable e) { testNotifier.addFailure(e); } } }
由于自定义的MyListener中已经扩展了测试执行时的监控信息,所以原待测试类的JUnit用例修改如下:
import static org.junit.Assert.*; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; //修改后的JUnit用例,此处RunWith注解注解用引入自定义Runner @RunWith(MyRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CalculateTest { private static Calculate cal; @BeforeClass public static void setUpBeforeClass() throws Exception { cal = new Calculate(); } @AfterClass public static void tearDownAfterClass() throws Exception { cal = null; } @Test public void testAdd_Positive() { assertEquals("加法运算出错", 6, cal.add(3, 3)); } @Test(expected=ArithmeticException.class) public void testAdd_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.add(1100000000, 1100000000)); } @Test public void testMinus_Positive() { assertEquals("减法运算出错", 0, cal.subtract(3, 3)); } @Test(expected=ArithmeticException.class) public void testMinus_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.subtract(1100000000, -1100000000)); } @Test public void testMultiple_Positive() { assertEquals("乘法运算出错", 9, cal.multiple(3, 3)); } @Test(expected=ArithmeticException.class) public void testMultiple_Negetive() { assertEquals("请检查运算结果是否溢出及测试平台支持的基本数据类型取值范围", 220000000, cal.multiple(1100000000, 2)); } @Test public void testDivide_Poisitive() { assertEquals("除法运算出错", 3, cal.divide(9, 3)); } @Test(expected=ArithmeticException.class) public void testDivide_Negetive() { assertEquals("请检查是否存在除0操作", 2, cal.divide(2, 0)); } @Test(timeout=200) public void testTimeout() throws InterruptedException { int sum = 0; for(int i=1; i<=100; i++) { Thread.sleep(1); sum = cal.add(i, sum); } } @Ignore @Test public void testMix() { assertEquals("混合运算出错", 30, cal.divide(cal.add(100, 200), cal.multiple(2, cal.subtract(10, 5)))); } }
以上JUnit用例执行输出如下:
Test Run Started! The Test Class is com.junit.test.CalculateTest. Number of Test Case is 10 =================================================================================== Test Method Named testAdd_Negetive Started! Test Method Named testAdd_Negetive Ended! =================================================================================== Test Method Named testAdd_Positive Started! Test Method Named testAdd_Positive Ended! =================================================================================== Test Method Named testDivide_Negetive Started! Test Method Named testDivide_Negetive Ended! =================================================================================== Test Method Named testDivide_Positive Started! Test Method Named testDivide_Positive Ended! =================================================================================== Test Method Named testMinus_Negetive Started! Test Method Named testMinus_Negetive Ended! =================================================================================== Test Method Named testMinus_Positive Started! Test Method Named testMinus_Positive Ended! =================================================================================== Test Method Named testMultiple_Negetive Started! Test Method Named testMultiple_Negetive Ended! =================================================================================== Test Method Named testMultiple_Positive Started! Test Method Named testMultiple_Positive Ended! =================================================================================== Test Method Named testTimeout Started! Test Method Named testTimeout Failed! Failure Cause is : org.junit.runners.model.TestTimedOutException: test timed out after 200 milliseconds Test Method Named testTimeout Ended! =================================================================================== Test Run Finished! Number of Test Case Executed is 9 Elipsed Time of this Test Run is 0 ===================================================================================
RunListener总结
事实上自定义监控器的做法在JUnit或TestNG中都比较常用,TestNG本身也是参考JUnit实现的,而且比JUnit具有更多的实用特性。此处以JUnit为例进行源码解析及扩展,是考虑到JUnit框架代码量相对较少好下手,同时其设计也比较灵活,有不少精炼的实现可供参考。实际项目中可根据开发和测试平台的实际需求决定采用JUnit还是TestNG做扩展,此处仅用作源码分析及模式总结。