你知道 Junit 是怎么跑的吗?

Junit 是由 Kent Beck 和 Erich Gamma 于 1995 年底着手编写的框架,自此以后,Junit 框架日益普及,现在已经成为单元测试 Java 应用程序的事实上的标准。

在软件开发领域中,从来没有这样的事情:少数几行代码对大量代码起着如此重要的作用 — Martin Fowler

从一个简单的例子开始认识 Junit

本文注重点在于研究 Junit 运行的基本原理和执行单元测试的流程,所以对于一些额外的信息和数据不单独准备,本文所使用的测试 case 如下:

package com.glmapper.bridge.boot;

import org.junit.*;

public class JunitSamplesTest {

    @Before
    public void before(){
        System.out.println(".....this is before test......");
    }

    @After
    public void after(){
        System.out.println(".....this is after test......");
    }

    @BeforeClass
    public static void beforeClass(){
        System.out.println(".....this is before class test......");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println(".....this is after class test......");
    }

    @Test
    public void testOne(){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(){
        System.out.println("this is test two");
    }
}

复制代码

执行结果如下:

.....this is before class test......
Disconnected from the target VM, address: '127.0.0.1:65400', transport: 'socket'
.....this is before test......
this is test one
.....this is after test......
.....this is before test......
this is test two
.....this is after test......
.....this is after class test......
复制代码

从代码和执行结果来看,BeforeClass 和 AfterClass 注解分别在测试类开始之前和之后执行,Before 和 After 注解在测试类中每个测试方法的前后执行。

问题域

从开发者的角度来看,对于任何一个技术产品组件,如果想要更好的使用它,就意味着必须了解它。通过上面提供的 case 可以看到,Junit 使用非常简单,基本 0 门槛上手,通过给测试的方法加一个 @Test 注解,然后将待测试逻辑放在 被 @Test 标注的方法内,然后 run 就好了。简单源于组件开发者的顶层抽象和封装,将技术细节屏蔽,然后以最简洁的 API 或者注解面向用户,这也是 Junit 能够让广大开发者容易接受的根本原因,值得我们借鉴学习。

回归正题,基于上面分析,Junit 使用简单在于其提供了非常简洁的 API 和注解,那对于我们来说,这些就是作为分析 Junit 的基本着手点;通过这些,来拨开 Junit 的基本原理。基于第一节的小案例,这里抛出这样几个问题:

  • Junit 是怎么触发执行的
  • 为什么被标注 @Test 注解的方法会被执行,而没有标注的不会
  • Before 和 After 执行时机
  • BeforeClass 和 AfterClass 执行时机
  • Junit 是怎么将执行结果收集并返回的(这里不关注 IDE 提供的渲染)

Junit 是如何执行的?

这里把断点直接打在目标测试方法位置,然后 debug 执行
image.png

通过堆栈来找到用例执行的整个路径。因为本 case 是通过 idea 启动执行,所以可以看到的入口实际是被 idea 包装过的。但是这里也抓到了 JUnitCore 这样的一个入口。

JUnitCore 是运行测试用例的门面入口,通过源码注释可以看到,JUnitCore 从 junit 4 才有,但是其向下兼容了 3.8.x 版本系列。我们在跑测试用例时,其实大多数情况下在本地都是通过 IDE 来触发用例运行,或者通过 mvn test 来运行用例,实际上,不管是 IDE 还是 mvn 都是对 JUnitCore 的封装。我们完全可以通过 main 方法的方式来运行,比如运行下面代码的 main 方法来通过一个 JUnitCore 实例,然后指定被测试类来触发用例执行,为了尽量使得堆栈更贴近 Junit 自己的代码,我们通过这种方式启动来减少堆栈对于代码执行路径的干扰。

public class JunitSamplesTest {

    @Before
    public void before(){
        System.out.println(".....this is before test......");
    }

    @After
    public void after(){
        System.out.println(".....this is after test......");
    }

    @BeforeClass
    public static void beforeClass(){
        System.out.println(".....this is before class test......");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println(".....this is after class test......");
    }

    @Test
    public void testOne(){
        System.out.println("this is test one");
    }

    @Test
    public void testTwo(){
        System.out.println("this is test two");
    }

