Java Junit5 使用小结

发布时间 2023-11-21 17:41:58作者: 进击的davis

在我们的日常开发中,代码一边编码一边自测是常有的事,做好单元测试也是一名开发应该掌握的技能,不说测试搞得多么强,至少会基本的,会功能测试,会性能测试。今天来学习下 单元测试。

1.JUnit5介绍

现在主要版本是 JUnit5,所以后面的内容也都是基于 JUnit5 做相关的介绍。JUnit5 是 JUnit 单元测试框架的重大升级,需要运行在 Java8 以上的环境。

JUnit5可以理解为是由三个不同而子项目构成:

  • 1.JUnit Platform,用于JVM上启动测试框架的基础服务,提供命令行,IDE和构建工具等方式执行测试的支持。
  • 2.JUnit Jupiter,包含 JUnit 5 新的编程模型和扩展模型,主要就是用于编写测试代码和扩展代码。
  • 3.JUnit Vintage,用于在JUnit 5 中兼容运行 JUnit3.x 和 JUnit4.x 的测试用例。

JUnit5目前的主要特性:

  • 提供全新的断言和测试注解,支持测试类内嵌
  • 更丰富的测试方式:支持动态测试,重复测试,参数化测试等
  • 实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖
  • 提供对 Java 8 的支持,如 Lambda 表达式,Sream API等。

2.测试环境

  • Java 21
  • Junit 5.10.0

3.Maven依赖

本篇学习内容涉及的依赖如下:

    <!-- 基础测试 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.10.0</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.10.0</version>
    </dependency>

    <!-- 带参测试 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-params</artifactId>
        <version>5.10.0</version>
    </dependency>

4.典型注解

@BeforeAll

修饰 static 方法,定义整个测试类在开始前的操作,比如一些初始化的操作。

@AfterAll

修饰 static 方法,定义整个测试类在结束时的操作,比如一些清理工作。

@BeforeEach && @AfterEach

标注在每个测试用例方法,表示方法开始前或结束时的执行,负责该测试用例所需要的的运行环境的准备和销毁。

@Test

表示这个测试方法是一个测试用例。

由上面的这些注解,我们可以做出如下流程来表示单元测试的完整过程:
image.png

@DisplayName

可以加在类上,也可以加在测试方法上,相当于一个显示名。

@Disable

相当于禁用当前的测试方法,测试时就会忽略该方法。

@RepeatedTest

表示该方法需要重复运行,具体几次,可以通过参数传入。

基于上面的介绍,我们写一个简单的类测试:

package org.example;

import org.junit.jupiter.api.*;

import java.time.Duration;

@DisplayName("我的第一个测试用例")
public class MyFirstTestCaseTest {
    @BeforeAll
    public static void init() {
        System.out.println("初始化数据");
    }

    @AfterAll
    public static void cleanup() {
        System.out.println("清理数据");
    }

    @BeforeEach
    public void tearUp() {
        System.out.println("当前测试方法开始");
    }

    @AfterEach
    public void tearDown() {
        System.out.println("当前测试方法结束");
    }

    @DisplayName("我的第一个测试")
    @Test
    public void testFirstTest() {
        System.out.println("我的第一个测试开始");
    }

    @DisplayName("我的第二个测试")
    @Test
    public void testSecondTest() {
        System.out.println("我的第二个测试开始");
    }

    @DisplayName("我的第三个测试")
    @Disabled
    @Test
    public void testThirdTest() {
        System.out.println("我的第三个测试开始测试");
    }

    @DisplayName("我的第四个测试-重复测试")
    @RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
    public void repeatedTest() {
        System.out.println("正在执行重复测试");
    }
}    

5.断言

在测试方法中,我们常常是给定一个预期的值和测试的结果值作比较,看对应的结果怎样,由此就由相关断言:

  • 断言相等,assertEqual
  • 断言不等,assertNotEqual
  • 多个断言,assertAll
  • 断言空或非空,assertNull/assertNotNull
  • 断言 true或false,assertTrue/assertFalse
  • 超时断言,assertTimeout/assertTimeoutPreemptively
  • 断言实例,assertInstanceOf
  • 断言异常,assertThrows

由于断言类包含内容很多,每个方法实际很多重载,这里仅挑选几个重点说说,其他都是类似的。

demo

@DisplayName("我的第五个测试-单个断言")
@Test
public void testSingleAssertion() {
    Integer num = 1;
    Assertions.assertEquals(num, 1);
}

@DisplayName("我的第六个测试-多断言")
@Test
public void testGroupAssertions() {
    int[] nums = {0, 1, 2, 3, 4};
    Assertions.assertAll("nums",
            () -> Assertions.assertEquals(nums[0], 0),
            () -> Assertions.assertEquals(nums[1], 1),
            () -> Assertions.assertEquals(nums[2], 2),
            () -> Assertions.assertEquals(nums[3], 3),
            () -> Assertions.assertEquals(nums[4], 4)
        );
}

@DisplayName("我的第七个测试-超时操作")
@Test
public void testShouldCompleteInOneSecond() {
    // 无法做到时间的精确匹配
    Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> Thread.sleep(999));
}

@DisplayName("我的第八个测试-异常测试")
@Test
public void testAssertThrowsException() {
    String str = null;
    // str 作为传入参数,会报非法传参异常,所以可以正常断言到
    Assertions.assertThrows(IllegalArgumentException.class, () -> {
        Integer.valueOf(str);
    });
}

6.带参数的测试方法

有的时候我们需要一些带参数的测试方法,比如一次测试一个参数不够,那就来一组参数,或者有的传参需要多个参数怎么办。

这里我们用到了 Junit5的 params 包提供功能,这也是上面我们在依赖中添加的依赖项。

在有带参数的单元测试中主要介绍几个常用的,其他感兴趣可以看看源码。

@ParameterizedTest

此处可以用来代替 @Test 注解,一样的功效。

@ValueSource

指定我们的传入的这一组参数,可以是:

  • ints
  • strings
  • classes等

包含类型覆盖基本类型。

@CsvSource

规定了传入的多个入参的组合形式,默认用“,”分隔。

下面是个简单的 demo:

package org.example;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

public class ParameterUnitTest {
    @ParameterizedTest
    @ValueSource(ints = {2, 4, 8, 10})
    public void testIsEvenNumber(int num) {
        Assertions.assertEquals(0, num % 2);
    }

    @ParameterizedTest
    @ValueSource(strings = {"Effective Java", "C Plus Plus"})
    public void testPrintTitle(String title) {
        System.out.println(title);
    }

    // 多参数
    @ParameterizedTest
    @CsvSource({"1,One", "2,Two"})
    public void testDataFromCSV(long id, String name) {
        System.out.printf("id: %d, name: %s\n", id, name);
    }
}

以上这些都是一些基本用法,有的时候我们需要依赖其他类,所以就会有 mock打桩 的需求,这是后话了,掌握这些基本用法能覆盖相当的开发自测了。

参考: