# Spring单元测试

# Junit

# 简介

JUnit 是一种广泛使用的单元测试框架,用于在 Java 中编写和运行测试。JUnit 作为测试驱动开发(TDD)的重要工具,能够帮助开发者编写可重复执行的测试,确保代码的正确性和质量。它提供了注解、断言和运行器等功能,支持自动化测试的执行和结果验证。

# 版本

JUnit 3.x:早期版本,基于继承的方式组织测试类和方法。

JUnit 4.x:引入了注解机制,简化了测试的编写方式,并且可以直接继承 TestCase 类。

JUnit 5.x:是当前的版本,具有更强大的功能和更高的灵活性,支持模块化架构。JUnit 5 的核心由以下三部分组成:

  • JUnit Platform:提供了运行测试的基础设施。
  • JUnit Jupiter:JUnit 5 的编程模型,提供新的注解和断言方法。
  • JUnit Vintage:用于支持运行 JUnit 3 和 JUnit 4 的测试。

# 测试方法命名

  • 清晰、简洁:方法名应清晰地描述测试的目标、条件和预期结果。
  • 遵循一致性:保持命名风格的一致性,便于团队成员理解和维护。
  • 行为驱动开发(BDD)风格:可以考虑使用 given-when-then 风格命名,尤其是当测试涉及复杂的场景时。

# 注解

注解 描述
@Test 表示该方法是测试方法
@ParameterizedTest 参数化测试,需要配合参数源注解使用(如 @ValueSource@CsvSource
@RepeatedTest 重复测试、验证一致性、模拟压力测试
@TestFactory 允许运行时根据逻辑生成一组动态的测试用例,批量测试
@TestTemplate 定义测试模板,需要实现TestTemplateInvocationContextProvider
@TestClassOrder 测试类执行顺序,有内置排序器,也可与实现了ClassOrderer 接口的类一起使用
@TestMethodOrder 测试方法执行顺序,需要与实现了MethodOrderer接口的类一起使用
@TestInstance 测试类的生命周期,每个方法一个测试实例,或每个类一个实例
@DisplayName 自定义显示名称,使得测试更容易理解
@DisplayNameGeneration 为测试类中的所有测试方法自动生成显示名称
@BeforeEach 在每个测试方法执行之前运行的方法,初始化测试资源
@AfterEach 在每个测试方法执行之后运行的方法
@BeforeAll 在整个测试类中的所有测试方法执行之前运行的方法,初始化一次性资源
@AfterAll 在整个测试类中的所有测试方法执行之后运行的方法
@Nested 嵌套测试类
@Disabled 禁用测试方法或测试类,调试的时候使用
@Tag 声明用于过滤测试的标签
@AutoClose 自动关闭的资源
@Timeout 定义超时限制
@TempDir 自动创建临时目录
@ExtendWith 用于扩展测试
@RegisterExtension 实例化扩展

# 组合注释

正常使用标签注解

@Test
@Tag("fast")
void test(){
    System.out.println("测试方法");
}

自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}

简洁的使用

@FastTest
void test(){
    System.out.println("测试方法");
}

# 断言Assert

JUnit 5 的断言类位于 org.junit.jupiter.api.Assertions 包中,包含了许多有用的静态方法

# 断言方法

断言方法 作用
assertEquals 验证两个值是否相等
assertNotEquals 验证两个值是否不相等
assertTrue 验证条件是否为 true
assertFalse 验证条件是否为 false
assertNull 验证对象是否为 null
assertNotNull 验证对象是否不为 null
assertArrayEquals 验证两个数组是否相等
assertThrows 验证是否抛出了特定类型的异常
assertTimeout 验证操作是否在规定时间内完成
assertAll 组合多个断言一起执行

# 第三方断言库

例如AssertJ (opens new window)Hamcrest (opens new window)Truth (opens new window)

# 假设Assume

当继续执行给定测试没有意义时,可以使用假设,例如,测试依赖于当前运行环境不存在的东西。

  • 假设有效时,假设方法不会引发异常,测试继续执行
  • 假设无效是,会抛出类型异常org.opentest4j.TestAbortedException

