写点什么

Junit 执行器 Runner 探索之旅 | 京东云技术团队

  • 2023-06-13
    北京
  • 本文字数:5714 字

    阅读完需:约 19 分钟

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 个抽象方法的实现如下:


@Overrideprotected void runChild(final FrameworkMethod method, RunNotifier notifier) {    Description description = describeChild(method);    if (isIgnored(method)) {        notifier.fireTestIgnored(description);    } else {        runLeaf(methodBlock(method), description, notifier);    }}@Overrideprotected 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;}@Overrideprotected 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 组合,让单元测试更加灵活,测试场景更加丰富,更好的实现了测试驱动开发,让系统更加牢固可靠。


作者:京东物流 陈昌浩

来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Junit执行器Runner探索之旅 | 京东云技术团队_单元测试_京东科技开发者_InfoQ写作社区