当前位置: 首页 > news >正文

[springboot系列] 探秘 JUnit 5:现代 Java 单元测试利器

介绍

JUnit 5 是一个用于 Java 编程语言的单元测试框架,它是 JUnit 框架的第五个版本,与 JUnit 4 相比,JUnit 5 提供了许多改进和新特性,包括更好的扩展性、灵活性和对现代 Java 特性的支持。

JUnit 5 由三个主要的子模块组成:

  • JUnit Platform:这是 JUnit 5 的基础,它提供了一个测试引擎的接口,可以运行多种测试框架。JUnit Platform 不仅支持 JUnit 5,还支持 JUnit 4 和其他测试框架(比如 TestNG)。它也定义了如何启动测试、报告结果等基础设施。
  • JUnit Jupiter:这是 JUnit 5 的核心模块,也是 JUnit 5 中最常用的部分,包含了用于编写和运行测试的 API 和注解。
  • JUnit Vintage:这个模块为 JUnit 4 和早期版本的测试提供兼容性支持,它允许在同一个项目中同时运行 JUnit 4 和 JUnit 5 的测试。


常用注解 Annotations

@Test

在 JUnit 5 中,@Test 用于标记一个普通的测试方法,表示该方法应该被测试框架识别并执行。

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;public class MyTest {@Testvoid testAddition() {int result = 1 + 1;assertEquals(2, result);}
}

在这个例子中,testAddition 方法被 @Test 注解标记为一个测试方法。当运行这个测试类时,JUnit 会自动执行 testAddition 方法并验证 assertEquals(2, result) 是否成立。如果条件不满足,测试将失败。


生命周期

在 JUnit 5 中,@BeforeAll@AfterAll@BeforeEach @AfterEach 等生命周期注解可以在测试方法的前后执行一些必要的初始化或清理工作,它们的作用域是所在测试类。

@BeforeAll

  • 作用:标记的方法会在所有测试方法执行之前执行一次,适用于需要在所有测试方法运行之前进行一次性初始化的场景。

  • 要求:被 @BeforeAll 注解的方法必须是 static,因为它是由 JUnit 框架在类加载时直接调用的,并不依赖于类的实例。

@AfterAll

  • 作用:标记的方法会在所有测试方法执行完毕之后执行一次,适用于需要在所有测试方法完成之后进行一些清理操作的场景。

  • 要求:和 @BeforeAll 一样,@AfterAll 标记的方法必须是 static,因为它也是由 JUnit 在类的生命周期结束时调用的。

@BeforeEach

  • 作用:标记的方法会在每个测试方法执行之前执行一次,适用于每个测试方法需要单独的初始化工作,例如每个测试方法都要创建一个新的数据库连接。

@AfterEach

  • 作用:标记的方法会在每个测试方法执行之后执行一次,适用于需要在每个测试方法后进行清理的操作,例如关闭数据库连接、清理测试环境。

示例:

import org.junit.jupiter.api.*;class LifecycleTest {// 在所有测试方法执行前执行一次@BeforeAllstatic void beforeAll() {System.out.println("Before All - 执行一次");}// 在所有测试方法执行后执行一次@AfterAllstatic void afterAll() {System.out.println("After All - 执行一次");}// 在每个测试方法执行前执行@BeforeEachvoid beforeEach() {System.out.println("Before Each - 每个测试方法前执行");}// 在每个测试方法执行后执行@AfterEachvoid afterEach() {System.out.println("After Each - 每个测试方法后执行");}@Testvoid test1() {System.out.println("Test 1 - 执行测试 1");}@Testvoid test2() {System.out.println("Test 2 - 执行测试 2");}
}

输出:

Before All - 执行一次
Before Each - 每个测试方法前执行
Test 1 - 执行测试 1
After Each - 每个测试方法后执行
Before Each - 每个测试方法前执行
Test 2 - 执行测试 2
After Each - 每个测试方法后执行
After All - 执行一次

@ParameterizedTest

在传统的单元测试中,每个测试方法通常都是针对特定的输入值进行测试。使用 @ParameterizedTest 可以为同一个测试方法提供不同的输入数据,从而更全面地验证代码的正确性。

如何使用 @ParameterizedTest

@ParameterizedTest 需要与 参数源(Source)一起使用,参数源提供了不同的输入数据集合。JUnit 5 提供了几种常用的参数源,包括 @ValueSource、@CsvSource、@MethodSource、@EnumSource 等。

