欢迎访问我的GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

关于《JUnit5学习》系列

《JUnit5学习》系列旨在通过实战提升SpringBoot环境下的单元测试技能,一共八篇文章,链接如下:

  1. 基本操作
  2. Assumptions类
  3. Assertions类
  4. 按条件执行
  5. 标签(Tag)和自定义注解
  6. 参数化测试(Parameterized Tests)基础
  7. 参数化测试(Parameterized Tests)进阶
  8. 综合进阶(终篇)

本篇概览

本文是《JUnit5学习》系列的第三篇,主要是学习Assertions类(org.junit.jupiter.api.Assertions),Assertions类的一系列静态方法给我们提供了单元测试时常用的断言功能,本篇主要内容如下:

  1. Assertions源码分析
  2. 写一段代码,使用Assertions的常用静态方法
  3. 使用异常断言
  4. 使用超时断言
  5. 了解第三方断言库

源码下载

  1. 如果您不想编码,可以在GitHub下载所有源码,地址和链接信息如下表所示:
名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  1. 这个git项目中有多个文件夹,本章的应用在junitpractice文件夹下,如下图红框所示:

  1. junitpractice是父子结构的工程,本篇的代码在assertassume子工程中,如下图:

Assertions源码分析

  1. 下图是一段最简单最常见的单元测试代码,也就是Assertions.assertEquals方法,及其执行效果:

  1. 将Assertions.assertEquals方法逐层展开,如下图所示,可见入参expected和actual的值如果不相等,就会在AssertionUtils.fail方法中抛出AssertionFailedError异常:

  1. 用类图工具查看Assertions类的方法,如下图,大部分是与assertEquals方法类似的判断,例如对象是否为空,数组是否相等,判断失败都会抛出AssertionFailedError异常:



4. 判断两个数组是否相等的逻辑与判断两个对象略有不同,可以重点看看,方法源码如下:

	public static void assertArrayEquals(Object[] expected, Object[] actual) {
AssertArrayEquals.assertArrayEquals(expected, actual);
}
  1. 将上述代码逐层展开,在AssertArrayEquals.java中见到了完整的数组比较逻辑,如下图:

  • 接下来,咱们编写一些单元测试代码,把Assertions类常用的方法都熟悉一遍;

编码实战

  1. 打开junitpractice工程的子工程assertassume,新建测试类AssertionsTest.java:



2. 最简单的判断,两个入参相等就不抛异常(AssertionFailedError):

    @Test
@DisplayName("最普通的判断")
void standardTest() {
assertEquals(2, Math.addExact(1, 1));
}
  1. 还有另一个assertEquals方法,能接受Supplier类型的入参,当判断不通过时才会调用Supplier.get方法获取字符串作为失败提示消息(如果测试通过则Supplier.get方法不会被执行):
    @Test
@DisplayName("带失败提示的判断(拼接消息字符串的代码只有判断失败时才执行)")
void assertWithLazilyRetrievedMessage() {
int expected = 2;
int actual = 1; assertEquals(expected,
actual,
// 这个lambda表达式,只有在expected和actual不相等时才执行
()->String.format("期望值[%d],实际值[%d]", expected, actual));
}
  1. assertAll方法可以将多个判断逻辑放在一起处理,只要有一个报错就会导致整体测试不通过,并且执行结果中会给出具体的失败详情:
    @Test
@DisplayName("批量判断(必须全部通过,否则就算失败)")
void groupedAssertions() {
// 将多个判断放在一起执行,只有全部通过才算通过,如果有未通过的,会有对应的提示
assertAll("单个测试方法中多个判断",
() -> assertEquals(1, 1),
() -> assertEquals(2, 1),
() -> assertEquals(3, 1)
);
}

上述代码执行结果如下:

异常断言

  1. Assertions.assertThrows方法,用来测试Executable实例执行execute方法时是否抛出指定类型的异常;
  2. 如果execute方法执行时不抛出异常,或者抛出的异常与期望类型不一致,都会导致测试失败;
  3. 写段代码验证一下,如下,1除以0会抛出ArithmeticException异常,符合assertThrows指定的异常类型,因此测试可以通过:
    @Test
