Skip to content

JUnit介绍

本文介绍如何使用JUnit进行单元测试。

什么是单元测试

单元测试(Unit Testing) 是软件开发中的一种测试方法,指对软件中的最小可测试单元进行检查和验证。

最小可测试单元通常是指一个方法(Method)或一个类(Class)

本文档内容基于JUnit 6.0.3版本文档,文档地址:https://docs.junit.org/6.0.3/overview.html。

1. JUnit概述

在JUnit官方文档中,JUnit由三部分构成:

JUnit 6.0.3 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform:是JVM上启动测试框架的基础,它定义了TestEngine API,测试框架只需要实现该接口,就可以运行在JUnit Platform上。

  • JUnit Jupiter:JUnit Jupiter 既定义了写测试的规范和工具(编程模型),也提供了自定义增强的能力(扩展模型);同时,它还内置了一套引擎,确保这些测试能在 JUnit 平台上顺利执行。

    更通俗地说:

    • 编程模型:决定了如何写测试。 它包含了在编写测试代码时用到的所有“语法糖”,例如:
      • 注解@Test, @BeforeEach, @AfterEach, @DisplayName
      • 断言Assertions.assertEquals()
    • 扩展模型:决定了如何增强测试功能。 JUnit 4 时代使用的是 RunnerRule,而 Jupiter 统一为了 Extension(扩展)。通过实现特定的接口(如 BeforeEachCallback),我们可以自定义测试行为,例如:
      • 自动注入测试数据。
      • 在每个测试方法前后自动开启和关闭数据库事务。
      • 集成 Spring 框架(@ExtendWith(SpringExtension.class))。
    • 引擎(TestEngine实现):Jupiter TestEngine 是JUnit Platform TestEngine的一个实现,专门负责发现并执行基于 Jupiter 编写的测试用例。

    综上,JUnit Jupiter是我们学习的重点。

  • JUnit Vintage:用于运行基于JUnit 3和JUnit 4的测试用例,这是为了向后兼容而存在的部分;

2. 测试案例介绍

2.1 引入依赖

本小节介绍如何编写测试用例。首先,引入依赖:

xml
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.0.3</version>
    <scope>test</scope>
</dependency>

以上依赖会间接引入以下依赖:

  • junit-jupiter-api:提供编程模型支持(包括扩展模型);
  • junit-jupiter-engine:提供测试引擎实现,在该依赖中又引入了org.junit.platform:junit-platform-engine
  • junit-jupiter-params:提供编程模型额外支持,主要是带参数的测试;

2.2 简单案例:@Test

假设我们的程序中有一个类Calculator(在目录src/main/java下),用于计算两个整数相加:

java
public class Calculator {
    public static Integer add(Integer num1, Integer num2)
    {
        if (num1 == null || num2 == null){
            throw new IllegalArgumentException("参数不能为空");
        }
        return num1 + num2;
    }
}

下面是一个简单的测试案例(在目录src/test/java下):

java
public class CalculatorTest {
    @Test
    public void testAdd()
    {
        Integer result = Calculator.add(1, 2);
        System.out.println(result);
    }
}

TIP

@Test注解表示某个方法是测试方法,测试方法需要满足以下要求:

  • 测试方法不能是私有的(private)、静态的(static),并且不能返回值(只能返回void);
  • 测试方法可以声明参数,但是参数必须能被ParameterResolver解析;

在IDEA中,我们可以直接运行testAdd()方法。

image-20260208153632638

2.3 显示名称:@DisplayName

在上面的测试中,我们可以看到结果中,测试类和测试方法名称显示在测试结果中,这是由@DisplayNameGenerator决定的,@DisplayNameGenerator 用于自动生成测试方法的显示名称(Display Name)。

这是一个只可以标注在测试类上的注解。

可选值如下:

  • Standard:默认值,保留方法名及其括号,例如:

    方法名: test_inventory_sync()

    显示名: test_inventory_sync()

  • Simple:这个生成器会去掉方法名后不带参数的括号 (),例如:

    方法名: test_inventory_sync()

    显示名: test_inventory_sync

  • ReplaceUnderscores:它会将方法名中的下划线 _ 替换为空格,例如:

    方法名: test_db_warehouse_connection_success()

    显示名: test db warehouse connection success

  • IndicativeSentences:它会将测试类名和方法名拼接在一起(用英文逗号,分隔),生成一个类似句子的完整描述,例如:

    类名: WarehouseTest

    方法名: should_save_data()

    显示名: WarehouseTest, should_save_data()

如果想在某个类上设置测试方法的显示名称,那么可以在该类上使用该注解,如果想全局设置(避免每个测试类上都设置),可以在src/test/resources/junit-platform.properties文件中设置如下:

properties
junit.jupiter.displayname.generator.default = \
    org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

除了设置测试方法名称的生成方式,我们也可以直接设置显示名称,使用@DisplayName注解,该注解可以在测试类和测试方法上设置。

@DisplayName的优先级高于@DisplayNameGenerator

java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("A special test case")
class DisplayNameDemo {

	@Test
	@DisplayName("Custom test name containing spaces")
	void testWithDisplayNameContainingSpaces() {
	}

	@Test
	@DisplayName("╯°□°)╯")
	void testWithDisplayNameContainingSpecialCharacters() {
	}

	@Test
	@DisplayName("😱")
	void testWithDisplayNameContainingEmoji() {
	}

}

image-20260208161014065

2.4 禁用测试:@Disabled

在 JUnit Jupiter 中,@Disabled 的作用非常简单直接:临时禁用(跳过)当前的测试类或测试方法

当测试引擎扫描到带有此注解的代码时,它会跳过执行并将其标记为“已忽略”(Ignored),而不是运行失败,还可以为它提供一个理由,以便理解为什么这段代码被禁用了。

  • 标注在类上:该测试类中的所有测试方法都会被禁用。
  • 标注在方法上:仅该特定方法不被执行。

在IDEA中,运行整个测试类时,如果某个测试方法标注了@Disabled,那么该测试方法被标注为灰色,并显示禁用原因。

在CI/CD 流水线中,运行 mvn test 时,这些测试会被自动跳过且不计入失败。

但是,如果在IDEA中单独运行标注了@Disabled的测试方法或测试类,该方法/类还是会运行,不会被禁用。

image-20260208161446705

3. 嵌套测试类

@Nested注解用于标注嵌套类为测试类。

注意,只有非静态的嵌套类才可以标注@Nested

@Nested并不在乎外层类中是否有测试方法,只要标注了@Nested注解,那么嵌套类就是可执行的测试类。

java
public class NestedTestDemo {

    @Test
    void test() {
        System.out.println("NestedTestDemo.test");
    }

    @Nested
    class NestedTest {
        @Test
        void test() {
            System.out.println("NestedTestDemo.NestedTest.test");
        }
    }
}

image-20260208165746435

4. 测试执行顺序

默认情况下,测试类和测试方法以一种确定性但不明显的顺序执行,以确保可以以相同的顺序重复执行顺序。

在实际中,测试是否成功不应该依赖测试执行顺序,但是,JUnit仍然提供了指定测试执行顺序的方法。

指定测试方法的执行顺序

如果要指定某个测试类中测试方法的执行顺序,可以在测试类上使用注解@TestMethodOrder,取值如下:

  • TestMethodOrder.DisplayName:按照测试方法显示的名称进行顺序执行;

  • TestMethodOrder.MethodName:按照测试方法名称进行顺序执行;

  • TestMethodOrder.OrderAnnotation:在测试方法上使用@Order指定执行顺序,数值越小越先开始执行;

  • TestMethodOrder.Random:以伪随机的方式,以随机的顺序执行测试方法;默认情况下,随机种子是在类加载阶段通过 System.nanoTime()获取的,如果想指定随机种子,可以在JUnit配置文件junit-platform.properties中添加以下配置:

    properties
    junit.jupiter.execution.order.random.seed=xxx

例如,使用OrderAnnotation的方案指定测试方法的执行顺序:

java
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestDemo {

    @Test
    @Order(2)
    void test() {
        System.out.println("NestedTestDemo.test");
    }

    @Test
    @Order(1)
    void test2() {
        System.out.println("NestedTestDemo.test2");
    }
}

如果想全局指定测试方法的执行顺序方案,可以在JUnit配置文件中添加如下配置:

properties
junit.jupiter.testmethod.order.default = \
	org.junit.jupiter.api.MethodOrderer$OrderAnnotation

指定测试类的执行顺序

如果在一个类中,存在多个嵌套测试类,我们可以使用注解@TestClassOrder指定嵌套测试类的执行顺序,可选值如下:

  • ClassOrderer.ClassName:根据类名称进行顺序执行;
  • ClassOrderer.DisplayName:根据测试类的显示名称进行顺序执行;
  • ClassOrderer.OrderAnnotation:根据测试类上的@Order指定的顺序进行顺序执行;
  • ClassOrderer.Random:以随机的顺序进行执行;

例如:

java
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
public class NestedTestDemo {

    @Nested
    @Order(2)
    class NestedTest {
        @Test
        void test() {
            System.out.println("NestedTestDemo.NestedTest.test");
        }
    }

    @Nested
    @Order(1)
    class NestedTest2 {
        @Test
        void test() {
            System.out.println("NestedTestDemo.NestedTest2.test");
        }
    }
}

5. 测试实例

默认情况下,JUnit为每一个测试方法创建一个测试类对象。

例如,现有如下测试类:

java
public class DemoTest {
    @Test
    public void test1() {
    }
    
    @Test
    public void test2() {
    }
}

执行该测试类时,模拟过程如下:

java
DemoTest demoTest1 = new DemoTest();
demoTest1.test1();

DemoTest demoTest2 = new DemoTest();
demoTest2.test2();

可以看到,每个测试方法都有对应的测试类对象。

我们可以通过注解@TestInstance调整该机制,取值如下:

  • Lifecycle.PER_CLASS:对于一个测试类,只创建一个测试类对象;
  • Lifecycle.PER_METHOD:对于一个测试方法,创建一个测试类对象;

如果调整为Lifecycle.PER_CLASS,那就是一个测试类创建一个对象,然后在该对象上执行所有的测试方法,如下:

java
DemoTest demoTest = new DemoTest();
demoTest.test1();
demoTest.test2();

我们也可以在JUnit配置文件中全局调整该配置:

properties
junit.jupiter.testinstance.lifecycle.default = per_class

建议不要调整,保持默认即可。

6. 测试生命周期方法

测试生命周期方法是控制测试前后“准备”与“清理”工作的核心工具,有些方法在每个测试用例前后运行,有些则在整个测试类开始和结束时各运行一次。

JUnit 提供了四个主要的生命周期注解:

注解执行时机典型用途
@BeforeAll在当前类的所有测试方法之前执行一次启动数据库、开启服务器、初始化昂贵的资源
@BeforeEach每个测试方法执行前运行一次重置测试数据、实例化对象、准备干净的测试环境
@AfterEach每个测试方法执行后运行一次关闭临时资源、清理内存、重置 Mock 对象
@AfterAll在当前类的所有测试方法之后执行一次关闭数据库连接、停止服务器、删除临时文件

假设有如下测试类:

java
import org.junit.jupiter.api.*;

class LifecycleTest {

    @BeforeAll
    static void initAll() {
        System.out.println("--- @BeforeAll: 整个类开始前执行(必须是静态方法) ---");
    }

    @BeforeEach
    void init() {
        System.out.println("  > @BeforeEach: 每个测试方法前执行");
    }

    @Test
    void testOne() {
        System.out.println("    执行测试 1");
    }

    @Test
    void testTwo() {
        System.out.println("    执行测试 2");
    }

    @AfterEach
    void tearDown() {
        System.out.println("  > @AfterEach: 每个测试方法后执行");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("--- @AfterAll: 整个类结束后执行(必须是静态方法) ---");
    }
}

结果如下:

txt
--- @BeforeAll: 整个类开始前执行(必须是静态方法) ---
  > @BeforeEach: 每个测试方法前执行
    执行测试 1
  > @AfterEach: 每个测试方法后执行
  > @BeforeEach: 每个测试方法前执行
    执行测试 2
  > @AfterEach: 每个测试方法后执行
--- @AfterAll: 整个类结束后执行(必须是静态方法) ---

7. 重复测试

@RepeatedTest 允许将同一个测试方法自动运行指定的次数。如果在测试一个涉及 Random 类或概率的模型,通过多次重复可以验证其结果是否落在预期的概率区间内,这就是重复测试的场景之一。

@RepeatedTest 参数如下:

  • value:指定要运行的次数;

  • failureThreshold:失败次数阈值,当测试方法达到该次数后,剩余未运行的测试将会被跳过,默认值为Integer.MAX_VALUE

    注意,如果设置了并发运行测试,那么该项设置并不能保证。

  • name:显示名称;有三个占位符可以使用:

    • {displayName}:显示名称;
    • {currentRepetition}:当前执行次数;
    • {totalRepetitions}:总执行次数;

    默认值为repetition {currentRepetition} of {totalRepetitions}

例如:

