Junit执行器Runner探索之旅
单元测试是每个程序员必备的技能,而Runner是每个单元测试类必有属性。本文通过解读Junit源码,介绍junit中每个执行器的使用方法,让读者在单元测试时,可以灵活的使用Runner执行器。
一、背景
在今年的敏捷团队建设中,京东物流通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此京东物流的Runner探索之旅开始了!
二、RunWith
RunWith的注释是当一个类用@RunWith注释或扩展一个用@RunWith注释的类时,JUnit将调用它引用的类来运行该类中的测试,而不是内置到JUnit中的运行器,就是测试类根据指定运行方式进行运行。
代码如下:
public @interface RunWith {
Class<? extends Runner> value();
}
其中:Runner 就是指定的运行方式。
三、Runner
Runner的作用是告诉Junit如何运行一个测试类,它是一个抽象类。通过RunWith 指定具体的实现类,如果不指定默认使用BlockJUnit4ClassRunner,Runner的代码如下:
public abstract class Runner implements Describable {
public abstract Description getDescription();
public abstract void run(RunNotifier notifier);
public int testCount() {
return getDescription().testCount();
}
}
3.1 ParentRunner
ParentRunner是一个抽象类,提供了大多数特定于运行器的功能,是经常使用运行器的父节点。实现了Filterable,Sortable接口,可以过滤和排序子对象。
提供了3个抽象方法:
protected abstract List<T> getChildren();
protected abstract Description describeChild(T child);
protected abstract void runChild(T child, RunNotifier notifier);
3.1.1 BlockJUnit4ClassRunner
BlockJUnit4ClassRunner是Juint4默认的运行器,具有与旧的测试类运行器(JUnit4ClassRunner)完全相同的行为。
ParentRunner3个抽象方法的实现如下:
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
@Override
protected Description describeChild(FrameworkMethod method) {
Description description = methodDescriptions.get(method);
if (description == null) {
description = Description.createTestDescription(getTestClass().getJavaClass(),
testName(method), method.getAnnotations());
methodDescriptions.putIfAbsent(method, description);
}
return description;
}
@Override
protected List<FrameworkMethod> getChildren() {
return computeTestMethods();
}
runChild() :
调用describeChild()
判断方法是否包含@Ignore注解,有就触发TestIgnored事件通知
构造Statement回调,通过methodBlock()构造并装饰测试方法
执行测试方法调用statement.evaluate()
describeChild() : 对测试方法创建Description并进行缓存
getChildren():返回运行测试的方法。 默认实现返回该类和超类上所有用@Test标注的未重写的方法
3.1.2 BlockJUnit4ClassRunnerWithParameters
BlockJUnit4ClassRunnerWithParameters是一个支持参数的BlockJUnit4ClassRunner。参数可以通过构造函数注入或注入到带注释的字段中。参数包含名称、测试类和一组参数。
private final Object[] parameters;
private final String name;
public BlockJUnit4ClassRunnerWithParameters(TestWithParameters test)
throws InitializationError {
super(test.getTestClass().getJavaClass());
parameters = test.getParameters().toArray(
new Object[test.getParameters().size()]);
name = test.getName();
}
参数代码如下:
public class TestWithParameters {
private final String name;
private final TestClass testClass;
private final List<Object> parameters;
public TestWithParameters(String name, TestClass testClass,
List<Object> parameters) {
notNull(name, "The name is missing.");
notNull(testClass, "The test class is missing.");
notNull(parameters, "The parameters are missing.");
this.name = name;
this.testClass = testClass;
this.parameters = unmodifiableList(new ArrayList<Object>(parameters));
}
BlockJUnit4ClassRunnerWithParameters一般结合Parameterized使用
3.1.3 Theories
Theories允许对无限数据点集的子集测试某种功能。提供一组参数的排列组合值作为待测方法的输入参数。同时注意到在使用Theories这个Runner的时候,待测方法可以拥有输入参数,可以使您的测试更加灵活。
测试代码如下:
@RunWith(Theories.class)
public class TheoriesTest {
@DataPoints
public static String[] tables = {"方桌子", "圆桌子"};
@DataPoints
public static int[] counts = {4,6,8};
@Theory
public void testMethod(String table, int count){
System.out.println(String.format("一套桌椅有一个%s和%d个椅子", table, count));
}
}
运行结果:
图2 Theories测试代码的执行结果
3.1.4 JUnit4
JUnit4是Junit4默认执行器的别名,想要显式地将一个类标记为JUnit4类,应该使用@RunWith(JUnit4.class),而不是,使用@RunWith(BlockJUnit4ClassRunner.class)
3.1.5 Suite
Suite允许您手动构建包含来自许多类的测试的套件.通过Suite.SuiteClasses定义要执行的测试类,一键执行所有的测试类。
测试代码如下:
@RunWith(Suite.class)
@Suite.SuiteClasses({Suite_test_a.class,Suite_test_b.class,Suite_test_c.class })
public class Suite_main {
}
public class Suite_test_a {
@Test
public void testRun(){
System.out.println("Suite_test_a_running");
}
}
public class Suite_test_b {
@Test
public void testRun(){
System.out.println("Suite_test_b_running");
}
}
public class Suite_test_c {
@Test
public void testRun(){
System.out.println("Suite_test_c_running");
}
}
执行结果:
图3 Suite测试代码的执行结果
如结果所示:执行MainSuit时依次执行了Suite_test_a,Suite_test_b,Suite_test_c 的方法,实现了一键执行。
3.1.6 Categories
Categories在给定的一组测试类中,只运行用带有@ inclecategory标注的类别或该类别的子类型标注的类和方法。通过ExcludeCategory过滤类型。
测试代码如下:
public interface BlackCategory {}
public interface WhiteCategory {}
public class Categories_test_a {
@Test
@Category(BlackCategory.class)
public void testFirst(){
System.out.println("Categories_test_a_testFirst_running");
}
@Test
@Category(WhiteCategory.class)
public void testSecond(){
System.out.println("Categories_test_a_testSecond_running");
}
}
public class Categories_test_b {
@Test
@Category(WhiteCategory.class)
public void testFirst(){
System.out.println("Categories_test_b_testFirst_running");
}
@Test
@Category(BlackCategory.class)
public void testSecond(){
System.out.println("Categories_test_b_testSecond_running");
}
}
执行带WhiteCategory的方法
@RunWith(Categories.class)
@Categories.IncludeCategory(WhiteCategory.class)
@Categories.ExcludeCategory(BlackCategory.class)
@Suite.SuiteClasses( { Categories_test_a.class, Categories_test_b.class })
public class Categories_main {
}
运行结果:
图4 Categories测试代码WhiteCategory分组执行结果
执行带BlackCategory的方法
@RunWith(Categories.class)
@Categories.IncludeCategory(BlackCategory.class)
@Categories.ExcludeCategory(WhiteCategory.class)
@Suite.SuiteClasses( { Categories_test_a.class, Categories_test_b.class })
public class Categories_main {
}
运行结果:
图5 Categories测试代码BlackCategory分组执行结果
如运行结果所示,通过IncludeCategory,ExcludeCategory可以灵活的运行具体的测试类和方法。
3.1.7 Enclosed
Enclosed使用Enclosed运行外部类,内部类中的测试将被运行。 您可以将测试放在内部类中,以便对它们进行分组或共享常量。
测试代码:
public class EnclosedTest {
@Test
public void runOutMethou(){
System.out.println("EnclosedTest_runOutMethou_running");
}
public static class EnclosedInnerTest {
@Test
public void runInMethou(){
System.out.println("EnclosedInnerTest_runInMethou_running");
}
}
}
运行结果:没有执行内部类的测试方法。
图6 Enclosed测试代码的执行结果
使用Enclosed执行器:
@RunWith(Enclosed.class)
public class EnclosedTest {
@Test
public void runOutMethou(){
System.out.println("EnclosedTest_runOutMethou_running");
}
public static class EnclosedInnerTest {
@Test
public void runInMethou(){
System.out.println("EnclosedInnerTest_runInMethou_running");
}
}
}
执行结果:执行了内部类的测试方法。
图7 Enclosed测试代码的执行结果
3.1.8 Parameterized
Parameterized实现参数化测试。 运行参数化的测试类时,会为测试方法和测试数据元素的交叉乘积创建实例。
Parameterized包含一个提供数据的方法,这个方法必须增加Parameters注解,并且这个方法必
须是静态static的,并且返回一个集合Collection,Collection中的值长度必须相等。
测试代码:
@RunWith(Parameterized.class)
public class ParameterizedTest {
@Parameterized.Parameters
public static Collection<Object[]> initData(){
return Arrays.asList(new Object[][]{
{"小白",1,"鸡腿"},{"小黑",2,"面包"},{"小红",1,"苹果"}
});
}
private String name;
private int count;
private String food;
public ParameterizedTest(String name, int count, String food) {
this.name = name;
this.count = count;
this.food = food;
}
@Test
public void eated(){
System.out.println(String.format("%s中午吃了%d个%s",name,count,food));
}
}
运行结果:
图8 Parameterized测试代码的执行结果
3.2 JUnit38ClassRunner
JUnit38ClassRunner及其子类是Junit4的内部运行器,有一个内部类OldTestClassAdaptingListener
实现了TestListener接口。
3.3 ErrorReportingRunner
ErrorReportingRunner也是Junit4运行错误时抛出的异常,代码如下:
private final List<Throwable> causes;
public ErrorReportingRunner(Class<?> testClass, Throwable cause) {
if (testClass == null) {
throw new NullPointerException("Test class cannot be null");
}
this.testClass = testClass;
causes = getCauses(cause);
}
private List<Throwable> getCauses(Throwable cause) {
if (cause instanceof InvocationTargetException) {
return getCauses(cause.getCause());
}
if (cause instanceof InitializationError) {
return ((InitializationError) cause).getCauses();
}
if (cause instanceof org.junit.internal.runners.InitializationError) {
return ((org.junit.internal.runners.InitializationError) cause)
.getCauses();
}
return Arrays.asList(cause);
}
当junit运行错误时,会抛出ErrorReportingRunner,例如:
public Runner getRunner() {
try {
Runner runner = request.getRunner();
fFilter.apply(runner);
return runner;
} catch (NoTestsRemainException e) {
return new ErrorReportingRunner(Filter.class, new Exception(String
.format("No tests found matching %s from %s", fFilter
.describe(), request.toString())));
}
}
3.4 IgnoredClassRunner
IgnoredClassRunner是当测试的方法包含Ignore注解时,会忽略该方法。
public class IgnoredClassRunner extends Runner {
private final Class<?> clazz;
public IgnoredClassRunner(Class<?> testClass) {
clazz = testClass;
}
@Override
public void run(RunNotifier notifier) {
notifier.fireTestIgnored(getDescription());
}
@Override
public Description getDescription() {
return Description.createSuiteDescription(clazz);
}
}
IgnoredClassRunner的使用
public class IgnoredBuilder extends RunnerBuilder {
@Override
public Runner runnerForClass(Class<?> testClass) {
if (testClass.getAnnotation(Ignore.class) != null) {
return new IgnoredClassRunner(testClass);
}
return null;
}
}
当测试时想忽略某些方法时,可以通过继承IgnoredClassRunner增加特定注解实现。
四、小结
Runner探索之旅结束了,可是单元测试之路才刚刚开始。不同的Runner组合,让单元测试更加灵活,测试场景更加丰富,更好的实现了测试驱动开发,让系统更加牢固可靠。
作者:京东物流 陈昌浩
来源:京东云开发者社区
Junit执行器Runner探索之旅的更多相关文章
- 【Linux探索之旅】第二部分第二课:命令行,世界尽在掌握
内容简介 1.第二部分第二课:命令行,世界尽在掌握 2.第二部分第三课预告:文件和目录,组织不会亏待你 命令行,世界尽在掌握 今天的标题是不是有点霸气侧漏呢? 读者:“小编,你为什么每次都要起这么非主 ...
- 【Linux探索之旅】第一部分第五课:Unity桌面,人生若只如初见
内容简介 1.第一部分第五课:Unity桌面,人生若只如初见 2.第一部分第六课预告:Linux如何安装在虚拟机中 Unity桌面,人生若只如初见 不容易啊,经过了前几课的学习,我们认识了Linux是 ...
- 【Web探索之旅】第四部分:Web程序员
内容简介 1.第四部分第一课:什么是Web程序员? 2.第四部分第二课:如何成为Web程序员? 3.第四部分第三课:成为优秀Web程序员的秘诀 第四部分:Web程序员(完结篇) 大家好.终于来到了[W ...
- 【Web探索之旅】第三部分第一课:服务器
内容简介 1.第三部分第一课:服务器 2.第三部分第二课预告:IP地址和域名 第三部分第一课:服务器 大家好,欢迎来到[Web探索之旅]的第三部分.这一部分有不少原理,还是很重要的. 这一部分我们会着 ...
- 【Web探索之旅】第三部分第二课:IP地址和域名
内容简介 1.第三部分第二课:IP地址和域名 2.第三部分第三课预告:协议 第三部分第二课:IP地址和域名 上一课我们说了在Web之中,全球各地有无数台机器,有些充当客户机,有些作为服务器. 那么这些 ...
- 【Web探索之旅】第一部分:什么是Web?
内容简介 1.Web探索之旅:开宗明义 2.第一部分第一课:什么是Web? 3.第一部分第二课:Web,服务和云 4.第一部分第三课:Web的诞生史 Web探索之旅:开宗明义 大家好. 我们这个系列课 ...
- 【C++探索之旅】第一部分第三课:第一个C++程序
内容简介 1.第一部分第三课:第一个C++程序 2.第一部分第四课预告:内存的使用 第一个C++程序 经过上两课之后,我们已经知道了什么是编程,编程的语言,编程的必要软件,C++是什么,我们也安装了适 ...
- 【C语言探索之旅】 第三部分第二课:SDL开发游戏之创建窗口和画布
内容简介 1.第三部分第二课: SDL开发游戏之创建窗口和画布 2.第三部分第三课预告: SDL开发游戏之显示图像 第三部分第二课:SDL开发游戏之创建窗口和画布 在上一课中,我们对SDL这个开源库做 ...
- 【C++探索之旅】开宗明义+第一部分第一课:什么是C++?
内容简介 1.课程大纲 2.第一部分第一课:什么是C++? 3.第一部分第二课预告:C++编程的必要软件 开宗明义 亲爱的读者,您是否对C++感兴趣,但是C++看起来很难,或者别人对你说C++挺难的, ...
- 【Linux探索之旅】开宗明义+第一部分第一课:什么是Linux?
内容简介 1.课程大纲 2.第一部分第一课:什么是Linux? 3.第一部分第二课预告:下载Linux,免费的噢! 开宗明义 我们总听到别人说:Linux挺复杂的,是给那些追求逼格的程序员用的.咱 ...
随机推荐
- 【ACM算法竞赛日常训练】DAY5题解与分析【储物点的距离】【糖糖别胡说,我真的不是签到题目】| 前缀和 | 思维
DAY5共2题: 储物点的距离(前缀和) 糖糖别胡说,我真的不是签到题目(multiset,思维) 作者:Eriktse 简介:19岁,211计算机在读,现役ACM银牌选手力争以通俗易懂的方式讲解算法 ...
- 四个常见的Linux面试问题
四个常见的Linux面试问题. 刚毕业要找工作了,只要是你找工作就会有面试这个环节,那么在面试环节中,有哪些注意事项值得我的关注呢?特别是专业技术岗位,这样的岗位询问一般都是在职的工程师,如何在面试环 ...
- 四月十五号java基础知识
1.今天下午做了一个题感受很深,自己做题没有思路或者有点思路死磕也没有搞清楚,看起来很简单的问题,在我手里很难 做咯许久还是室友帮忙解决的,后面重新打一遍还是出问题,找他解决的,问了问他我自己的问题, ...
- 脚本:auto_send_tablespace定期发送表空间巡检到邮箱
简述:周期定时发送表空间到指定邮箱内 1.修改邮箱配置 /etc/mail.rc,具体细节见网上教程 $ vi /etc/mail.rc set from=123456@qq.comset smtp= ...
- java项目 宿舍管理系统 (源码+数据库文件+1w字论文+ppt)
java项目 宿舍管理系统 (源码+数据库文件+1w字论文+ppt)技术框架:java+springboot+vue+mysql后端框架: Spring Boot.Spring MVC.MyBatis ...
- 【总结】浅刷leetcode,对于位运算提高性能的一些总结
目录 什么是位运算? 位运算技巧 1. 判断奇偶性 2. 交换两个数 3. 判断一个数是否是2的幂次方 4. 取绝对值 5. 计算平均数 结论 位运算技巧是计算机科学中非常重要的一部分,它可以用来解决 ...
- 阿里云 AIGC 白嫖 FC 搭建 stable diffusion
下午瞎逛在 V 站看到阿里在做推广,正好这几天在研究 stable-diffusion,就进去看了看,活动地址: https://developer.aliyun.com/topic/aigc . 主 ...
- element-ui Table 表格行间隔及行边框效果
<el-table :data="tableData" style="width: 100%" :cell-class-name="tableC ...
- VS 查看引用的DLL/Nuget包源码时,无法看到注释
一.问题描述 在下面的截图中,我们发现,源码有添加一段注释. 然后通过Nuget包引用,在VS中用Reshaper反编译时,发现没有注释: 原来,DLL是默认不带注释的.即你生成一个DLL,给另一个项 ...
- 快速上手Linux核心命令(十):Linux安装软件
目录 前言 rpm rpm包管理器 yum 自动化RPM包管理工具 前言 这期呢主要说一说Linux中包软件管理相关命令,这一期的命令虽然只有两个.但 软件包的安装和卸载都是我们平常最常用的,需要熟练 ...