SpringBoot第二十二天 - JUnit单元测试

SpringBoot - JUnit单元测试

本节学习SpringBoot使用Junit进行单元测试。

1. JUnit概述

1.1 JUnit简介

JUnit 5 is the next generation of JUnit. The goal is to create an up-to-date foundation for developer-side testing on the JVM. This includes focusing on Java 8 and above, as well as enabling many different styles of testing.
JUnit 5 is the result of JUnit Lambda and its crowdfunding campaign on Indiegogo.

About - JUnit 5

JUnit是一个Java语言的单元测试框架。它由Kent Beck和Erich Gamma建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中最为成功的一个。JUnit有它自己的JUnit扩展生态圈。多数Java的开发环境都已经集成了JUnit作为单元测试的工具。
JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

junit - 百度百科

1.2 JUnit5特性

作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成:

Unlike previous versions of JUnit, JUnit 5 is composed of several different modules from three different sub-projects.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

The JUnit Platform serves as a foundation for launching testing frameworks on the JVM. It also defines the TestEngine API for developing a testing framework that runs on the platform. Furthermore, the platform provides a Console Launcher to launch the platform from the command line and a JUnit 4 based Runner for running any TestEngine on the platform in a JUnit 4 based environment. First-class support for the JUnit Platform also exists in popular IDEs (see IntelliJ IDEA, Eclipse, NetBeans, and Visual Studio Code) and build tools (see Gradle, Maven, and Ant).

JUnit Jupiter is the combination of the new programming model and extension model for writing tests and extensions in JUnit 5. The Jupiter sub-project provides a TestEngine for running Jupiter based tests on the platform.

JUnit Vintage provides a TestEngine for running JUnit 3 and JUnit 4 based tests on the platform. It requires JUnit 4.12 or later to be present on the class/module path.

What is JUnit 5? - JUnit 5 User Guide

JUnit Platform:JUnit Platform是在JVM上启动测试框架的基础,不仅支持JUnit自制的测试引擎,其他测试引擎也都可以接入。

JUnit Jupiter:JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在JUnit Platform上运行。

JUnit Vintage:由于JUnit已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit 4.x,JUnit 3.x的测试引擎。

1.3 SpringBoot整合JUnit

SpringBoot 2.2.0版本开始引入JUnit5作为单元测试默认库。SpringBoot 2.4以上移除了默认对JUnit Vintage的依赖,如果需要兼容JUnit4需要自行引入。

JUnit 5’s Vintage Engine Removed from spring-boot-starter-test

If you upgrade to Spring Boot 2.4 and see test compilation errors for JUnit classes such as org.junit.Test, this may be because JUnit 5’s vintage engine has been removed from spring-boot-starter-test. The vintage engine allows tests written with JUnit 4 to be run by JUnit 5. If you do not want to migrate your tests to JUnit 5 and wish to continue using JUnit 4, add a dependency on the Vintage Engine, as shown in the following example for Maven:

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Spring Boot 2.4 Release Notes - GitHub/Spring-projects

2. 单元测试的基本使用

2.1 导入单元测试模块starter

Maven引入SpringBoot单元测试模块starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <!-- 使用SpringBoot的版本管理机制 -->
</dependency>

2.2 编写并分析简单测试类

现有业务层需要对其CRUD功能进行测试:

@Service
public class UserServiceImpl implements UserService {
    private UserMapper mapper;
    @Autowired
    public void setMapper(UserMapper mapper) {
        this.mapper = mapper;
    }
    /**
     * 根据ID获取用户记录
     * 
     * @param id ID
     * @return 用户记录
     */
    public User getUserById(int id) {
        return mapper.selectUserById(id);
    }
}

根据上述代码编写简单测试类测试其CRUD功能:

@SpringBootTest
@Slf4j
public class TestUserServiceImpl {
    private UserService service;
    @Autowired
    public void setService(UserService service) {
        this.service = service;
    }
    @Test
    public void testGetUserById() {
        log.info("测试参数:{},测试结果:{}", 1, service.getUserById(1));
    }
}