java
@DisplayName("自定义测试")
@RepeatedTest(value = 5, name = "{displayName} {currentRepetition}/{totalRepetitions}")
public void test1() {
    System.out.println("test1");
}

image-20260215165500772

8. 依赖注入

在JUnit之前的版本中,测试类构造方法和测试方法不允许有参数,在JUnit Jupiter实现中,构造方法、测试方法、生命周期方法允许有参数。

通常情况下,Java 方法的参数需要在代码中手动传入,但测试方法(如 @Test)是由框架自动调用的。如果想在这些方法里加参数,框架就必须知道从哪儿获取到这些数据,这就是 ParameterResolver 的用武之地。

ParameterResolver定义了一套标准接口,如果一个测试扩展(Extension)想要在运行时动态地为方法提供参数值,就必须实现这个接口,该接口中定义了两个方法:

java
boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
			throws ParameterResolutionException;

Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
			throws ParameterResolutionException;
  • 判定 (Supports):解析器检查参数的类型或注解,决定自己是否支持这个参数;

  • 解析 (Resolve):如果支持,解析器计算或获取具体的值,并传给方法;

在JUnit Jupiter中,以下是内置的参数解析器:

  • TestInfoParameterResolver:用于提供TestInfo类型参数,TestInfo可以用于获取当前测试信息,例如测试类、测试方法、显示名称以及标签;

  • RepetitionExtension:用在重复测试中,用于提供RepetitionInfo类型参数,RepetitionInfo可以用于获取重复测试中的信息,例如当前重复次数、总重复次数、失败次数、失败阈值;

  • TestReporterParameterResolver:用于提供TestReporter类型参数,TestReporter可以用来

    • 发布数据 (publishEntry):发送键值对信息(比如:"状态": "服务器已连接")。

    • 关联文件:将运行过程中的截图、日志文件或生成的 PDF 挂载到当前的测试记录中。

    些信息会直接显示在你的 IDE的测试面板里,或者出现在生成的 HTML 测试报告中。

  • TempDirectory:声明了一个类型为 java.nio.file.Pathjava.io.File 的参数,并加上 @TempDir 注解时,TempDirectory会注入一个物理存在的临时目录。

    @TempDir 可以用在字段上,也可以用在方法参数

例如:

java
@DisplayName("自定义测试")
@RepeatedTest(value = 5, name = "{displayName} {currentRepetition}/{totalRepetitions}")
public void test1(
        TestInfo testInfo,
        RepetitionInfo repetitionInfo,
        TestReporter testReporter,
        @TempDir Path path) {
    String displayName = testInfo.getDisplayName();

    int currentRepetition = repetitionInfo.getCurrentRepetition();
    int totalRepetitions = repetitionInfo.getTotalRepetitions();

    String value = displayName + ": " + currentRepetition + "/" + totalRepetitions;

    System.out.println(path.toAbsolutePath());
    testReporter.publishEntry(value);
}

image-20260218111622624

9. 参数化测试

参数化测试允许我们使用不同的参数运行同一个测试方法/测试类。

  • 如果要定义参数化测试方法,只需要将@Test注解改为@ParameterizedTest注解,然后提供参数来源;
  • 如果要定义参数化测试类,需要在测试类上加@ParameterizedClass注解,并且提供参数来源;

本小节重点介绍参数化测试方法。

在参数化测试方法中,参数被分为三类,它们必须按以下顺序出现:

顺序参数类型说明示例
第一索引参数 (Indexed Parameters)直接对应数据源中的每一列数据。String name, int age
第二聚合器 (Aggregators)将多列数据合并为一个对象。ArgumentsAccessor@AggregateWith
第三解析器参数 (Resolvers)由 JUnit 注入的辅助工具。TestInfo, TestReporter
  • 索引参数是指方法的参数位置(索引)与数据源(Arguments Source)提供的每一列数据索引完全对等;

  • 如果索引参数过多,那么可以使用聚合器参数,将多个参数聚合成一个实体,聚合器参数有两种:

    • ArgumentsAccessor:一个通用的容器,可以从中按索引取值(如 accessor.getString(0));

    • @AggregateWith:自定义聚合逻辑,把多列数据直接转成一个 POJO 对象(如 User 对象);

  • 解析器参数是由JUnit自动注入的参数;

9.1 索引参数

在JUnit中,索引参数可以由以下注解提供值

@ValueSource

@ValueSource 允许指定一个基础类型(字面量)的数组。每次测试运行时,JUnit 会从数组中取出一个元素,把它作为参数传给测试方法。

  • 限制:每个测试方法只能接收 1 个 参数。
  • 支持类型:支持所有的 Java 基础类型(int, long, double 等)、String 以及 Class

