单元测试是每个程序员必备的技能,而 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 组合,让单元测试更加灵便,测试场景更加丰盛,更好的实现了测试驱动开发,让零碎更加牢固牢靠。
作者:京东物流 陈昌浩
起源:京东云开发者社区