    public static void main(String[] args) {
        JUnitCore jUnitCore = new JUnitCore();
        jUnitCore.run(JunitSamplesTest.class);
    }
}

复制代码

这里得到了最简化的测试执行入口:

image.png

如果使用 java 命令来引导启动,其实就是从 JunitCore 内部自己的 main 方法开始执行的

/** 
 * Run the tests contained in the classes named in the args. If all tests run successfully, exit with a status of 0. Otherwise exit with a status of 1. Write
 * feedback while tests are running and write stack traces for all failed tests after the tests all complete.
 * Params:
 * args – names of classes in which to find tests to run
 **/

public static void main(String... args) {
    Result result = new JUnitCore().runMain(new RealSystem(), args);
    System.exit(result.wasSuccessful() ? 0 : 1);
}

复制代码

为什么被标注 @Test 注解的方法会被执行,而没有标注的不会

这里比较好理解,被打了 @Test 注解的方法,一定是 Junit 通过某种方式将其扫描到了,然后作为待执行的一个集合或者队列中。下面通过分析代码来论证下。

org.junit.runners.BlockJUnit4ClassRunner#getChildren

@Override
protected List<FrameworkMethod> getChildren() {
    return computeTestMethods();
}
复制代码

通过方法 computeTestMethods 方法名其实就可以看出其目的,就是计算出所有的测试方法。

image.png

etAnnotatedMethods 通过指定的 annotationClass 类型,将当前 TestClass 中类型为 annotationClass 类型注解标注的方法过滤出来,

image.png

getFilteredChildren 中最后将获取得到的测试方法放在 filteredChildren 中缓存起来。这里简单汇总下 @Test 注解被识别的整个过程(其他注解如 @Before 都是一样的)

  • 1、Junit 在初始化构建 Runner 的过程,内部会基于给定的 测试类创建一个 TestClass 对象模型,用于描述当前测试类在 Junit 中的表示。
// clazz 是待测试类
public TestClass(Class<?> clazz) {
    this.clazz = clazz;
    if (clazz != null && clazz.getConstructors().length > 1) {
        // 测试类不能有有参构造函数
        throw new IllegalArgumentException(
            "Test class can only have one constructor");
    }

    Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkMethod>>();
    Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations =
        new LinkedHashMap<Class<? extends Annotation>, List<FrameworkField>>();
    // 扫描待测试类中所有的 Junit 注解,包括 @Test @Before @After 等等
    scanAnnotatedMembers(methodsForAnnotations, fieldsForAnnotations);
	// 过滤出打在方法上的注解,
    this.methodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
    // 过滤出打在变量上的注解
    this.fieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
}
复制代码

methodsForAnnotations 和 fieldsForAnnotations 缓存了当前待测试类所有被 junit 注解标注过的方法和变量