1. 使用 @ValueSource 传递单个参数

@ValueSource 是最常见的参数源之一,它允许为测试方法传递单一类型的多个值。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.ParameterizedTest;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.provider.ValueSource;public class ParameterizedTestExample {@ParameterizedTest@ValueSource(ints = {1, 2, 3, 4, 5})  // 提供参数源:一个整数数组void testWithValues(int number) {Assertions.assertTrue(number > 0);  // 对每个输入参数执行相同的断言}
}

在这个示例中,@ParameterizedTest 与 @ValueSource 结合,提供了多个整数值。每次执行 testWithValues 方法时,JUnit 5 会用不同的整数值执行测试,验证它们是否大于 0。

@ValueSource 支持的类型包括但不限于以下:

  • ints
  • floats
  • booleans
  • strings
  • classes

值得注意的是,如果需要传递对象型的参数,可以传递 json 字符串,然后反序列化成对象。

2. 使用 @CsvSource 传递多个参数

@CsvSource 允许以CSV 格式(逗号分隔的值)传递多个参数,这对需要多个输入的测试非常有用。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.ParameterizedTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.provider.CsvSource;public class ParameterizedTestCsvSource {@ParameterizedTest@CsvSource({"1, 2, 3","4, 5, 9",  // 两个整数和预期结果"10, 20, 30"})void testSum(int a, int b, int expectedSum) {Assertions.assertEquals(expectedSum, a + b);  // 验证 a + b 是否等于预期结果}
}

在这个例子中,@CsvSource 提供了三组数据,每一组数据有两个整数值和一个期望的结果。JUnit 5 会为每一组数据执行一次 testSum 方法。

3. 使用 @MethodSource 从方法中提供参数

@MethodSource 允许从一个静态方法中提供参数。这个方法必须返回一个包含多个测试数据的 Stream、Iterable 或 Object[]。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.ParameterizedTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.provider.MethodSource;import java.util.stream.Stream;public class ParameterizedTestMethodSource {static Stream<String> stringProvider() {return Stream.of("apple", "banana", "cherry");}@ParameterizedTest@MethodSource("stringProvider")void testWithMethodSource(String fruit) {Assertions.assertNotNull(fruit);  // 确保输入的每个字符串都是非空的}
}

在这个示例中,stringProvider() 方法返回了一个 Stream,这个 Stream 会被用作 testWithMethodSource 的参数源。每次运行时,JUnit 5 会用 apple、banana 和 cherry 分别执行 testWithMethodSource 方法。

4. 使用 @EnumSource 来测试枚举值

@EnumSource 是一个特别有用的注解,用于枚举类型的参数化测试。它允许为测试方法传递一个枚举类的所有值,或者指定枚举值的子集。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.ParameterizedTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.provider.EnumSource;public class ParameterizedTestEnumSource {enum Fruit {APPLE, BANANA, CHERRY}@ParameterizedTest@EnumSource(Fruit.class)  // 传入枚举类的所有值void testWithEnumSource(Fruit fruit) {Assertions.assertNotNull(fruit);  // 确保传入的枚举值非空}
}

在这个示例中,@EnumSource(Fruit.class) 会让 JUnit 5 将 Fruit 枚举中的每个值(APPLE、BANANA、CHERRY)传递给 testWithEnumSource 方法。


@Tag

@Tag 注解的用途

  • 测试分类:可以为测试方法或测试类指定一个或多个标签,进而根据标签来组织和筛选测试。
  • 条件化执行:可以根据不同的标签在测试执行时选择性地执行某些测试,而跳过其他测试,例如“快速测试”或“性能测试”。

在测试类或方法上使用 @Tag

@Tag 可以应用于 测试类测试方法 上,指定一个或多个标签。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;@Tag("smoke")
public class MultiTagTest {@Test@Tag("fast")@Tag("smoke")void fastSmokeTest() {// 这个测试同时是 "fast" 和 "smoke" 类型的测试}
}

使用构建工具(Maven、Gradle)筛选标签

在构建工具(如 Maven 或 Gradle)中,可以根据标签来筛选哪些测试需要执行。

<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0-M5</version><configuration><includes><include>@fast</include> <!-- 只运行标签为 fast 的测试 --></includes></configuration>
</plugin>

@DisplayName

在 JUnit 5 中,@DisplayName 注解用来为测试类、或测试方法指定一个 自定义的显示名称。这个显示名称会在测试报告中展示,以使得测试结果更具可读性和易于理解。

import org.junit.jupiter.api.*;@DisplayName("测试生命周期方法的注解")
class DisplayNameTest {@BeforeEachvoid setUp() {System.out.println("BeforeEach - 每个测试前执行");}@Test@DisplayName("测试方法 1:加法测试")void testAddition() {int result = 2 + 3;Assertions.assertEquals(5, result);}@Test@DisplayName("测试方法 2:减法测试")void testSubtraction() {int result = 5 - 3;Assertions.assertEquals(2, result);}
}

@Disabled

@Disabled 用于 禁用 某个测试类或测试方法,使其在测试运行时 不被执行

import org.junit.jupiter.api.*;class DisabledTest {@Test@Disabled("此测试暂时禁用")void testNotRun() {System.out.println("这个测试方法不会被执行");}@Testvoid testRun() {System.out.println("这个测试方法会被执行");}
}

@ExtendWith

@ExtendWith 是 JUnit5 中用于扩展测试功能的注解,它允许在测试类或测试方法中添加扩展(Extension),扩展可以是 JUnit5 提供的标准扩展,也可以是自定义的扩展。@ExtendWith 注解的主要目的是使 JUnit5 更加灵活,能够集成第三方库、提供特定的生命周期管理或自定义测试行为。

添加Mockito扩展示例:

@ExtendWith(MockitoExtension.class)


断言 Assertions

org.junit.Assert.assertTrue 是 JUnit 4 中用于检查条件是否为 true 的断言方法。

在 JUnit 5 中,这个方法被移动到了 org.junit.jupiter.api.Assertions 类中,并且推荐使用 import static 导入,方便直接调用断言方法。

Assertions 类包含的常见断言方法:

  • assertEquals(expected, actual) — 判断预期值与实际值是否相等。
  • assertNotEquals(expected, actual) — 判断预期值与实际值是否不相等。
  • assertTrue(condition) — 判断条件是否为 true。
  • assertFalse(condition) — 判断条件是否为 false。
  • assertNull(object) — 判断对象是否为 null。
  • assertNotNull(object) — 判断对象是否不为 null。
  • assertSame(expected, actual) — 判断两个对象是否是同一个实例。
  • assertNotSame(expected, actual) — 判断两个对象是否不是同一个实例。
  • assertThrows(expectedType, executable) — 判断代码块是否抛出预期的异常。
  • assertAll(Executable... assertions) — 执行多个断言并报告所有失败的情况。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;public class CalculatorTest {@Testvoid testAddition() {int sum = 2 + 3;assertEquals(5, sum, "2 + 3 should equal 5");}@Testvoid testIsEmpty() {String str = "";assertTrue(str.isEmpty(), "String should be empty");}@Testvoid testNotNull() {String name = "JUnit";assertNotNull(name, "Name should not be null");}
}


假设Assumptions

在 JUnit 中,Assumptions 是一种用来在测试运行之前检查前提条件(假设条件)的机制。如果假设条件不成立,测试会被跳过,而不是失败

1. Assume.assumeTrue(boolean condition)

Assume.assumeTrue() 会检查给定的条件是否为 true。如果条件为 false,则跳过当前测试。

2. Assume.assumeFalse(boolean condition)

Assume.assumeFalse() 与 assumeTrue() 类似,但它会检查条件是否为 false。如果条件为 true,则跳过当前测试。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assumptions;
import static org.junit.jupiter.api.Assertions.*;public class OSBasedTest {@Testvoid testOnlyOnLinux() {// 假设条件:当前操作系统必须是 LinuxAssumptions.assumeTrue(System.getProperty("os.name").contains("Linux"), "Test can only be run on Linux");// 这里放置 Linux 上才有意义的测试逻辑String expected = "LinuxTest";String actual = getLinuxSpecificFeature();assertEquals(expected, actual);}private String getLinuxSpecificFeature() {// 模拟在 Linux 上获取某个特定的功能return "LinuxTest";}
}


异常处理

在 JUnit 5 中,异常处理是一个重要的功能,用于验证代码是否按预期抛出异常。JUnit 5 提供了几种方法来检查代码是否会抛出预期的异常,并且可以对异常进行更详细的控制和验证。

assertThrows 方法

在 JUnit 5 中,最常用的异常验证方法是 assertThrows()。它用于验证代码是否抛出了预期类型的异常。

import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;class CalculatorTest {@Testvoid testDivideByZero() {Calculator calculator = new Calculator();assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0),"Expected ArithmeticException when dividing by zero");}
}

assertDoesNotThrow 方法

与 assertThrows() 相对的是 assertDoesNotThrow(),它用于验证代码块是否 不抛出任何异常

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test;class CalculatorTest {@Testvoid testAdd() {Calculator calculator = new Calculator();assertDoesNotThrow(() -> calculator.add(2, 3),"Addition should not throw any exception");}
}

assertThrows 检查异常消息

在 JUnit 5 中,assertThrows() 除了可以检查异常类型,还可以检查异常的消息。要检查异常的消息,我们可以获取到抛出的异常对象,并验证其 getMessage() 方法。

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;class CalculatorTest {@Testvoid testDivideByZeroWithMessage() {Calculator calculator = new Calculator();ArithmeticException thrown = assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0));assertEquals("Cannot divide by zero", thrown.getMessage(), "The exception message should match the expected message");}
}

throws 异常传播

JUnit 5 允许在测试方法上声明 throws,使得测试方法可以抛出异常并且让 JUnit 处理。这对那些会抛出受检查异常(例如 IOException)的测试方法非常有用。

import org.junit.jupiter.api.Test;
import java.io.IOException;class FileTest {@Testvoid testFileRead() throws IOException {// 这里的代码可能会抛出 IOExceptionthrow new IOException("File not found");}
}


条件执行 Conditional

在 JUnit 5 中,条件执行(Conditional Execution)允许根据不同的条件来决定是否执行某个测试或生命周期方法。条件执行可以在不同的环境、操作系统、配置或其他因素下动态地跳过或启用某些测试方法。

JUnit 5 提供了多个注解和机制来支持条件执行,主要包括:

  • @EnabledIf 系列注解
  • @DisabledIf 系列注解
  • @EnabledIfSystemProperty 和 @DisabledIfSystemProperty
  • @EnabledIfEnvironmentVariable 和 @DisabledIfEnvironmentVariable
  • @EnabledIfModule 和 @DisabledIfModule
  • @EnabledIfAny 和 @DisabledIfAny
  • @EnabledIfAll 和 @DisabledIfAll

示例: 

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.condition.DisabledIf;class ConditionalTest {@Test@EnabledIf(expression = "#{systemProperties['os.name'].contains('Linux')}", reason = "Runs only on Linux")void testOnlyOnLinux() {System.out.println("This test runs only on Linux");}@Test@DisabledIf(expression = "#{systemProperties['os.name'].contains('Windows')}", reason = "Does not run on Windows")void testNotOnWindows() {System.out.println("This test does not run on Windows");}
}


测试执行顺序 (Test Execution Order)

在 JUnit 5 中,测试类和测试方法的执行顺序默认是确定性的,这意味着虽然每次运行测试套件时,测试类和测试方法会按照固定的顺序执行,但我们不应该预测或依赖于这一顺序来编写测试。


JUnit4 vs JUnit5 以及注意事项

1. 架构上的差异

  • JUnit4:JUnit4 使用的是一个单一的类库,所有的测试逻辑和扩展都集成在一起。
  • JUnit5:JUnit5 引入了一个更为模块化的架构,分为三个主要的子模块:
    • JUnit Platform:负责启动测试框架,提供支持运行测试的基础设施。
    • JUnit Jupiter:包含 JUnit5 的新特性和扩展机制(如新的注解、条件化测试等)。
    • JUnit Vintage:支持运行 JUnit4 和早期版本的测试。

2. @Test

  • JUnit4 的 @Test 注解位于 org.junit.Test 包中。
  • JUnit5 的 @Test 注解位于 org.junit.jupiter.api.Test 包中。JUnit5 将所有新的测试框架和 API 放在了 org.junit.jupiter 中。

3. @Before / @After 与 @BeforeEach / @AfterEach

  • JUnit4:使用 @Before 和 @After 注解在每个测试方法执行前后执行初始化和清理操作。
  • JUnit5:使用 @BeforeEach @AfterEach 注解,功能和 @Before / @After 相同。

4. @BeforeClass / @AfterClass 与 @BeforeAll / @AfterAll

  • JUnit4:使用 @BeforeClass 和 @AfterClass 注解执行类级别的初始化和清理操作,这些方法必须是 static 的。
  • JUnit5:使用 @BeforeAll @AfterAll 注解,功能类似 @BeforeClass 和 @AfterClass,也需要方法是 static 的。

5. @RunWith 与 @ExtendWith

  • JUnit4:使用 @RunWith 注解来指定测试运行器,如 @RunWith(MockitoJUnitRunner.class)。
  • JUnit5:使用 @ExtendWith 注解来指定扩展,JUnit5 使用了更灵活的扩展机制。

6. @Ignore 与 @Disabled

  • JUnit4:使用 @Ignore 注解来跳过某个测试方法或测试类。
  • JUnit5:使用 @Disabled 注解来禁用某个测试方法或测试类。

7. @Test(expected = Exception.class) 与 assertThrows

  • JUnit4:使用 @Test(expected = Exception.class) 来测试方法是否抛出指定的异常。
  • JUnit5:使用 assertThrows() 方法。

8. @ClassRule 和 @Rule 与 @ExtendWith

  • JUnit4:使用 @ClassRule 和 @Rule 来创建类级别和方法级别的规则。
  • JUnit5:JUnit5 更加灵活,使用 @ExtendWith 注解来扩展规则,可以实现类似的功能。

9. 断言 (Assertions)

  • JUnit4:断言方法位于 org.junit.Assert 类中。
  • JUnit5:断言方法移至 org.junit.jupiter.api.Assertions 类,并增加了更丰富的断言方法。

不推荐使用 assert 关键字 来做单元测试的断言,因为 Java 中的 assert 语句并不是为单元测试设计的,而是用于程序运行时的调试和开发阶段。

10. 集成 Mockito

  • MockitoAnnotations.initMocks(testClass) 是在 JUnit 4 中使用 Mockito 进行依赖注入的常见方式。
  • @ExtendWith(MockitoExtension.class) 是 JUnit 5 引入的功能,用于更简洁地集成 Mockito。


参考

JUnit 5 User Guide

http://www.dtcms.com/a/263002.html

相关文章:

  • Spring Boot 实现不同用户不同访问权限
  • 基于uniapp的老年皮肤健康管理微信小程序平台(源码+论文+部署+安装+售后)
  • 跨时间潜运动迁移以实现操作中的多帧预测
  • Instrct-GPT 强化学习奖励模型 Reward modeling 的训练过程原理实例化详解
  • nifi1.28.1集群部署详细记录
  • 大语言模型LLM在训练/推理时的padding
  • 用户行为序列建模(篇十一)-小结篇(篇一)
  • 如何读取运行jar中引用jar中的文件
  • C++ --- list
  • 《Effective Python》第十一章 性能——使用 timeit 微基准测试优化性能关键代码
  • 分发糖果
  • Spring Boot 集成 tess4j 实现图片识别文本
  • Springboot + vue + uni-app小程序web端全套家具商场
  • Serverless 架构入门与实战:AWS Lambda、Azure Functions、Cloudflare Workers 对比
  • 人工智能参与高考作文写作的实证研究
  • 华为物联网认证:开启万物互联的钥匙
  • 设计模式-观察者模式(发布订阅模式)
  • YOLOv12_ultralytics-8.3.145_2025_5_27部分代码阅读笔记-torch_utils.py
  • 现代JavaScript前端开发概念
  • spring-ai-alibaba官方 Playground 示例
  • 使用pyflink进行kafka实时数据消费
  • 电脑开机加速工具,优化启动项管理
  • 【Unity】MiniGame编辑器小游戏(七)贪吃蛇【Snake】
  • Java项目:基于SSM框架实现的云端学习管理系统【ssm+B/S架构+源码+数据库+毕业论文】
  • 离线环境安装elk及设置密码认证
  • 通过案例来了解let、const、var的区别
  • DAY 47 注意力热图可视化
  • 有些Android旧平台,在Settings菜单里的,设置-电池菜单下,没有电池使用数据,如何处理
  • RK3568平台开发系列讲解:HDMI显示驱动
  • 六自由度按摩机器人 MATLAB 仿真