Junit5 的基本使用

449 阅读15分钟

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 4JUnit 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);
    }
}

执行结果

image.png

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("执行测试");
}

image.png

除了指定重复次数外,还可以通过@RepeatedTest注解的name属性为每次重复配置自定义显示名称。此外,显示名称可以是模式,由静态文本和动态占位符的组合而成。目前支持以下占位符:

  • {displayName}: @RepeatedTest方法的显示名称
  • {currentRepetition}: 当前重复次数
  • {totalRepetitions}: 重复的总次数

image.png

测试例子

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() {
        // ...
    }
}

执行结果:

image.png

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");
       }
   }
}

image.png

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);
}

image.png

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);
}

image.png

@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()))
                        ))
                )));
    }
}

执行结果:

image.png

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");
}

image.png

@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() 的区别为: 两者都是断言超时,前者在指定时间没有完成任务就会立即返回断言失败;后者会在任务执行完毕之后才返回。

image.png

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;
    }
}

image.png

  • testAssertThrows : 该用例期望抛出ArithmeticException,因为1除以0抛出的异常就是ArithmeticException,所以该用例通过;
  • testAssertThrowsWithMessage:该用例期望抛出IOException,ArithmeticException并不是IOException的子类,所以测试未通过;
  • testAssertThrowsWithMessageSupplier:该用例期望抛出Exception,ArithmeticException是Exception的子类,所以测试通过。

使用Supplier messageSupplier参数的断言相比使用String message参数断言有一个优点,就是只有在断言不通过的时候,才会构造字符串对象。