所有 JUnit Jupiter 假设都是org.junit.jupiter.api.Assumptions类中的静态方法

# 假设方法

假设方法 作用
assumeTrue 如果条件为True,继续执行,否则跳过测试
assumeFalse 如果条件为False,继续执行,否则跳过测试
assumeNotNull 如果对象不为null,继续执行,否则跳过测试
assumeNull 如果对象为null,继续执行,否则跳过测试

# 和断言的比较

  • 假设:假设用于设定前提条件,确保测试在某些条件下才会执行如果条件不满足,测试会被跳过,而不是失败

  • 断言:断言用于验证实际结果与预警结果是否一致,如果不一致,测试会失败

# 测试执行顺序

# 方法顺序

内置MethodOrderer实现:

  • MethodOrderer.DisplayName:根据显示名称按字母顺序对测试方法进行排序

  • MethodOrderer.MethodName:根据测试方法的名称和形式参数列表按字母顺序对其进行排序

  • MethodOrderer.OrderAnnotation:根据通过注释指定的值按数字方式对测试方法进行排序@Order

  • MethodOrderer.Random:以伪随机方式排列测试方法,并支持配置自定义种子

  • MethodOrderer.Alphanumeric:根据测试方法的名称和形式参数列表*按字母顺序对其进行排序

    已弃用,改为使用MethodOrderer.MethodName,将在 6.0 版中删除

# 类顺序

内置ClassOrderer实现:

  • ClassOrderer.ClassName:根据测试类的完全限定类名按字母顺序对其进行排序
  • ClassOrderer.DisplayName:根据显示名称的字母数字顺序对测试类进行排序(请参阅显示名称生成优先规则 (opens new window)
  • ClassOrderer.OrderAnnotation:根据通过注释指定的值按数字方式对测试类进行排序@Order
  • ClassOrderer.Random:伪随机排序测试类并支持配置自定义种子

# 异常处理(补充)

# 参数化测试

参数化测试允许使用不同的参数多次运行测试,此外必须至少声明一个数据源

# 数据源

# @ValueSource

允许指定一个数组,每次为参数化测试调用提供一个参数

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertTrue(argument > 0 && argument < 4);
}

无效数据源和空源:

# @EnumSource

用于枚举类型的数据源,提供枚举类型的所有常量

enum Season { WINTER, SPRING, SUMMER, FALL }

@ParameterizedTest
@EnumSource(Season.class) // 使用所有枚举常量
void testWithEnumSource(Season season) {
    assertNotNull(season);
}

# @MethodSource

指定一个返回流或者数组的静态方法,这个方法可以返回 StreamCollectionIterableIterator 或数组

@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("apple", "banana", "cherry");
}

# @FieldSource

引用测试类或外部类的一个或多个字段

@ParameterizedTest
@FieldSource("listOfFruits")
void singleFieldSource(String fruit) {
    assertFruit(fruit);
}

static final List<String> listOfFruits = Arrays.asList("apple", "banana");

# @CsvSource

用于提供csv格式的多个数据行,每一行数据对应测试方法的一个参数集,多个参数使用逗号分隔

@ParameterizedTest
@CsvSource({
    "apple, 1",
    "banana, 2",
    "cherry, 3"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertTrue(rank > 0);
}

# @CsvFileSource

和@CsvSource类似,允许从CSV文件加载数据

@ParameterizedTest
@CsvFileSource(resources = "/testData.csv", numLinesToSkip = 1)
void testWithCsvFileSource(String name, int age) {
    assertNotNull(name);
    assertTrue(age > 0);
}

# @ArgumentsSource

用于指定一个实现了 ArgumentsProvider 接口的类或方法

class CustomArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(Arguments.of("apple", 1), Arguments.of("banana", 2));
    }
}

@ParameterizedTest
@ArgumentsSource(CustomArgumentsProvider.class)
void testWithArgumentsSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertTrue(rank > 0);
}

# 参考文档

Junit5官方文档 (opens new window)

掘金文章-怎么写好单元测试 (opens new window)

Last Updated: 2/8/2025, 8:12:41 AM