例如:

java
@ParameterizedTest
@ValueSource(ints = {1,2,3})
@DisplayName("ValueSource Test")
void test(Integer num){
    System.out.println(num);
}

image-20260218182555893

null和空值

对于单个参数的测试方法,JUnit可以为参数提供null和空值,分别使用以下注解:

  • @NullSource:为参数提供null值,注意,如果参数类型为基本类型,则不能使用该注解;
  • @EmptySource:为参数提供空值,适用于java.lang.Stringjava.util.Collection、对象数组(Object[])、基本类型数组(int[])等类型;
  • NullAndEmptySource:结合@NullSource@EmptySource,为参数提供null和空值;

例如:

java
@ParameterizedTest
@ValueSource(strings = {"a","b"})
@NullAndEmptySource
void test01(String str){
    System.out.println(str);
}

image-20260218210130559

@EnumSource

为参数提供枚举值,例如:

java
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void test03(ChronoUnit chronoUnit){
    System.out.println(chronoUnit);
}

image-20260218210520836

@MethodSource

@MethodSource可以使用一个方法来为测试方法的参数提供值,这个方法称为工厂方法,工厂方法可以定义在同一个测试类中,也可以定义在外部类中,但需要是静态static的。

工厂方法的返回值必须为参数流(例如Stream<Arguments>),或者是可以转换为参数流的类型(例如Collection,对象数组等)。在参数流中的元素,可以是Arguments的实例、对象数组(Object[])、或者是单个值(如果测试方法只接受一个参数)。

例如,测试方法只有一个参数,那么参数流元素可以是参数类型元素:

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

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

例如,如果测试方法有多个参数,那么参数流元素为Arguments实例:

java
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
	assertEquals(5, str.length());
	assertTrue(num >=1 && num <=2);
	assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
	return Stream.of(
		arguments("apple", 1, Arrays.asList("a", "b")),
		arguments("lemon", 2, Arrays.asList("x", "y"))
	);
}

arguments()是Arguments接口中的静态方法。

如果工厂方法定义在外部类,那么可以使用全限定名称:

java
class ExternalMethodSourceDemo {

	@ParameterizedTest
	@MethodSource("example.StringsProviders#tinyStrings")
	void testWithExternalMethodSource(String tinyString) {
		// test with tiny string
	}
}

class StringsProviders {
	static Stream<String> tinyStrings() {
		return Stream.of(".", "oo", "OOO");
	}
}

如果工厂方法定义在测试类中,那么可以忽略掉@MethodSourcevalue值,此时工厂方法需要与测试方法名称相同:

java
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
	assertNotNull(argument);
}

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

@CsvSource

@CsvSource 允许以 CSV(逗号分隔值) 的格式提供多列数据。每一行代表一次测试运行,每一列对应测试方法的一个参数。

例如:

