IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 你知道 Junit 是怎么跑的吗?带你深入了解Junit运行流程 -> 正文阅读

[开发测试]你知道 Junit 是怎么跑的吗?带你深入了解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);
}

?我这边有个技术交流群:2931934545,没事大家平时一起学习交流、分享经验,群内还有大量功能测试、性能测试、python自动化、java自动化、测试开发、接口测试、APP测试等多个知识点的测试资料免费领取哦

为什么被标注 @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 。


标添加微信获取更多学习资料,备注来意即可题


?

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章           查看所有文章
加:2021-07-07 11:52:23  更:2021-07-07 11:54:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/3 13:19:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码