@DisplayName("判断抛出的异常是否是指定类型")
void exceptionTesting() { // assertThrows的第二个参数是Executable,
// 其execute方法执行时,如果抛出了异常,并且异常的类型是assertThrows的第一个参数(这里是ArithmeticException.class),
// 那么测试就通过了,返回值是异常的实例
Exception exception = assertThrows(ArithmeticException.class, () -> Math.floorDiv(1,0)); log.info("assertThrows通过后,返回的异常实例:{}", exception.getMessage());
}
  • 以上是Assertions的常规用法,接下来要重点关注的就是和超时相关的测试方法;

超时相关的测试

  1. 超时测试的主要目标是验证指定代码能否在规定时间内执行完,最常用的assertTimeout方法内部实现如下图,可见被测试的代码通过ThrowingSupplier实例传入,被执行后再检查耗时是否超过规定时间,超过就调用fail方法抛AssertionFailedError异常:

  1. assertTimeout的用法如下,期望时间是1秒,实际上Executable实例的execute用了两秒才完成,因此测试失败:
    @Test
@DisplayName("在指定时间内完成测试")
void timeoutExceeded() {
// 指定时间是1秒,实际执行用了2秒
assertTimeout(ofSeconds(1), () -> {
try{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

执行结果如下图:



3. 上面的演示中,assertTimeout的第二个入参类型是Executable,此外还有另一个assertTimeout方法,其第二个入参是ThrowingSupplier类型,该类型入参的get方法必须要有返回值,假设是XXX,而assertTimeout就拿这个XXX作为它自己的返回值,使用方法如下:

    @Test
@DisplayName("在指定时间内完成测试")
void timeoutNotExceededWithResult() { // 准备ThrowingSupplier类型的实例,
// 里面的get方法sleep了1秒钟,然后返回一个字符串
ThrowingSupplier<String> supplier = () -> { try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} return "我是ThrowingSupplier的get方法的返回值";
}; // 指定时间是2秒,实际上ThrowingSupplier的get方法只用了1秒
String actualResult = assertTimeout(ofSeconds(2), supplier); log.info("assertTimeout的返回值:{}", actualResult);
}

上述代码执行结果如下,测试通过并且ThrowingSupplier实例的get方法的返回值也被打印出来:



4. 刚才咱们看过了assertTimeout的内部实现代码,是将入参Executable的execute方法执行完成后,再检查execute方法的耗时是否超过预期,这种方法的弊端是必须等待execute方法执行完成才知道是否超时,assertTimeoutPreemptively方法也是用来检测代码执行是否超时的,但是避免了assertTimeout的必须等待execute执行完成的弊端,避免的方法是用一个新的线程来执行execute方法,下面是assertTimeoutPreemptively的源码:

public static void assertTimeoutPreemptively(Duration timeout, Executable executable) {
AssertTimeout.assertTimeoutPreemptively(timeout, executable);
}
  1. assertTimeoutPreemptively方法的Executable入参,其execute方法会在一个新的线程执行,假设是XXX线程,当等待时间超过入参timeout的值时,XXX线程就会被中断,并且测试结果是失败,下面是assertTimeoutPreemptively的用法演示,设置的超时时间是2秒,而Executable实例的execute却sleep了10秒:
    @Test
void timeoutExceededWithPreemptiveTermination() {
log.info("开始timeoutExceededWithPreemptiveTermination");
assertTimeoutPreemptively(ofSeconds(2), () -> {
log.info("开始sleep");
try{
Thread.sleep(10000);
log.info("sleep了10秒");
} catch (InterruptedException e) {
log.error("线程sleep被中断了", e);
}
});
}
  1. 来看看执行结果,如下图,通过日志可见,Executable的execute方法是在新的线程执行的,并且被中断了,提前完成单元测试,测试结果是不通过:

第三方断言库

  1. 除了junit的Assertions类,还可以选择第三方库提供的断言能力,比较典型的有AssertJ, Hamcrest, Truth这三种,它们都有各自的特色和适用场景,例如Hamcrest的特点是匹配器(matchers ),而Truth来自谷歌的Guava团队,编写的代码是链式调用风格,简单易读,断言类型相对更少却不失功能;
  2. springboot默认依赖了hamcrest库,依赖关系如下图:

  1. 一个简单的基于hamcrest的匹配器的单元测试代码如下,由于预期和实际的值不相等,因此会匹配失败:
package com.bolingcavalry.assertassume.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; @SpringBootTest
@Slf4j
public class HamcrestTest { @Test
@DisplayName("体验hamcrest")
void assertWithHamcrestMatcher() {
assertThat(Math.addExact(1, 2), is(equalTo(5)));
}
}
  1. 执行结果如下:

  • 以上就是JUnit5常用的断言功能,希望本篇能助您夯实基础,为后续写出更合适的用例做好准备;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界...

https://github.com/zq2599/blog_demos

JUnit5学习之三:Assertions类的更多相关文章

  1. JUnit5学习之二:Assumptions类

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  2. JUnit5学习之一:基本操作

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  3. JUnit5学习之四:按条件执行

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  4. JUnit5学习之五:标签(Tag)和自定义注解

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  5. JUnit5学习之六:参数化测试(Parameterized Tests)基础

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  6. JUnit5学习之七:参数化测试(Parameterized Tests)进阶

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  7. JUnit5学习之八:综合进阶(终篇)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  8. AspectJ基础学习之三HelloWorld(转载)

    AspectJ基础学习之三HelloWorld(转载) 一.创建项目 我们将project命名为:aspectjDemo.然后我们新建2个package:com.aspectj.demo.aspect ...

  9. C++11并发学习之三:线程同步(转载)

    C++11并发学习之三:线程同步 1.<mutex> 头文件介绍 Mutex又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mutex> 头文 ...

随机推荐

  1. Java 8新特性(Lambda,Stream API)

    由于最近总监要求学习Java 8的一些知识,就去网上找了 一套教程来学习学习,将学习结果做一个小的总结记录,方便以后使用: 1.Java 8的优点 2.Lambda表达式优点 2.1Lambda实例 ...

  2. Spark Dataset DataFrame 操作

    Spark Dataset DataFrame 操作 相关博文参考 sparksql中dataframe的用法 一.Spark2 Dataset DataFrame空值null,NaN判断和处理 1. ...

  3. Hadoop优势,组成的相关架构,大数据生态体系下的模式

    Hadoop优势,组成的相关架构,大数据生态体系下的模式 一.Hadoop的优势 二.Hadoop的组成 2.1 HDFS架构 2.2 Yarn架构 2.3 MapReduce架构 三.大数据生态体系 ...

  4. Java 复习整理day03

    变量或者是常量, 只能用来存储一个数据, 例如: 存储一个整数, 小数或者字符串等. 如果需要同时存储多个同类型的数据, 用变量或者常量来实现的话, 非常的繁琐. 针对于 这种情况, 我们就可以通过数 ...

  5. 使用C#实现数据结构堆

    一. 堆的介绍: 堆是用来排序的,通常是一个可以被看做一棵树的数组对象.堆满足已下特性: 1. 堆中某个节点的值总是不大于或不小于其父节点的值 任意节点的值小于(或大于)它的所有后裔,所以最小元(或最 ...

  6. Codeforces Round #646 (Div. 2) E. Tree Shuffling dfs

    题意: 给你n个节点,这n个节点构成了一颗以1为树根的树.每一个节点有一个初始值bi,从任意节点 i 的子树中选择任意k个节点,并按他的意愿随机排列这些节点中的数字,从而产生k⋅ai 的成本.对于一个 ...

  7. ERROR 1045 (28000): Access denied for user 'ODBC'@'localhost' (using password: NO)

    cmd mysql -h localhost -u root -p r然后报错 ERROR 1045 (28000): Access denied for user 'ODBC'@'localhost ...

  8. Netty(六)揭开 BootStrap 的神秘面纱

    6.1 客户端 BootStrap 6.1.1 Channel 简介 在 Netty 中,Channel 是一个 Socket 的抽象,它为用户提供了关于 Socket 状态(是否是连接还是断开)以及 ...

  9. Doris开发手记1:解决蛋疼的MySQL 8.0连接问题

    笔者作为Apache Doris的开发者,平时感觉相关Doris的文章写的很少.主要是很多时候不知道应该去记录一些怎么样的问题,感觉写的不好就会很慌张.新的一年,希望记录自己在Doris开发过程之中所 ...

  10. for-in循环等

    一.for-in循环 in表示从(字符串.序列等)中一次取值,又称为遍历 其便利对象必须是可迭代对象 语法结构: for 自定义的变量 in 可迭代对象: 循环体 for item in 'Pytho ...