Appearance
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上启动测试框架的基础,它定义了
TestEngineAPI,测试框架只需要实现该接口,就可以运行在JUnit Platform上。JUnit Jupiter:JUnit Jupiter 既定义了写测试的规范和工具(编程模型),也提供了自定义增强的能力(扩展模型);同时,它还内置了一套引擎,确保这些测试能在 JUnit 平台上顺利执行。
更通俗地说:
- 编程模型:决定了如何写测试。 它包含了在编写测试代码时用到的所有“语法糖”,例如:
- 注解:
@Test,@BeforeEach,@AfterEach,@DisplayName。 - 断言:
Assertions.assertEquals()。
- 注解:
- 扩展模型:决定了如何增强测试功能。 JUnit 4 时代使用的是
Runner和Rule,而 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()方法。

2.3 显示名称:@DisplayName
在上面的测试中,我们可以看到结果中,测试类和测试方法名称显示在测试结果中,这是由@DisplayNameGenerator决定的,@DisplayNameGenerator 用于自动生成测试方法的显示名称(Display Name)。
这是一个只可以标注在测试类上的注解。
可选值如下:
Standard:默认值,保留方法名及其括号,例如:方法名:
test_inventory_sync()显示名:
test_inventory_sync()Simple:这个生成器会去掉方法名后不带参数的括号(),例如:方法名:
test_inventory_sync()显示名:
test_inventory_syncReplaceUnderscores:它会将方法名中的下划线_替换为空格,例如:方法名:
test_db_warehouse_connection_success()显示名:
test db warehouse connection successIndicativeSentences:它会将测试类名和方法名拼接在一起(用英文逗号,分隔),生成一个类似句子的完整描述,例如:类名:
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() {
}
}
2.4 禁用测试:@Disabled
在 JUnit Jupiter 中,@Disabled 的作用非常简单直接:临时禁用(跳过)当前的测试类或测试方法。
当测试引擎扫描到带有此注解的代码时,它会跳过执行并将其标记为“已忽略”(Ignored),而不是运行失败,还可以为它提供一个理由,以便理解为什么这段代码被禁用了。
- 标注在类上:该测试类中的所有测试方法都会被禁用。
- 标注在方法上:仅该特定方法不被执行。
在IDEA中,运行整个测试类时,如果某个测试方法标注了@Disabled,那么该测试方法被标注为灰色,并显示禁用原因。
在CI/CD 流水线中,运行 mvn test 时,这些测试会被自动跳过且不计入失败。
但是,如果在IDEA中单独运行标注了@Disabled的测试方法或测试类,该方法/类还是会运行,不会被禁用。

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");
}
}
}
4. 测试执行顺序
默认情况下,测试类和测试方法以一种确定性但不明显的顺序执行,以确保可以以相同的顺序重复执行顺序。
在实际中,测试是否成功不应该依赖测试执行顺序,但是,JUnit仍然提供了指定测试执行顺序的方法。
指定测试方法的执行顺序
如果要指定某个测试类中测试方法的执行顺序,可以在测试类上使用注解@TestMethodOrder,取值如下:
TestMethodOrder.DisplayName:按照测试方法显示的名称进行顺序执行;TestMethodOrder.MethodName:按照测试方法名称进行顺序执行;TestMethodOrder.OrderAnnotation:在测试方法上使用@Order指定执行顺序,数值越小越先开始执行;TestMethodOrder.Random:以伪随机的方式,以随机的顺序执行测试方法;默认情况下,随机种子是在类加载阶段通过System.nanoTime()获取的,如果想指定随机种子,可以在JUnit配置文件junit-platform.properties中添加以下配置:propertiesjunit.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");
}
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.Path或java.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);
}
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);
}
null和空值
对于单个参数的测试方法,JUnit可以为参数提供null和空值,分别使用以下注解:
@NullSource:为参数提供null值,注意,如果参数类型为基本类型,则不能使用该注解;@EmptySource:为参数提供空值,适用于java.lang.String、java.util.Collection、对象数组(Object[])、基本类型数组(int[])等类型;NullAndEmptySource:结合@NullSource和@EmptySource,为参数提供null和空值;
例如:
java
@ParameterizedTest
@ValueSource(strings = {"a","b"})
@NullAndEmptySource
void test01(String str){
System.out.println(str);
}
@EnumSource
为参数提供枚举值,例如:
java
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void test03(ChronoUnit chronoUnit){
System.out.println(chronoUnit);
}
@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");
}
}如果工厂方法定义在测试类中,那么可以忽略掉@MethodSource的value值,此时工厂方法需要与测试方法名称相同:
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);
}
默认情况下,@CsvSource中的value数组,其中一个元素(一行)代表一次测试运行,每一行以英文逗号,分隔成列,每一列对应测试方法的一个参数。
在@CsvSource中,还有以下属性可以设置:
delimiter:列分隔符,默认为英文逗号,可以设置为其他字符,例如|,也可以使用delimiterString用来设置分隔字符串;quoteCharacter:引用字符,默认为单引号',应用场景见下表;ignoreLeadingAndTrailingWhitespace:是否忽略前后的空白字符,默认值为true;nullValues:定义表示null值的字符串;
下表是一些使用示例:
| Example Input | Resulting 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 }javapublic 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类型参数,还可以接受long、float、double类型参数。例如:
java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void test(float num) {
System.out.println(num);
}9.3.2 隐式转换
为了支持@CsvSource等参数来源,JUnit提供了许多内置的隐式转换器,都是从字符串转换为特定的类型。例如下表:
| Target Type | Example |
|---|---|
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) |
除了以上的隐式转换器,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);
}