  • 2、getFilteredChildren 中,从 methodsForAnnotations 中筛选出所有 @Test 注解标注的方法。(getDescription()-> getFilteredChildren -> computeTestMethods -> 从 methodsForAnnotations 按类型过滤)
  • 3、返回所有 @Test 注解标注的方法

Before 和 After 执行时机

要搞定这个问题,其实有必要了解下 Junit 中一个比较重要的概念 Statement。

public abstract class Statement {
    /**
     * Run the action, throwing a {@code Throwable} if anything goes wrong.
     */
    public abstract void evaluate() throws Throwable;
}
复制代码

Statement 从 junit 4.5 版本被提出,Statement 表示在运行 JUnit 测试组件的过程中要在运行时执行的一个或多个操作,简单说就是,对于被 @Before @After 注解标注的方法,在 JUnit 会被作为一种 Statement 存在,分别对应于 RunBefores 和 RunnerAfter,这些 statement 中持有了当前运行所有的 FrameworkMethod。

FrameworkMethod 是 JUnit 中所有被 junit 注解标注方式的内部描述,@Test, @Before, @After, @BeforeClass, @AfterClass 标注的方法最终都作为 FrameworkMethod 实例存在。

Statement 的创建有两种方式,基于 FrameworkMethod 的 methodBlock 和基于 RunNotifier 的 classBlock,这里介绍 methodBlock ,classBlock 下节讨论。

protected Statement methodBlock(final FrameworkMethod method) {
        Object test;
        try {
            test = new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest(method);
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement = methodInvoker(method, test);
        statement = possiblyExpectingExceptions(method, test, statement);
        statement = withPotentialTimeout(method, test, statement);
        statement = withBefores(method, test, statement);
        statement = withAfters(method, test, statement);
        statement = withRules(method, test, statement);
        statement = withInterruptIsolation(statement);
        return statement;
    }
复制代码

withAfters、withBefores 会将 RunAfters 和 RunBefore 绑定到 statement,最后 形成一个 statement 链,这个链的执行入口时 RunAfters#evaluate。

@Override
public void evaluate() throws Throwable {
    List<Throwable> errors = new ArrayList<Throwable>();
    try {
        next.evaluate();
    } catch (Throwable e) {
        errors.add(e);
    } finally {
        // 在 finally 中执行 after 方法
        for (FrameworkMethod each : afters) {
            try {
                invokeMethod(each);
            } catch (Throwable e) {
                errors.add(e);
            }
        }
    }
    MultipleFailureException.assertEmpty(errors);
}
复制代码

next 链中包括 before 和待执行的测试方法

image.png

所以我们看到的就是 before -> testMethod -> after。

这里其实和预想的不太一样,关于 before 和 after 这种逻辑,第一想法是通过代理的方式,对测试方法进行代理拦截,类似 Spring AOP 中的 Before 和 After,其实不然。

BeforeClass 和 AfterClass 执行时机

前面分析了 methodBlock,了解到 junit 中通过这个方法创建 statement 并且将 before 和 after 的方法绑定给 statement,以此推断,classBlock 的作用就是将 BeforeClass 和 AfterClass 绑定给statement 。

protected Statement classBlock(final RunNotifier notifier) {
    // childrenInvoker 这里会调用到 methodBlock
    Statement statement = childrenInvoker(notifier);
    if (!areAllChildrenIgnored()) {
        statement = withBeforeClasses(statement);
        statement = withAfterClasses(statement);
        statement = withClassRules(statement);
        statement = withInterruptIsolation(statement);
    }
    return statement;
}
复制代码

BeforeClass 和 before 都会对应创建一个 RunnerBefores,区别在于 BeforeClass 在创建 RunnerBefores 时,不会指定目标测试方法。

  • BeforeClass 在执行 statement 之前,运行该类和超类上所有非覆盖的@BeforeClass方法;如果有抛出异常,停止执行并传递异常。
  • AfterClass 在执行 statement链最后,在该类和超类上运行所有未覆盖的 @AfterClass 方法;始终执行所有 AfterClass 方法:如有必要,将前面步骤抛出的异常与来自 AfterClass 方法的异常合并到 org.junit.runners.model.MultipleFailureException 中。

Junit 是怎么将执行结果收集并返回的

junit 所有执行的结果都存放在 Result 中

// 所有 case 数
private final AtomicInteger count;
// 忽略执行的 case 数(被打了 ignore)
private final AtomicInteger ignoreCount;
// 失败 case 数
private final AtomicInteger assumptionFailureCount;
// 所有失败 case 的结果
private final CopyOnWriteArrayList<Failure> failures;
// 执行时间
private final AtomicLong runTime;
// 开始时间
private final AtomicLong startTime;
复制代码

Result 中内置了一个默认的来监听器,这个监听器会在每个 case 执行完成之后进行相应的回调,Listener 如下:

@RunListener.ThreadSafe
    private class Listener extends RunListener {
    // 设置开始时间
        @Override
        public void testRunStarted(Description description) throws Exception {
            startTime.set(System.currentTimeMillis());
        }
		
        // 执行完所有 case
        @Override
        public void testRunFinished(Result result) throws Exception {
            long endTime = System.currentTimeMillis();
            runTime.addAndGet(endTime - startTime.get());
        }
		// 执行完某个 case
        @Override
        public void testFinished(Description description) throws Exception {
            count.getAndIncrement();
        }
		// 执行完某个 case 失败
        @Override
        public void testFailure(Failure failure) throws Exception {
            failures.add(failure);
        }
		// 执行完某个ignore case
        @Override
        public void testIgnored(Description description) throws Exception {
            ignoreCount.getAndIncrement();
        }

        @Override
        public void testAssumptionFailure(Failure failure) {
        // Assumption 产生的失败
            assumptionFailureCount.getAndIncrement();
        }
    }
复制代码

JUnit 4 开始在测试中支持假设 Assumptions,在 Assumptions 中,封装了一组使用的方法,以支持基于假设的条件测试执行。假设实际就是指定某个特定条件,假如不能满足假设条件,假设不会导致测试失败,只是终止当前测试。这也是假设与断言的最大区别,因为对于断言而言,会导致测试失败。

所以 JUnit 通过监听器机制收集所有的测试信息,最终封装到 Result 中返回。

总结

Junit 中有一些比较基本的概念,比如 Runner,statement 等;在初始化时,默认情况下 junit 会构建出 BlockJUnit4ClassRunner 这样的一个 Runner,并且在这个 Runner 中会持有被测试类的所有信息。Runner 运行测试并在执行此操作时将重要事件通知 RunNotifier。

也可以使用 RunWith 调用自定义 Runner,这里只要你的 Runner 是 org.junit.runner.Runner 子类即可;创建自定义运行程序时,除了在此处实现抽象方法外,还必须提供一个构造函数,这个构造函数将包含测试的类作为参数–如:SpringRunner。

Runner 的 run 方法内部就是构建和执行 Statement 链的过程,Statement 中描述了单元测试中需要执行的一系列操作,每个 case 均以 RunnerAfter -> TargetMethod -> RunnerBefore 的执行顺序依次执行;执行过程中,junit 通过监听器机制回调 case 调用的每个生命周期阶段,并将各个case 执行的信息进行收集汇总,最终返回执行结果 Result 。

原创文章,作者:睿达君,如若转载,请注明出处:https://zrrd.net.cn/2025.html

发表评论

登录后才能评论
咨询电话
联系电话:0451-81320577

地址:哈尔滨市松北区中小企业总部基地13F

微信咨询
微信咨询
QQ咨询
分享本页
返回顶部