1. Junit5 简介
资料
Junit5官方资料:
1.1 Junit5的架构
架构图如下:
与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Platform:是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
- JUnit Jupiter:是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter子项目提供了一个TestEngine在平台上运行基于Jupiter的测试。
- JUnit Vintage:提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试
1.1.1 Junit Platform
该模块提供了在JVM上启动测试框架的核心基础功能,充当JUnit与其客户端(如构建工具[Maven、Gradle]和IDE[Eclipse、IntelliJ])之间的接口。它引入了Launcher(启动器)的概念,外部工具可以使用它来发现、过滤和执行测试用例。
它还提供了TestEngine API,用于开发在JUnit上运行的测试框架;使用TestEngine API,第三方测试库(如Spock、Cucumber和FitNesse)可以直接j集成它们的自定义TestEngine。
1.1.2 Junit Jupiter
该模块为在Junit 5中编写测试和扩展提供了一个新的编程模型和扩展模型。
它有一套全新的注解来编写Junit5中的测试用例,其中包括@BeforeEach、@AfterEach、@AfterAll、@BeforeAll等。可以理解为是对Junit Platform的TestEngine API的实现,以便Junit5测试可以运行。
1.1.3 Junit Vintage
Vintage从字面意思理解是“古老的,经典的”。
该模块就是为了对Junit4和JUnit3编写的测试用例提供支持。因此,Junit5具备向后兼容能力。
1.2 JUnit Jupiter API 的使用
JUnit Jupiter是在JUnit5中编写测试和扩展的新编程模型和扩展模型的组合; 所以我们使用Jupiter来学习Junit5。
1.2.1 常用注解
- @Test 表示方法是一种测试方法。 与JUnit 4的@Test注解不同,此注释不会声明任何属性。
- @ParameterizedTest 表示方法是参数化测试
- @RepeatedTest 表示方法是重复测试模板
- @TestFactory 表示方法是动态测试的测试工程
- @DisplayName 为测试类或者测试方法自定义一个名称
- @BeforeEach 表示方法在每个测试方法运行前都会运行 ,
- @AfterEach 表示方法在每个测试方法运行之后都会运行
- @BeforeAll 表示方法在所有测试方法之前运行 ,
- @AfterAll 表示方法在所有测试方法之后运行
- @Nested 表示带注解的类是嵌套的非静态测试类,@BeforeAll和 @AfterAll方法不能直接在@Nested测试类中使用,除非修改测试实例生命周期。
- @Tag 用于在类或方法级别声明用于过滤测试的标记
- @Disabled 用于禁用测试类或测试方法
- @ExtendWith 用于注册自定义扩展,该注解可以继承
- @FixMethodOrder(MethodSorters.NAME_ASCENDING) ,控制测试类中方法执行的顺序,这种测试方式将按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致;不过这种方式需要对测试方法有一定的命名规则,如 测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)
1.2.2 JUnit 5 对 JUnit 4注解对比
JUnit 5 对 JUnit 4 中的注解进行了变更和补充 ,以下是对照表
| JUnit 4 | JUnit 5 | 注解说明 |
|---|---|---|
| @Test | @Test | 声明一个测试方法 |
| @BeforeClass | @BeforeAll | 在当前类 所有的测试方法执行之前,执行 |
| @AfterClass | @AfterAll | 在当前类 所有的测试方法执行之后,执行 |
| @Before | @BeforeEach | 在每个测试方法执行之前,执行 |
| @After | @AfterEach | 在每个测试方法执行之后,执行 |
| @Ignore | @Disabled | 禁用一个测试方法或测试类 |
| @Category | @Tag | 给测试类或测试方法打标签 |
| NA | @DisplayName | 设置测试类或测试方法的展示名称 |
| NA | @RepeatedTest | 重复性测试 |
| NA | @ValueSource 、@CsvSource 等 | 参数化测试 |
| NA | @Nested | 内嵌其他测试类 |
| NA | @TestFactory | 使用 TestFactory 进行动态测试 |
2 基本使用
2.1 Maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>learn</artifactId>
<groupId>com.evan</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>junit5_learn</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
2.2 Hello World
第一个测试:
package evan.junit5.learn;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HelloWorld {
@Test
void firstTest() {
assertEquals(2, 1 + 1);
}
}
执行结果
2.3 注解基本使用
2.3.1 @Disabled(忽略)
测试类上加@Disabled:
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled
class DisabledClassTest {
@Test
void testWillBeSkipped() {
}
}
测试类中的方法加@Disabled:
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTest {
@Disabled
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
2.3.2 @RepeatedTest(重复性测试)
JUnit Jupiter通过使用@RepeatedTest注解方法并指定所需的重复次数,提供了重复测试指定次数的功能。每次重复测试的调用都像执行常规的@Test方法一样,完全支持相同的生命周期回调和扩展。
以下示例演示了如何声明名为repeatedTest()的测试,该测试将自动重复10次。
@DisplayName("重复测试")
@RepeatedTest(value = 3)
public void test() {
System.out.println("执行测试");
}
除了指定重复次数外,还可以通过@RepeatedTest注解的name属性为每次重复配置自定义显示名称。此外,显示名称可以是模式,由静态文本和动态占位符的组合而成。目前支持以下占位符:
- {displayName}: @RepeatedTest方法的显示名称
- {currentRepetition}: 当前重复次数
- {totalRepetitions}: 重复的总次数
测试例子
package evan.junit5.learn;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Repeat Test.
*/
public class RepeatTest {
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
System.out.printf("About to execute repetition %d of %d for %s%n", currentRepetition, totalRepetitions, methodName);
}
@RepeatedTest(3)
void repeatedTest() {
// ...
}
@DisplayName("自定义名称重复测试")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void test() {
System.out.println("执行测试");
}
@RepeatedTest(2)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(2, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
}
@RepeatedTest(value = 2, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
执行结果:
2.3.3 @Nested(嵌套)
当我们编写的类和代码逐渐增多,随之而来的需要测试的对应测试类也会越来越多。为了解决测试类数量爆炸的问题,JUnit 5 提供了 @Nested 注解,能够以静态内部成员类的形式对测试用例类进行逻辑分组。 并且每个静态内部类都可以有自己的生命周期方法, 这些方法将按从外到内层次顺序执行。 此外,嵌套的类也可以用 @DisplayName 标记,这样我们就可以使用正确的测试名称。
嵌套测试给测试编写者更多的能力,来表达几组测试之间的关系。这里有一个详细的例子。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* @author yatao.xu
* @version 1.0.0
* @date 2022-07-02
**/
@DisplayName("内嵌测试类")
public class MyTest {
@Nested
@DisplayName("内嵌测试类A")
class FirstNestTest {
@DisplayName("测试方法A1")
@Test
void test() {
System.out.println("测试方法A1");
}
}
@Nested
@DisplayName("内嵌测试类B")
class SecondNestTest {
@DisplayName("测试方法B1")
@Test
void test() {
System.out.println("测试方法B1");
}
@DisplayName("测试方法B2")
@Test
void test2() {
System.out.println("测试方法B2");
}
}
}
2.4 @ParameterizedTest(参数化测试)
要使用 JUnit 5 参数化测试功能,除了 junit-jupiter-engine 基础依赖之外,还需要 junit-jupiter-params 依赖参数化测试使用 @ParameterizedTest 注解,替代 @Test 注解
JUnit Jupiter开箱即用,提供了不少source注解。
2.4.1 @ValueSource
@ValueSource 是 JUnit5 提供的最简单的数据参数源,支持 Java 的八大基本类型和字符串,Class,使用时赋值给注解上对应类型属性,以数组方式传递
@DisplayName("String类型参数化测试")
@ParameterizedTest
@ValueSource(strings = {"evan","eva","bob","pop"})
void testPrintTitleName(String titleName){
System.out.println(titleName);
}
2.4.2 @EnumSource
@EnumSource提供了一个使用Enum常量的简便方法。该注释提供了一个可选的name参数,可以指定使用哪些常量。如果省略,所有的常量将被用在下面的例子中。
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithEnumSource(TimeUnit timeUnit) {
assertNotNull(timeUnit);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
@EnumSource注解还提供了一个可选的mode参数,可以对将哪些常量传递给测试方法进行细化控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如下例所示。
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
void testWithEnumSourceExclude(TimeUnit timeUnit) {
assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
assertTrue(timeUnit.name().length() > 5);
}
@ParameterizedTest
@EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
void testWithEnumSourceRegex(TimeUnit timeUnit) {
String name = timeUnit.name();
assertTrue(name.startsWith("M") || name.startsWith("N"));
assertTrue(name.endsWith("SECONDS"));
}
2.4.3 @MethodSource
@MethodSource允许你引用一个或多个测试类的工厂方法。这样的方法必须返回一个Stream,Iterable,Iterator或者参数数组。另外,这种方法不能接受任何参数。默认情况下,除非测试类用@TestInstance(Lifecycle.PER_CLASS)注解,否则这些方法必须是静态的。
如果只需要一个参数,则可以返回参数类型的实例Stream,如以下示例所示。
@ParameterizedTest
@MethodSource("stringProvider")
void testWithSimpleMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("foo", "bar");
}
支持原始类型(DoubleStream,IntStream和LongStream)的流,示例如下:
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}
如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示。请注意,Arguments.of(Object…)是Arguments接口中定义的静态工厂方法。
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(3, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
Arguments.of("foo", 1, Arrays.asList("a", "b")),
Arguments.of("bar", 2, Arrays.asList("x", "y"))
);
}
2.4.4 @CsvSource
@CsvSource 注解可以支持 CSV 格式的数据,默认分隔符为逗号,也可以通过 delimiter 属性指定
@DisplayName("csv文件 参数化测试")
@ParameterizedTest
@CsvSource(value = {"1@evan", "2@eva", "3@bob", "4@pop"}, delimiter = '@')
void testPrintCSVName(String id, String name) {
System.out.println(id + ":" + name);
}
@CsvSource使用'作为转义字符。 请参阅上述示例和下表中的’baz, qux’值。 一个空的引用值''会导致一个空的String; 而一个完全空的值被解释为一个null引用。如果null引用的目标类型是基本类型,则引发ArgumentConversionException。
| 示例输入 | 结果字符列表 |
|---|---|
| @CsvSource({ “foo, bar” }) | "foo", "bar" |
| @CsvSource({ “foo, ‘baz, qux’” }) | "foo", "baz, qux" |
| @CsvSource({ “foo, ‘’” }) | "foo", "" |
| @CsvSource({ “foo, “ }) | "foo", null |
2.4.5 @CsvFileSource
除了像 @CsvSource 直接把数据写在代码里,JUnit 5 还支持使用 @CsvFileSource 注解,指定外部 csv 文件,指定的资源文件路径时要以 / 开始,寻找当前测试资源目录下文件
@DisplayName("外部csv文件 参数化测试")
@ParameterizedTest
@CsvFileSource(resources = "/test.csv", delimiter = '@')
void testPrintCSVFileName(String id, String name) {
System.out.println(id + ":" + name);
}
test.csv
1@evan
2@eva
3@bob
4@pop
与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为转义字符,
2.5 @TestFactory(动态测试)
除了这些标准测试外,JUnit Jupiter还引入了一种全新的测试编程模型。这种新的测试是动态测试,它是由 @TestFactory 注解的工厂方法在运行时生成的。
与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产物。从技术上讲,@TestFactory方法必须返回DynamicNode实例的Stream,Collection,Iterable或Iterator。 DynamicNode的可实例化的子类是DynamicContainer和DynamicTest。
DynamicContainer实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。然后,DynamicTest实例将被延迟执行,从而实现测试用例的动态甚至非确定性生成。
任何由@TestFactory返回的Stream都要通过调用stream.close()来正确关闭,使得使用诸如Files.lines()之类的资源变得安全。
与@Test方法一样,@TestFactory方法不能是private或static,并且可以选择声明参数,以便通过ParameterResolvers解析。
DynamicTest是运行时生成的测试用例。它由显示名称和Executable组成。 Executable是@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用来提供。
package evan.junit5.learn;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
import java.util.*;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Dynamic Test.
*/
public class DynamicsTest {
// 返回JUnitException异常
//must return a single DynamicNode or a Stream, Collection, Iterable, Iterator, or array of DynamicNode.
@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(true)),
dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(true)),
dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(true)),
dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
).iterator();
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("A", "B", "C")
.map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
//为前10个偶数生成测试
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTests() {
//生成0到100之间的随机正整数,直到遇到可被7整除的数字。
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
//生成显示名称,如:input:5、input:37、input:85等
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
//根据当前输入值执行测试。
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
//返回动态测试流。
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
))
)));
}
}
执行结果:
3. 断言
在阿里巴巴Java开发手册中,有这么一段话:单元测试应该是全自动执行的,并且非交互式的。我们不能依靠人肉来验证程序的正确性。所以应该使用断言(Asset)来验证程序正确性。
所有JUnit5的断言都在org.juit.jupiter.Assertions类中。这些方法支持Java8 lambda表达式,并被大量重载以支持不同类型,如基本数据类型、对象、stream、数组等。
准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果。一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。
3.1 常用断言方法
| 断言方法 | 作用 |
|---|---|
| assertNull() | 断言实际输出为null. |
| assertNotNull() | 断言实际输出不为null. |
| fail() | 使单元测试失败 |
| assertSame() | 断言期望值和实际值是用一个对象 |
| assertNotSame() | 断言期望值和实际值不是用一个对象 |
| assertTrue() | 断言实际值为true |
| assertFalse() | 断言实际值为false |
| assertEquals() | 断言期望值和实际值相等 |
| assertNotEquals() | 断言期望值和实际值不相等 |
| assertArrayEquals() | 断言期望数组和实际数组相等 |
| assertIterableEquals() | 断言期望可迭代容器和实际可迭代容器相等 |
| assertThrows() | 断言可执行代码中会抛出期望的异常类型 |
| assertAll() | 断言一组中的多个 |
| assertTimeout() | 断言一段可执行代码的会在指定时间执行结束 |
| assertTimeoutPreemptively() | 断言可执行代码如果超过指定时间会被抢占中止 |
3.2 常用断言方法的基本使用
3.2.1 基本使用
无论哪种断言方法,都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来。
@Test
void asserts() {
assertEquals(1,2, () -> "1要是1");
}
@Test
void assume() {
assumingThat("DEV".equals(System.getenv("ENV")),
() -> {
// 如果不为true这里将不执行
assertEquals(1, 1);
});
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// 如果不为true这里将不执行
}
3.2.2 简单断言的demo
package evan.junit5.learn;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.*;
public class AssertionsTest {
@Test
void asserts() {
assertEquals(1, 2, () -> "期望结果为1");
}
Person person = new Person("John", "Doe");
@Test
@DisplayName("assertEquals assertTrue的使用")
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion message is now the last parameter.");
assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
assertTrue(4 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
@DisplayName("assertAll的使用")
void groupedAssertions() {
// 断言组中的多个断言
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// 在代码块中,如果断言失败,则将跳过同一块中的后续代码。
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// 仅在前一个断言有效时执行
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
},
() -> {
// 分组断言,独立于名字断言的结果进行处理。
String lastName = person.getLastName();
assertNotNull(lastName);
// 仅在前一个断言有效时执行
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// 断言成功
assertTimeout(ofMinutes(2), () -> {
// 模拟任务耗时不超过2分钟
});
}
@Test
void timeoutNotExceededWithResult() {
//断言成功,并返回提供的对象 actualResult
String actualResult = assertTimeout(ofMinutes(2), () -> "a result");
assertEquals("a result", actualResult);
}
@Test
void timeoutExceeded() {
//断言失败,错误消息类似:execution exceeded timeout of 10 ms by 94 ms
//超时时间为10ms 但执行了94ms
assertTimeout(ofMillis(10), () -> {
// 模拟任务耗时超过10毫秒
Thread.sleep(100);
});
}
@Test
@DisplayName("断言可执行代码如果超过指定时间会被抢占中止")
void timeoutExceededWithPreemptiveTermination() {
// 在10ms后程序会中止
assertTimeoutPreemptively(ofMillis(10), () -> {
// 模拟任务耗时超过10毫秒
Thread.sleep(100);
});
}
@Data
@AllArgsConstructor
static class Person {
private String firstName;
private String lastName;
}
}
assertTimeoutPreemptively() 和 assertTimeout() 的区别为: 两者都是断言超时,前者在指定时间没有完成任务就会立即返回断言失败;后者会在任务执行完毕之后才返回。
3.2.4 assertThrows()
该断言方法有助于断言用于验证一段代码中是否抛出期望的异常。
- 如果没有引发异常,或者引发了不同类型的异常,则此方法将失败;
- 它遵循继承层次结构,因此如果期望的类型是Exception,而实际是RuntimeException,则断言也会通过。
assertThrow()也有三种有用的重载方法:
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable)
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, String message)
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, Supplier<String> messageSupplier)
- 第一个参数为异常类
- 第二个为函数式接口参数,跟 Runnable 接口相似,不需要参数,也没有返回,并且支持 Lambda表达式
我们通过以下测试代码来看一下这个断言:
public class AssertThrowTest {
@Test
public void testAssertThrows() {
assertThrows(ArithmeticException.class, () -> divide(1, 0));
}
@Test
public void testAssertThrowsWithMessage() {
assertThrows(IOException.class, () -> divide(1, 0), "除以0啦!!!");
}
@Test
public void testAssertThrowsWithMessageSupplier() {
assertThrows(Exception.class, () -> divide(1, 0), () -> "除以0啦!!!");
}
private int divide(int a, int b) {
return a / b;
}
}
- testAssertThrows : 该用例期望抛出ArithmeticException,因为1除以0抛出的异常就是ArithmeticException,所以该用例通过;
- testAssertThrowsWithMessage:该用例期望抛出IOException,ArithmeticException并不是IOException的子类,所以测试未通过;
- testAssertThrowsWithMessageSupplier:该用例期望抛出Exception,ArithmeticException是Exception的子类,所以测试通过。
使用Supplier messageSupplier参数的断言相比使用String message参数断言有一个优点,就是只有在断言不通过的时候,才会构造字符串对象。