java
@ParameterizedTest
@CsvSource({
	"apple,         1",
	"banana,        2",
	"'lemon, lime', 0xF1",
	"strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
	assertNotNull(fruit);
	assertNotEquals(0, rank);
}

image-20260218215918938

默认情况下,@CsvSource中的value数组,其中一个元素(一行)代表一次测试运行,每一行以英文逗号,分隔成列,每一列对应测试方法的一个参数。

@CsvSource中,还有以下属性可以设置:

  • delimiter:列分隔符,默认为英文逗号,可以设置为其他字符,例如|,也可以使用delimiterString用来设置分隔字符串;
  • quoteCharacter:引用字符,默认为单引号',应用场景见下表;
  • ignoreLeadingAndTrailingWhitespace:是否忽略前后的空白字符,默认值为true
  • nullValues:定义表示null值的字符串;

下表是一些使用示例:

Example InputResulting Argument List
@CsvSource({ "apple, banana" })"apple", "banana"
@CsvSource({ "apple, 'lemon, lime'" })"apple", "lemon, lime"
@CsvSource({ "apple, ''" })"apple", ""
@CsvSource({ "apple, " })"apple", null
@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL")"apple", "banana", null
@CsvSource(value = { " apple , banana" }, ignoreLeadingAndTrailingWhitespace = false)" apple ", " banana"

除了使用@CsvSource,还可以使用@CsvFileSource,用于从CSV文件中获取参数值。

9.2 聚合参数

当参数过多时,我们可以使用聚合参数,有两种用法:

  • ArgumentsAccessor:一个通用的容器,可以从中按索引取值(如 accessor.getString(0));

    java
    @ParameterizedTest
    @CsvSource({
    	"Jane, Doe, F, 1990-05-20",
    	"John, Doe, M, 1990-10-22"
    })
    void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
    	Person person = new Person(
    								arguments.getString(0),
    								arguments.getString(1),
    								arguments.get(2, Gender.class),
    								arguments.get(3, LocalDate.class));
    
    	if (person.getFirstName().equals("Jane")) {
    		assertEquals(Gender.F, person.getGender());
    	}
    	else {
    		assertEquals(Gender.M, person.getGender());
    	}
    	assertEquals("Doe", person.getLastName());
    	assertEquals(1990, person.getDateOfBirth().getYear());
    }
  • @AggregateWith:自定义聚合逻辑,把多列数据直接转成一个 POJO 对象(如 User 对象);

    java
    @ParameterizedTest
    @CsvSource({
    	"Jane, Doe, F, 1990-05-20",
    	"John, Doe, M, 1990-10-22"
    })
    void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
    	// perform assertions against person
    }
    java
    public class PersonAggregator extends SimpleArgumentsAggregator {
    	@Override
    	protected Person aggregateArguments(ArgumentsAccessor arguments, Class<?> targetType,
    			AnnotatedElementContext context, int parameterIndex) {
    		return new Person(
    							arguments.getString(0),
    							arguments.getString(1),
    							arguments.get(2, Gender.class),
    							arguments.get(3, LocalDate.class));
    	}
    }

9.3 类型转换

在下面的@CsvSource中,1、2、0xF1、700_000默认转换为了int类型,说明在参数化测试中,存在着类型转换。

Details
java
@ParameterizedTest
@CsvSource({
	"apple,         1",
	"banana,        2",
	"'lemon, lime', 0xF1",
	"strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
	assertNotNull(fruit);
	assertNotEquals(0, rank);
}

在JUnit 中,存在着三种类型转换:

9.3.1 扩展转换

例如,参数化测试方法标注了参数来源@ValueSource(ints = { 1, 2, 3 }),那么该测试方法不仅可以接受int类型参数,还可以接受longfloatdouble类型参数。例如:

java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void test(float num) {
    System.out.println(num);
}

9.3.2 隐式转换

为了支持@CsvSource等参数来源,JUnit提供了许多内置的隐式转换器,都是从字符串转换为特定的类型。例如下表:

Target TypeExample
boolean/Boolean"true"true (only accepts values 'true' or 'false', case-insensitive)
byte/Byte"15", "0xF", or "017"(byte) 15
char/Character"o"'o'
short/Short"15", "0xF", or "017"(short) 15
int/Integer"15", "0xF", or "017"15
java.time.LocalDateTime"2017-03-14T12:34:56.789"LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)

完整列表参照:https://docs.junit.org/6.0.3/writing-tests/parameterized-classes-and-tests.html#argument-conversion-implicit

除了以上的隐式转换器,JUnit还提供了回退字符串到对象的转换器,可以将字符串转换为指定的类型,这需要目标类型满足下面两个条件之一:

  • 提供工厂方法:非私有的、静态的工厂方法,接受参数为一个字符串,返回目标类型对象,对方法名称没有要求;
  • 提供工厂构造器:非私有的构造器方法,接受参数为一个字符串;

注意,目标类型必须为外部类或内部静态类。

例如:

java
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
	assertEquals("42 Cats", book.getTitle());
}
java
public class Book {

	private final String title;

	private Book(String title) {
		this.title = title;
	}

	public static Book fromTitle(String title) {
		return new Book(title);
	}

	public String getTitle() {
		return this.title;
	}
}

9.3.3 显式转换

我们也可以实现自己的转换器,并且显式指定自定义的转换器。

首先自定义转换器:

java
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {

	protected ToLengthArgumentConverter() {
		super(String.class, Integer.class);
	}

	@Override
	protected Integer convert(String source) {
		return (source != null ? source.length() : 0);
	}

}

也可以继承更通用的类:SimpleArgumentConverter

然后使用自定义的转换器:

java
@ParameterizedTest
@ValueSource(strings = {"apple", "banana"})
void testWithExplicitArgumentConversion(
        @ConvertWith(ToLengthArgumentConverter.class) int length) {
    System.out.println(length);
}

参考资料

[1] https://docs.junit.org/6.0.3/writing-tests/intro.html