运行测试方法,控制台打印结果:

2021-10-12 10:32:25.709  INFO 6420 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-10-12 10:32:25.927  INFO 6420 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-10-12 10:32:25.979  INFO 6420 --- [           main] pers.dyj123.test.TestUserServiceImpl     : 测试参数:1,测试结果:Account(id=1, name=张三, age=24)

简单分析测试类:

  • 测试类需用@SpringBootTest注解,表明这是一个SpringBoot的测试类
  • @Test测试方法可直接运行,无需编写main方法;
  • SpringBoot的测试类可以使用Spring功能(使用@Autowired自动装配功能等);
  • 测试类及测试方法命名规范:测试类命名需按照Test+需要测试的类名,测试方法需按照test+需要测试的方法名

3. JUnit5常用注解

JUnit5提供了非常多的常用注解,JUnit5的注解相较JUnit4的注解有所变化:

注解 功能
@Test 表示方法是测试方法。但是与JUnit4的@Test不同,它的职责非常单一不能声明任何属性,
拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest 表示测试方法是参数化测试,后半部分将详细介绍
@RepeatedTest 表示测试方法可重复执行
@DisplayName 为测试类或者测试方法设置展示名称
@BeforeEach 标注此注解的方法会在每个测试方法执行之前执行
@AfterEach 标注此注解的方法会在每个测试方法执行之后执行
@BeforeAll 标注此注解的方法会在所有测试方法执行之前执行
@AfterAll 标注此注解的方法会在所有测试方法执行之后执行
@Tag 表示单元测试类别,类似JUnit4中的@Catagories
@Disabled 表示测试类或者测试方法不执行,类似JUnit4中的@Ignore
@Timeout 表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith 为测试类或者测试方法提供扩展类引用

关于更多注解的介绍与使用方式可以参考 JUnit官方文档

3.1 @DisplayName

@DisplayName注解用于给测试类或者测试方法设置名称,参数如下:

参数 类型 作用
value String 指定展示名称

使用方式:

@SpringBootTest
@DisplayName("注解@DisplayName测试类")
public class TestDisplayName {
    @Test
    @DisplayName("注解@DisplayName测试方法")
    public void testDisplayName() {
        System.out.println("测试范例");
    }
}

运行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

3.2 @BeforeEach和@AfterEach

@BeforeEach@AfterEach注解用于在每个测试方法执行前和执行后执行某些方法,此注解没有参数。

使用方式:

@SpringBootTest
public class TestEach {
    @BeforeEach
    public void testBeforeEach() {
        System.out.println("执行了@BeforeEach方法");
    }
    @AfterEach
    public void testAfterEach() {
        System.out.println("执行了@AfterEach方法");
    }
    @Test
    public void test1() {
        System.out.println("执行了test1方法");
    }
    @Test
    public void test2() {
        System.out.println("执行了test2方法");
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

3.3 @BeforeAll和@AfterAll

@BeforeAll@AfterAll注解用于在所有测试方法执行前和执行后执行某些方法,此注解没有参数。

需要注意的是建议静态方法使用此注解:

Such methods are inherited (unless they are hidden or overridden) and must be static (unless the “per-class” test instance lifecycle is used).

2.1. Annotations/@BeforeAll - JUnit 5 User Guide

使用方式:

@SpringBootTest
public class TestAll {
    @BeforeAll
    static void testBeforeAll() {
        System.out.println("执行了@BeforeAll方法");
    }
    @AfterAll
    static void testAfterAll() {
        System.out.println("执行了@AfterAll方法");
    }
    @Test
    public void test1() {
        System.out.println("执行了test1方法");
    }
    @Test
    public void test2() {
        System.out.println("执行了test2方法");
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

3.4 @Disabled

@Disabled注解标注的测试方法将会被禁用,跳过测试不会执行;被标注的测试类的所有测试方法将不会执行。参数如下:

参数 类型 作用
value String 指定被禁用的原因

使用方式:

@SpringBootTest
public class TestDisabled {
    @Test
    @Disabled
    public void test1() {
        System.out.println("执行了test1方法");
    }
    @Test
    public void test2() {
        System.out.println("执行了test2方法");
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

3.5 @Timeout

@Timeout注解标注的测试方法,如果执行超过设置的时间,就会报错结束。参数如下:

参数 类型 作用
value long 指定超时的时间数字
units TimeUnit 指定超时的时间单位

使用方式:

@SpringBootTest
public class TestTimeout {
    @Test
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) // 限时500毫秒
    public void testTimeout() throws InterruptedException {
        Thread.sleep(600); // 线程休眠600毫秒
        System.out.println("执行了testTimeout方法");
    }
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

可以看到如果超时过久,方法就会被强制停止运行。

3.6 @ExtendWith

@ExtendWith注解用于在测试中添加或使用其他测试框架的功能扩展,和JUnit4中的@RunWith注解效果相同,参数如下:

参数 类型 作用
value Class<? extends Extension>[] 指定需要添加的类

在JUnit4中我们想要使用Spring功能,需要在测试类标注@RunWith(SpringJUnit4ClassRunner.class)
在SpringBoot 2.4以上版本中,JUnit5使用Spring功能,我们只需在测试类标注@SpringBootTest注解即可。

分析@SpringBootTest注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class) // 使用@ExtendWith添加Spring功能扩展
public @interface SpringBootTest {
    // ... 省略部分代码
}

发现@SpringBootTest注解已经使用了@ExtendWith为我们添加了Spring的功能扩展,所以测试类只需标注@SpringBootTest注解即可使用Spring功能。

3.7 @RepeatedTest

@RepeatedTest注解可以使测试方法重复执行,可以设置重复执行的次数。参数如下:

参数 类型 作用
value int 执行的循环次数
name String 设置循环执行时的展示名称

JUnit提供了2种展示名称:

  • SHORT_DISPLAY_NAME(默认使用):“repetition {currentRepetition} of {totalRepetitions}”
  • LONG_DISPLAY_NAME:"{displayName} :: repetition {currentRepetition} of {totalRepetitions}"

上述三个占位符:{displayName}是使用@DisplayName注解设置的展示名称,{currentRepetition}是当前循环次数,{totalRepetitions}是总循环次数。
可以使用上述三个占位符自定义循环执行的展示名称。

使用方式:

@SpringBootTest
public class TestRepeatedTest {
    @RepeatedTest(value = 3, name = RepeatedTest.LONG_DISPLAY_NAME)
    @DisplayName("注解@RepeatedTest测试方法")
    public void testRepeatedTest() {
        System.out.println("执行了testRepeatedTest方法");
    }
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

4. 断言机制

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证,检查业务逻辑返回的数据是否合理,断言机制的特性:

  • JUnit5内置的断言方法都是org.junit.jupiter.api.Assertions中的静态方法
  • 每个断言方法都有message参数,可以指定自定义错误信息。
  • 一旦测试方法中的断言判断失败,则此断言之后的代码都不会运行,测试方法测试失败。

下面将简单介绍部分断言方法,关于其他断言方法的使用方式和更多信息,可以参照 JUnit5官方文档

4.1 简单断言

下列方法用于对单个值进行简单的验证:

方法 作用
assertEquals 判断两个对象是否等价(相当于equal)
assertNotEquals 判断两个对象是否不等价(相当于!equal)
assertSame 判断两个对象引用是否指向同一个对象(相当于==)
assertNotSame 判断两个对象引用是否指向同一个对象(相当于!=)
assertTrue 判断给定的布尔值是否为true
assertFalse 判断给定的布尔值是否为false
assertNull 判断给定的对象引用是否为null
assertNotNull 判断给定的对象引用是否不为null

使用assertEquals(int expected, int actual)方法测试简单断言机制:

@Test
@DisplayName("测试简单断言")
public void testAssertions() {
    int sum = 4 + 5; // 模拟业务逻辑
    Assertions.assertEquals(10, sum); // 进行断言检查
    System.out.println("执行了testSimpleAssertions方法");
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

4.2 数组断言

JUnit提供了数组断言方法(部分):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfaJDE6k-1634041079319)(H:\Documents Library\IDEA Projects\READMEimage\springboot_test_junit_断言机制_数组断言_数组断言方法展示.JPG)]

使用assertArraysEqual(int[] expected, int[] actual, String message)方法测试数组断言机制:

@Test
@DisplayName("测试数组断言")
public void testArrayAssertions() {
    Assertions.assertArrayEquals(new int[]{1, 2}, new int[]{2, 1}, "数组不匹配");
    System.out.println("执行了testArrayAssertions方法");
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

数组断言原理:

// org.junit.jupiter.api.AssertArrayEquals.assertArrayEquals(int[], int[], java.util.Deque<java.lang.Integer>, java.lang.Object)
// AssertArrayEquals.java Line:233~247

private static void assertArrayEquals(int[] expected, int[] actual, Deque<Integer> indexes,
        Object messageOrSupplier) {

    if (expected == actual) {
        return;
    }
    assertArraysNotNull(expected, actual, indexes, messageOrSupplier);
    assertArraysHaveSameLength(expected.length, actual.length, indexes, messageOrSupplier);

    for (int i = 0; i < expected.length; i++) { // 使用for循环将两个数组的元素使用!=一一比对,如果有某个元素不匹配则判断失败
        if (expected[i] != actual[i]) {
            failArraysNotEqual(expected[i], actual[i], nullSafeIndexes(indexes, i), messageOrSupplier);
        }
    }
}

4.3 组合断言

JUnit提供了组合断言方法assertAll(String heading, Executable... executables),可以将多个断言组合成一个断言,如果有单个断言失败,则都失败。参数如下:

  • heading:指定自定义错误信息;
  • executablesExecutable是一个函数式接口,可以使用lambda表达式编写要测试的代码。

使用方式:

@Test
@DisplayName("测试组合断言")
public void testAllAssertions() {
    Assertions.assertAll(
            "组合断言判断失败",
            () -> Assertions.assertEquals(1.0, 2.0),
            () -> Assertions.assertEquals("测试组合断言", "测试组合断言")
    );
    System.out.println("执行了testAllAssertions方法");
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

4.4 异常断言

JUnit提供了异常断言方法assertThrowsassertDoesNotThrow,分别判断是否抛出特定异常和判断是否没有抛出任何异常。

使用方式:

@Test
@DisplayName("测试异常断言assertThrows")
public void testAssertThrows() {
    Assertions.assertThrows(
            ArithmeticException.class,
            () -> {
                int i = 10 / 1;
            }
    );
    System.out.println("执行了testAssertThrows方法");
}
    
@Test
@DisplayName("测试异常断言assertDoesNotThrow")
public void testAssertDoesNotThrow() {
    Assertions.assertDoesNotThrow(
            () -> {
            throw new NullPointerException();
            }
    );
    System.out.println("执行了testAssertDoesNotThrow方法");
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

SpringBoot第二十二天 - JUnit单元测试

4.5 超时断言

JUnit提供了超时断言方法assertTimeout(Duration timeout, Executable executable),执行方法如果超时将断言失败。

使用方式:

@Test
@DisplayName("测试超时断言")
public void testTimeoutAssertions() {
    Assertions.assertTimeout(
            Duration.ofMillis(500),
            () -> {
                Thread.sleep(600);
            }
    );
    System.out.println("执行了testTimeoutAssertions方法");
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

4.6 失败断言

JUnit提供了失败断言方法fail,使用此断言方法的测试方法必定断言失败,常用于检测不能被调用的方法被非法调用或者某些条件判断错误。

使用方式:

@Test
@DisplayName("测试失败断言")
public void testFailAssertions() {
    Assertions.fail("此方法不能被调用");
    System.out.println("执行了testFailAssertions方法");
}

执行测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

5. 前置条件

JUnit5中的前置条件(assumptions(假设))类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止(相当于标注了@Disabled注解)。

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

JUnit5内置的前置条件方法都是org.junit.jupiter.api.Assumptions中的静态方法

SpringBoot第二十二天 - JUnit单元测试

使用方式:

@SpringBootTest
public class TestAssumptions {
    @Test
    @DisplayName("测试前置条件为true")
    public void testAssumeTrue() {
        Assumptions.assumeTrue(false, "前置条件需为true"); // 前置条件未满足
        System.out.println("执行了testAssumeTrue方法");
    }
    @Test
    @DisplayName("测试前置条件为false")
    public void testAssumeFalse() {
        Assumptions.assumeFalse(true, "前置条件需为false"); // 前置条件未满足
        System.out.println("执行了testAssumeFalse方法");
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

不满足前置条件方法的测试方法将会被直接跳过(不是失败),不予执行。

6. 嵌套测试

JUnit支持在测试类内部再添加测试类,内部测试类使用@Nested注解标注。

  • 外部测试类的测试方法执行时,内部测试类标注@BeforeEach@AfterEach等注解的方法不会执行
  • 内部测试类的测试方法执行时,内部测试类标注@BeforeEach@AfterEach等注解的方法正常执行

举个例子,编写测试方法:

@SpringBootTest
public class TestNested {
    List<Integer> list; // 声明一个List集合
    @BeforeEach
    public void outerBeforeEach() {
        list = new ArrayList<>();
        // 外部测试类的BeforeEach方法将List集合初始化
    }
    @Test
    @DisplayName("外部测试类的测试方法1")
    public void outerTest1() {
        Assertions.assertNull(list);
        // 外部测试类的BeforeEach方法已经将List集合初始化,所以不为null,断言失败
    }
    @Test
    @DisplayName("外部测试类的测试方法2")
    public void outerTest2() {
        Assertions.assertTrue(list.isEmpty());
        // 外部测试类并没有给List集合放入元素,执行此方法时内部测试类的BeforeEach方法还未执行,所以List集合为空,断言成功
    }
    @Nested
    class TestNestedInner {
        @BeforeEach
        public void innerBeforeEach() {
            list.add(1);
            // 内部测试类的BeforeEach方法给List集合放入一个元素
        }
        @Test
        @DisplayName("内部测试类的测试方法1")
        public void innerTest1() {
            Assertions.assertNull(list);
            // 外部测试类的BeforeEach方法已经将List集合初始化,所以不为null,断言失败
        }
        @Test
        @DisplayName("内部测试类的测试方法2")
        public void innerTest2() {
            Assertions.assertTrue(list.isEmpty());
            // 内部测试类的BeforeEach方法已经给List集合放入一个元素,所以List集合不为空,断言失败
        }
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

7. 参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码:

注解 作用
@ValueSource 为参数化测试指定入参来源,支持基本类型以及String类型和Class类型
@NullSource 为参数化测试提供一个null的入参
@EnumSource 为参数化测试提供一个枚举类型的入参
@CsvFileSource 读取指定CSV文件内容作为参数化测试的入参
@MethodSource 读取指定方法的返回值作为参数化测试的入参(注意方法返回值需要是一个流)

需要进行参数化测试的测试方法需用@ParameterizedTest注解标注,而且需要使用上述注解提供入参来源。

关于其他入参来源及详细说明可以参考 JUnit官方文档

使用@ValueSource@MethodSource注解举例,编写测试方法:

@SpringBootTest
public class TestParameterizedTest {
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3}) // 传入一个数组作为入参
    public void testValueSource(int param) {
        System.out.println("执行了testValueSource方法,第" + param + "次执行");
    }
    @ParameterizedTest
    @MethodSource("paramStream") // 使用paramStream返回的流作为入参
    public void testMethodSource(int param) {
        System.out.println("执行了testMethodSource方法,第" + param + "次执行");
    }
    static Stream<Integer> paramStream() {
        return Stream.of(1, 2, 3);
    }
}

执行所有测试方法,测试结果:

SpringBoot第二十二天 - JUnit单元测试

上一篇:1.图标库和安装


下一篇:10 窗体排列