官方文档中文版
https://www.bookstack.cn/read/junit5/README.md
此文为对JUnit5官网用户指南的学习,版本为5.4.0版本,对部分内容进行翻译、截取和自我理解,官网手册地址为https://junit.org/junit5/docs/current/user-guide/。 (中文的翻译是5.3版本的)
1总览
JUnit5最低需要JDK8来支持。
与之前的版本不同,JUnit5由3个模块组成:
1、JUnit Platform,用于在JVM上启动测试框架,并通过命令行定义TestEngine API。可以相当于JUnit 4 中的Runner ,要用他做测试引擎。简单地说这个有关的包是用来调用测试用例的,IDE正式因为加载了与这个有关的插件,所以idea里边才可以右键选择执行测试方法。
2、JUnit Jupiter是用于在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合。提供了一堆测试要用的注解和类。
3、Junit Vintage,用于在JUnit5平台上运行JUnit3和4测试用例。
1.1. JUnit Platform
Group ID: org.junit.platform
Version: 1.2.0
Artifact IDs:
junit-platform-commons
JUnit 内部通用类库/实用工具,它们仅用于JUnit框架本身,不支持任何外部使用,外部使用风险自负。
junit-platform-console
支持从控制台中发现和执行JUnit Platform上的测试。详情请参阅 控制台启动器。
junit-platform-console-standalone
一个包含了Maven仓库中的 junit-platform-console-standalone 目录下所有依赖项的可执行JAR包。详情请参阅 控制台启动器。
junit-platform-engine
测试引擎的公共API。详情请参阅 插入你自己的测试引擎。
junit-platform-gradle-plugin
支持使用 Gralde 来发现和执行JUnit Platform上的测试。
junit-platform-launcher
配置和加载测试计划的公共API – 典型的使用场景是IDE和构建工具。详情请参阅 JUnit Platform启动器API。
junit-platform-runner
在一个JUnit 4环境中的JUnit Platform上执行测试和测试套件的运行器。详情请参阅 使用JUnit 4运行JUnit Platform。
junit-platform-suite-api
在JUnit Platform上配置测试套件的注解。被 JUnit Platform运行器 所支持,也有可能被第三方的TestEngine实现所支持。
junit-platform-surefire-provider
支持使用 Maven Surefire 来发现和执行JUnit Platform上的测试。
1.2. JUnit Jupiter
Group ID: org.junit.jupiter
Version: 5.3.0
Artifact IDs:
junit-jupiter-api
junit-jupiter-engine
JUnit Jupiter测试引擎的实现,仅仅在运行时需要。
junit-jupiter-params
支持JUnit Jupiter中的 参数化测试。
junit-jupiter-migration-support
支持从JUnit 4迁移到JUnit Jupiter,仅在使用了JUnit 4规则的测试中才需要。
1.3. JUnit Vintage
Group ID: org.junit.vintage
Version: 5.3.0
Artifact ID:
junit-vintage-engine
JUnit Vintage测试引擎实现,允许在新的JUnit Platform上运行低版本的JUnit测试,即那些以JUnit 3或JUnit 4风格编写的测试。
2编写测试
package TestJunit5; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class _1_test { @Test void testIntAddition() { assertEquals(2, 1 + 1); } } |
2.1 注解
除非另有说明,否则所有核心注释都位于junit-jupiter-api模块的org.junit.jupiter.api包中。
注解 |
描述 |
@Test |
表示该方法是一个测试方法。与JUnit 4的@Test注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们自己的专用注解来完成的。这样的方法会被继承,除非它们被覆盖。 |
@ParameterizedTest |
表示该方法是一个 参数化测试。这样的方法会被继承,除非它们被覆盖。 |
@RepeatedTest |
表示该方法是一个 重复测试 的测试模板。这样的方法会被继承,除非它们被覆盖。 |
@TestFactory |
表示该方法是一个 动态测试 的测试工厂。这样的方法会被继承,除非它们被覆盖。 |
@TestInstance |
用于配置所标注的测试类的 测试实例生命周期。这些注解会被继承。 |
@TestTemplate |
表示该方法是一个 测试模板,它会依据注册的 提供者 所返回的调用上下文的数量被多次调用。 这样的方法会被继承,除非它们被覆盖。 |
@TestMethodOrder |
用于配置带注释的测试类的测试方法执行顺序;类似于JUnit 4的@FixMethodOrder。注解可以被继承。 |
@DisplayName |
为测试类或测试方法声明一个自定义的显示名称。该注解不能被继承。 |
@DisplayNameGeneration |
声明测试类的自定义显示名称生成器。注解会被继承。 |
@BeforeEach |
表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行;类似于JUnit 4的 @Before。这样的方法会被继承,除非它们被覆盖。 |
@AfterEach |
表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后 执行;类似于JUnit 4的 @After。这样的方法会被继承,除非它们被覆盖。 |
@BeforeAll |
表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行;类似于JUnit 4的 @BeforeClass。这样的方法会被继承(除非它们被隐藏 或覆盖),并且它必须是 static方法(除非"per-class" 测试实例生命周期 被使用)。 |
@AfterAll |
表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行;类似于JUnit 4的 @AfterClass。这样的方法会被继承(除非它们被隐藏 或覆盖),并且它必须是 static方法(除非"per-class" 测试实例生命周期 被使用)。 |
@Nested |
表示使用了该注解的类是一个内嵌、非静态的测试类。@BeforeAll和@AfterAll方法不能直接在@Nested测试类中使用,(除非"per-class" 测试实例生命周期 被使用)。该注解不能被继承。 |
@Tag |
用于声明过滤测试的tags,该注解可以用在方法或类上;类似于TesgNG的测试组或JUnit 4的分类。该注解能被继承,但仅限于类级别,而非方法级别。 |
@Disable |
用于禁用一个测试类或测试方法;类似于JUnit 4的@Ignore。该注解不能被继承。 |
@ExtendWith |
用于注册自定义 扩展。该注解不能被继承。 |
@RegisterExtension |
用于通过属性以编程方式注册扩展。这些属性是继承的,除非它们被(子类)屏蔽。 |
@TempDir |
用于通过生命周期方法或测试方法中的字段注入或参数注入来提供临时目录。位于org.junit.jupiter.api.io包中。 |
还有一些注解在实验中。具体可以看Experimental APIs 表。
2.2 测试类和测试方法
测试类和测试方法可以不为public,但必须不为private。
这里先记得@BeforeAll和@AfterAll修饰的是静态方法
2.3 显示名称
这里@DisplayName注解用来标注名称,方便查看。
@DisplayNameGeneration,是一个用来生成DisplayName的注解,配合DisplayNameGenerator类使用。@DisplayName注解的优先级更高
class DisplayNameGeneratorDemo { @Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @Test void if_it_is_zero() { } @DisplayName("A negative value for year is not supported by the leap year computation.") @ParameterizedTest(name = "For example, year {0} is not supported.") @ValueSource(ints = {1, -4}) void if_it_is_negative(int year) { } } } |
生成结果如下,可以看到使用这个注解之后,测试之后的名称下划线消失了,这是因为传入了DisplayNameGenerator.ReplaceUnderscores.class,然后可以看到的确是@DisplayName的优先级更高一些,下面的方法的名称是@DisplayName传入进去的。
然后如果想要自定义名称生成的规则,可以去继承内部的ReplaceUnderscores 进行改进。
2.4 断言
所有的JUnit Jupiter断言都是 org.junit.jupiter.api.Assertions类中static方法。可以使用Lambda表达式。
然后如果断言不能满足要求,可以导入第三方的断言库。
2.5 假设
假设与断言的区别:假设失败则停止测试,断言失败则抛出错误。假设可以配合断言使用
@Test void testInAllEnvironments() { assumingThat("CI".equals(System.getenv("ENV")), () -> { // 假设成立才去执行断言 assertEquals(2, 2); }); // perform these assertions in all environments assertEquals(42, 42); } |
2.6 禁用测试
@Disabled可以禁用测试,再或者通过自定义的 ExecutionCondition 来禁用 整个测试类或单个测试方法。
2.7 有条件的执行测试
JUnit Jupiter中的 ExecutionCondition 扩展API允许开发人员以编程的方式基于某些条件启用或禁用容器或测试。这种情况的最简单示例是内置的 DisabledCondition,它支持 @Disabled 注解(请参阅 禁用测试)。除了@Disabled之外,JUnit Jupiter还支持 org.junit.jupiter.api.condition包中的其他几个基于注解的条件,允许开发人员以 声明的方式启用或禁用容器和测试。
2.7.1 操作系统条件
可以通过 @EnabledOnOs 和 @DisabledOnOs 注释在特定操作系统上启用或禁用容器或测试。
2.7.2 操作系统条件
可以通过 @EnabledOnJre 和 @DisabledOnJre 注解在特定版本的Java运行时环境(JRE)上启用或禁用容器或测试。
2.7.3 系统属性条件
可以通过 @EnabledIfSystemProperty 和 @DisabledIfSystemProperty 注解根据指定的JVM系统属性的值启用或禁用容器或测试。通过matches属性提供的值将被解释为正则表达式。
2.7.5 脚本条件
根据对通过 @EnabledIf 或 [@DisabledIf](https://junit.org/junit5/docs/5.3.0/api/org/junit/jupiter/api/condition/DisabledIf.html) 注解配置的脚本的评估,JUnit Jupiter提供了 启用或禁用 容器或测试的功能。脚本可以用JavaScript,Groovy或任何其他支持Java脚本API的脚本语言编写,由JSR 223定义。
脚本绑定
以下名称绑定到每个脚本上下文,因此在脚本中使用。访问器 通过简单的String get(String name)方法提供对类似Map结构的访问。
名称 |
类型 |
描述 |
systemEnvironment |
accessor |
操作系统环境变量访问器。 |
systemProperty |
accessor |
JVM 系统属性访问器。 |
JunitConfigurationParameter |
accessor |
配置参数访问器。 |
JunitDisplayName |
String |
测试或容器的显示名称。 |
junitTags |
Set<String> |
所有分配给测试或容器的标记。 |
junitUniqueId |
String |
测试或容器的唯一ID。 |
2.8 标记和过滤
可以用@Tag标签标记,然后后面可以根据里边传入的信息对测试进行发现和过滤。
标记的限制(trimmed指两端空格被去掉)
标记不能为null或空。
trimmed 的标记不能包含空格。
trimmed 的标记不能包含IOS字符。
trimmed 的标记不能包含一下保留字符。
,:逗号
(:左括号
):右括号
&:& 符号
|:竖线
!:感叹号
2.9 测试执行顺序
可以在类上标注@TestMethodOrder来声明测试方法要有执行顺序,里边可以传入三种类Alphanumeric、OrderAnnotation、Random,分别代表字母排序、数字排序、随机。然后对方法加@Order注解里边传入参数决定顺序。
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @TestMethodOrder(OrderAnnotation.class) class OrderedTestsDemo { @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(3) void validValues() { // perform assertions against valid values } } |
2.10 测试实例生命周期
为了隔离地执行单个测试方法,以及避免由于不稳定的测试实例状态引发非预期的副作用,JUnit会在执行每个测试方法执行之前创建一个新的实例。这个”per-method”测试实例生命周期是JUnit Jupiter的默认行为,这点类似于JUnit以前的所有版本。
如果你希望JUnit Jupiter在同一个实例上执行所有的测试方法,在你的测试类上加上注解@TestInstance(Lifecycle.PER_CLASS)即可。启用了该模式后,每一个测试类只会创建一次实例。因此,如果你的测试方法依赖实例变量存储的状态,你可能需要在@BeforeEach或@AfterEach方法中重置状态。
"per-class"模式相比于默认的"per-method"模式有一些额外的好处。具体来说,使用了"per-class"模式之后,你就可以在非静态方法和接口的default方法上声明@BeforeAll和 @AfterAll。因此,"per-class"模式使得在@Nested测试类中使用@BeforeAll和@AfterAll注解成为了可能。
2.10.1 更改默认的测试实例生命周期
如果测试类或测试接口上没有使用@TestInstance注解,JUnit Jupiter 将使用默认 的生命周期模式。标准的默认 模式是PER_METHOD。然而,整个测试计划执行的默认值 是可以被更改的。要更改默认测试实例生命周期模式,只需将junit.jupiter.testinstance.lifecycle.default配置参数 设置为定义在TestInstance.Lifecycle中的枚举常量名称即可,名称忽略大小写。它也作为一个JVM系统属性、作为一个传递给Launcher的LauncherDiscoveryRequest中的配置参数、或通过JUnit Platform配置文件来提供(详细信息请参阅 配置参数)。
例如,要将默认测试实例生命周期模式设置为Lifecycle.PER_CLASS,你可以使用以下系统属性启动JVM。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
但是请注意,通过JUnit Platform配置文件来设置默认的测试实例生命周期模式是一个更强大的解决方案,因为配置文件可以与项目一起被提交到版本控制系统中,因此可用于IDE和构建软件。
要通过JUnit Platform配置文件将默认测试实例生命周期模式设置为Lifecycle.PER_CLASS,你需要在类路径的根目录(例如,src/test/resources)中创建一个名为junit-platform.properties的文件,并写入以下内容。
junit.jupiter.testinstance.lifecycle.default = per_class
2.11 嵌套测试
对应@Nested注解。没有这个注解的话他就不测试内部类了。
@Nested测试类必须是非静态嵌套类(即内部类),并且可以有任意多层的嵌套。这些内部类被认为是测试类家族的正式成员,但有一个例外:@BeforeAll和@AfterAll方法默认 不会工作。原因是Java不允许内部类中存在static成员。不过这种限制可以使用@TestInstance(Lifecycle.PER_CLASS)标注@Nested测试类来绕开。
2.12 构造器和方法的依赖注入
之前版本不行,现在可以了。
ParameterResolver 为测试扩展定义了API,它可以在运行时动态解析参数。如果一个测试的构造函数方法接收一个参数,这个参数就必须在运行时被一个已注册的ParameterResolver解析。
目前有三种被自动注册的内置解析器。
①TestInfoParameterResolver:如果一个方法参数的类型是 TestInfo,TestInfoParameterResolver将根据当前的测试提供一个TestInfo的实例用于填充参数的值。然后,TestInfo就可以被用来检索关于当前测试的信息,例如:显示名称、测试类、测试方法或相关的Tag。显示名称要么是一个类似于测试类或测试方法的技术名称,要么是一个通过@DisplayName配置的自定义名称。
简单的说就是参数是TestInfo类型,那么就可以在函数中调用,这个类包含很多测试的信息。
import org.junit.jupiter.api.TestInfo; @DisplayName("TestInfo Demo") class TestInfoDemo { TestInfoDemo(TestInfo testInfo) { assertEquals("TestInfo Demo", testInfo.getDisplayName()); } @BeforeEach void init(TestInfo testInfo) { String displayName = testInfo.getDisplayName(); assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()")); } @Test @DisplayName("TEST 1") @Tag("my-tag") void test1(TestInfo testInfo) { assertEquals("TEST 1", testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains("my-tag")); } @Test void test2() { } } |
②RepetitionInfoParameterResolver:如果一个位于@RepeatedTest、@BeforeEach或者@AfterEach方法的参数的类型是 RepetitionInfo,RepetitionInfoParameterResolver会提供一个RepetitionInfo实例。然后,RepetitionInfo就可以被用来检索对应@RepeatedTest方法的当前重复以及总重复次数等相关信息。但是请注意,RepetitionInfoParameterResolver不是在@RepeatedTest的上下文之外被注册的。请参阅 重复测试示例。
③TestInfoParameterResolver:如果一个方法参数的类型是 TestReporter,TestReporterParameterResolver会提供一个TestReporter实例。然后,TestReporter就可以被用来发布有关当前测试运行的其他数据。这些数据可以通过 TestExecutionListener 的reportingEntryPublished()方法来消费,因此可以被IDE查看或包含在报告中。
在JUnit Jupiter中,你应该使用TestReporter来代替你在JUnit 4中打印信息到stdout或stderr的习惯。使用@RunWith(JUnitPlatform.class)会将报告的所有条目都输出到stdout中。
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestReporter; class TestReporterDemo { @Test void reportSingleValue(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); } @Test void reportSeveralValues(TestReporter testReporter) { HashMap<String, String> values = new HashMap<>(); values.put("user name", "dk38"); values.put("award year", "1974"); testReporter.publishEntry(values);//会在控制台输出 } } |
其他的参数解析器必须通过@ExtendWith注册合适的 扩展 来明确地开启。
可以查看 RandomParametersExtension 获取自定义 ParameterResolver 的示例。虽然并不打算大量使用它,但它演示了扩展模型和参数解决过程中的简单性和表现力。MyRandomParametersTest演示了如何将随机值注入到@Test方法中。
@ExtendWith(RandomParametersExtension.class) class MyRandomParametersTest { @Test void injectsInteger(@Random int i, @Random int j) { assertNotEquals(i, j); } @Test void injectsDouble(@Random double d) { assertEquals(0.0, d, 1.0); } } |
对于真实的使用场景,请查看 MockitoExtension 和 SpringExtension 的源码。
2.13 测试接口和默认方法
JUnit Jupiter允许将@Test、@RepeatedTest、@ParameterizedTest、@TestFactory、TestTemplate、@BeforeEach和@AfterEach注解声明在接口的default方法上。如果 测试接口或测试类使用了@TestInstance(Lifecycle.PER_CLASS)注解(请参阅 测试实例生命周期),则可以在测试接口中的static方法或接口的default方法上声明@BeforeAll和@AfterAll。
可以在测试接口上声明@ExtendWith和@Tag,以便实现了该接口的类自动继承它的tags和扩展。
2.14 重复测试
@RepeatedTest中填入次数可以重复测试。
除了指定重复次数之外,我们还可以通过@RepeatedTest注解的name属性为每次重复配置自定义的显示名称。此外,显示名称可以是由静态文本和动态占位符的组合而组成的模式。目前支持以下占位符。
{displayName}: @RepeatedTest方法的显示名称。
{currentRepetition}: 当前的重复次数。
{totalRepetitions}: 总的重复次数。
一个特定重复的默认显示名称基于以下模式生成:"repetition {currentRepetition} of {totalRepetitions}"。因此,之前的repeatTest()例子的单个重复的显示名称将是:repetition 1 of 10, repetition 2 of 10,等等。如果你希望每个重复的名称中包含@RepeatedTest方法的显示名称,你可以自定义自己的模式或使用预定义的RepeatedTest.LONG_DISPLAY_NAME。后者等同于"{displayName} :: repetition {currentRepetition} of {totalRepetitions}",在这种模式下,repeatedTest()方法单次重复的显示名称长成这样:repeatedTest() :: repetition 1 of 10, repeatedTest() :: repetition 2 of 10,等等。
为了以编程方式获取有关当前重复和总重复次数的信息,开发人员可以选择将一个RepetitionInfo的实例注入到@RepeatedTest,@BeforeEach或@AfterEach方法中。
@RepeatedTest(2) void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { assertEquals(2, repetitionInfo.getTotalRepetitions()); } @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}") @DisplayName("Repeat!") void customDisplayName(TestInfo testInfo) { assertEquals(testInfo.getDisplayName(), "Repeat! 1/1"); } @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) @DisplayName("Details...") void customDisplayNameWithLongPattern(TestInfo testInfo) { assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1"); } @RepeatedTest(value = 2, name = "Wiederholung {currentRepetition} von {totalRepetitions}") void repeatedTestInGerman() { // ... } |
测试结果如下图所示。简单地说就是可以在注解的name属性中传入测试名与占位符,这样在测试输出的时候更容易通过名称确认是哪个测试的哪一次。然后可以用之前的方法传参章节中的内容,通过在测试方法中传入TestInfo或RepetitionInfo类来实现测试的方法体中获取测试参数,如获取总共测试多少次,获得当前是第几次。
2.15 参数化测试
使用@ParameterizedTest注解,参数化测试使得测试可以测试多次使用不同的参数值。
为了使用参数化测试,你必须添加junit-jupiter-params依赖。
@ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } |
参数化测试方法通常会在参数源索引和方法参数索引之间采用一对一关联(请参阅 @CsvSource 中的示例)之后直接从配置的源中消耗参数(请参阅 参数源)。但是,参数化测试方法也可以选择将来自源的参数聚合 为传递给该方法的单个对象(请参阅 参数聚合)。其他参数也可以由ParameterResolver提供(例如,获取TestInfo,TestReporter等的实例)。具体而言,参数化测试方法必须根据以下规则声明形式参数。
首先必须声明零个或多个索引参数。
接下来必须声明零个或多个聚合器。
由ParameterResolver提供的零个或多个参数必须声明为最后一个。
2.15.1 参数源
1、@ValueSource是最简单的来源之一。它允许你指定单个数组的文字值,并且只能用于为每个参数化的测试调用提供单个参数。
@ValueSource支持以下类型的字面值:
short
byte
int
long
float
double
char
java.lang.String
java.lang.Class
为了测试一些极端情况,null啊或者为空之类的,还提供了一些额外的注解。
@NullSource注解用来给参数测试提供一个null元素,要求传参的类型不能是基本类型(基本类型不能是null值)
@EmptySource为java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays (e.g., int[], char[][], etc.), object arrays (e.g.,String[], Integer[][], etc.)类型提供了空参数。
@NullAndEmptySource 注解为上边两个注解的合并。
@ParameterizedTest @NullSource @EmptySource @ValueSource(strings = { " ", " ", " ", " " }) void nullEmptyAndBlankStrings(String text) { assertTrue(text == null || text.trim().isEmpty()); } |
可以看到@VauleSource中只有4个参数,但却有6个测试,另外两个是通过注解添加的。
2、@EnumSource能够很方便地提供Enum常量。该注解提供了一个可选的names参数,你可以用它来指定使用哪些常量。如果省略了,就意味着所有的常量将被使用,就像下面的例子所示。
@ParameterizedTest @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) void testWithEnumSourceInclude(TimeUnit timeUnit) { assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); } |
@EnumSource注解还提供了一个可选的mode参数,它能够细粒度地控制哪些常量将会被传递到测试方法中。例如,你可以从枚举常量池中排除一些名称或者指定正则表达式,如下面代码所示。第一个exclude是排除names中的枚举类型,第二个是匹配names中的枚举类型。
@ParameterizedTest @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = {"DAYS", "HOURS"}) void testWithEnumSourceExclude(TimeUnit timeUnit) { assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); assertTrue(timeUnit.name().length() > 5); } @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$") void testWithEnumSourceRegex(TimeUnit timeUnit) { String name = timeUnit.name(); assertTrue(name.startsWith("M") || name.startsWith("N")); assertTrue(name.endsWith("SECONDS")); } |
3、@MethodSource允许你引用测试类或外部类中的一个或多个工厂 方法。
除非使用@TestInstance(Lifecycle.PER_CLASS)注解标注测试类,否则测试类中的工厂方法必须是static的。 而外部类中的工厂方法必须始终是static的。 此外,此类工厂方法不能接受任何参数。
每个工厂方法必须生成一个参数流,并且流中的每组参数将被作为被@ParameterizedTest标注的方法的单独调用的物理参数来提供。 一般来说,这会转换为Arguments的Stream(即,Stream<Arguments>); 但是,实际的具体返回类型可以采用多种形式。 在此上下文中,”流?是JUnit可以可靠地转换为Stream的任何内容,例如Stream,DoubleStream,LongStream,IntStream,Collection,Iterator,Iterable,对象数组或基元数组。 流中的”参数”可以作为参数的实例,对象数组(例如,Object[])提供,或者如果参数化测试方法接受单个参数,则提供单个值。
@ParameterizedTest @MethodSource("stringProvider") void testWithSimpleMethodSource(String argument) { assertNotNull(argument); } static Stream<String> stringProvider() { return Stream.of("foo", "bar"); } |
如果你未通过@MethodSource明确提供工厂方法名称,则JUnit Jupiter将按照约定去搜索与当前@ParameterizedTest方法名称相同的工厂方法。
如果参数化测试方法声明了多个参数,则需要返回Arguments实例或对象数组的集合,流或数组,如下所示(有关支持的返回类型的更多详细信息,请参阅@MethodSource的JavaDoc)。 请注意,arguments(Object ...)是Arguments接口中定义的静态工厂方法。
@ParameterizedTest @MethodSource("stringIntAndListProvider") void testWithMultiArgMethodSource(String str, int num, List<String> list) { assertEquals(3, str.length()); assertTrue(num >=1 && num <=2); assertEquals(2, list.size()); } static Stream<Arguments> stringIntAndListProvider() { return Stream.of( Arguments.of("foo", 1, Arrays.asList("a", "b")), Arguments.of("bar", 2, Arrays.asList("x", "y")) ); } |
此外,可以引用别的类里边的静态工厂方法。用#分隔类和方法
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"); } } |
4、@CsvSource允许你将参数列表定义为以逗号分隔的值(即String类型的值)。
@CsvSource使用单引号'作为引用字符。请参考上述示例和下表中的'baz,qux'值。一个空的引用值''表示一个空的String;而一个完全空的值被当成一个null引用。如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException。
示例输入 |
生成的参数列表 |
@CsvSource({ "foo, bar" }) |
"foo", "bar" |
@CsvSource({ "foo, 'baz, qux'" }) |
"foo", "baz, qux" |
@CsvSource({ "foo, ''" }) |
"foo", "" |
@CsvSource({ "foo, " }) |
"foo", null |
@ParameterizedTest @CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" }) void testWithCsvSource(String first, int second) { assertNotNull(first); assertNotEquals(0, second); } |
5、@CsvFileSource允许你使用类路径中的CSV文件。CSV文件中的每一行都会触发参数化测试的一次调用。
@ParameterizedTest @CsvFileSource(resources = "/two-column.csv") void testWithCsvFileSource(String first, int second) { assertNotNull(first); assertNotEquals(0, second); } 文件内容为: foo, 1 bar, 2 "baz, qux", 3 与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为引号字符,请参考上面例子中的"baz,qux"值,一个空的带引号的值""表示一个空String,一个完全为空的值被当成null引用,如果null引用的目标类型是基本类型,则会抛出一个ArgumentConversionException。 |
6、@ArgumentsSource 可以用来指定一个自定义且能够复用的ArgumentsProvider。
@ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void testWithArgumentsSource(String argument) { assertNotNull(argument); } static class MyArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } } |
2.15.2 参数转换
扩展转换
JUnit Jupiter为提供给@ParameterizedTest的参数提供了 扩展基本类型转换 的支持。例如,使用@ValueSource(ints = {1,2,3})注解的参数化测试可以声明为不仅接受int类型的参数,还接受long,float或double类型的参数。
隐式转换
为了支持像@CsvSource这样的使用场景,JUnit Jupiter提供了一些内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。
例如,如果一个@ParameterizedTest方法声明了TimeUnit类型的参数,而实际上提供了一个String,此时字符串会被自动转换成对应的TimeUnit枚举常量。
回退String-to-Object转换
除了从字符串到上表中列出的目标类型的隐式转换之外,如果目标类型只声明一个合适的工厂方法 或工厂构造函数,则JUnit Jupiter还提供了一个从String自动转换为给定目标类型的回退机制,工厂方法和工厂构造函数定义如下:
工厂方法:在目标类型中声明的非私有静态方法,它接受单个String参数并返回目标类型的实例。该方法的名称可以是任意的,不需要遵循任何特定的约定。
工厂构造函数:目标类型中的一个非私有构造函数,它接受一个String参数。
如果发现多个工厂方法,它们将被忽略。如果同时发现了工厂方法 和工厂构造函数,则将使用工厂方法 而不使用构造函数。
显式转换
除了使用隐式转换参数,你还可以使用@ConvertWith注解来显式指定一个ArgumentConverter用于某个参数,例如下面代码所示。
@ParameterizedTest @EnumSource(TimeUnit.class) void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) { assertNotNull(TimeUnit.valueOf(argument)); } static class ToStringArgumentConverter extends SimpleArgumentConverter { @Override protected Object convert(Object source, Class<?> targetType) { assertEquals(String.class, targetType, "Can only convert to String"); return String.valueOf(source); } } |
显式参数转换器意味着开发人员要自己去实现它。正因为这样,junit-jupiter-params仅仅提供了一个可以作为参考实现的显式参数转换器:JavaTimeArgumentConverter。你可以通过组合注解JavaTimeArgumentConverter来使用它。
@ParameterizedTest @ValueSource(strings = { "01.01.2017", "31.12.2017" }) void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) { assertEquals(2017, argument.getYear()); } |
2.15.3 参数聚合
默认情况下,提供给@ParameterizedTest方法的每个参数对应于单个方法参数。因此,期望提供大量参数的参数源可能导致大的方法签名。
在这种情况下,可以使用ArgumentsAccessor而不是多个参数。使用此API,你可以通过传递给你的测试方法的单个参数去访问提供的参数。另外,它还支持类型转换,如 隐式转换中所述。
@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()); } |
自定义聚合器
除了使用ArgumentsAccessor直接访问@ParameterizedTest方法的参数外,JUnit Jupiter还支持使用自定义的可重用聚合器。
要使用自定义聚合器,只需实现ArgumentsAggregator接口并通过@AggregateWith注释将其注册到@ParameterizedTest方法的兼容参数中。当调用参数化测试时,聚合结果将作为相应参数的参数提供。
@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 } public class PersonAggregator implements ArgumentsAggregator { @Override public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { return new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class)); } } |
如果你发现自己在代码库中为多个参数化测试方法重复声明@AggregateWith(MyTypeAggregator.class),此时你可能希望创建一个自定义组合注解,比如@CsvToMyType,它使用@AggregateWith(MyTypeAggregator.class)进行元注解。以下示例通过自定义@CsvToPerson注解演示了这一点。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" }) void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) { // perform assertions against person } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @AggregateWith(PersonAggregator.class) public @interface CsvToPerson { } |
2.15.4 自定义显示名称
在参数化测试中,通过以下方法自定义显示方法名称。
@DisplayName("Display name of container") @ParameterizedTest(name = "{index} ==> fruit=''{0}'', rank={1}") @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" }) void testWithCustomDisplayNames(String fruit, int rank) { } |
2.15.5 生命周期和互通性
可以在函数参数列表中混合使用多种参数,但是通过@ValueSource注解传入方法中的参数必须在首位(也可以像之前章节说的对参数使用ParameterResolver进行扩展),而下边后边的TestReporter就是前边说的方法中传入参数。
@ParameterizedTest @ValueSource(strings = "foo") void testWithRegularParameterResolver(String argument, TestReporter testReporter) { testReporter.publishEntry("argument", argument); } |
2.16 测试模板
@TestTemplate 方法不是一个常规的测试用例,它是测试用例的模板。因此,它的设计初衷是用来被多次调用,而调用次数取决于注册提供者返回的调用上下文数量。所以,它必须结合 TestTemplateInvocationContextProvider 扩展一起使用。测试模板方法每一次调用跟执行常规@Test方法一样,它也完全支持相同的生命周期回调和扩展。关于它的用例请参阅 为测试模板提供调用上下文。
@RepeatedTest和@ ParameterizedTest就是固有的测试模板。
2.17 动态测试
@TestFactory注解用以实现测试的动态运行。
与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。 因此,动态测试是工厂的产物。 从技术上讲,@TestFactory方法必须返回Stream,Collection,Iterable,Iterator或DynamicNode实例数组。 DynamicNode的可实例化子类是DynamicContainer和DynamicTest。 DynamicContainer实例由显示名称和动态子节点列表组成,你可以创建任意嵌套的动态节点层次结构。 DynamicTest实例将被延迟执行,从而动态甚至非确定性地生成测试用例。
任何由@TestFactory方法返回的Stream在调用stream.close()的时候会被正确地关闭,这样我们就可以安全地使用一个资源,例如:Files.lines()。
跟@Test方法一样,@TestFactory方法不能是private或static的。但它可以声明被ParameterResolvers解析的参数。
DynamicTest是运行时生成的测试用例。它由一个显示名称 和Executable组成。Executable是一个@FunctionalInterface,这意味着动态测试的实现可以是一个lambda表达式 或方法引用。
同一个@TestFactory所生成的n个动态测试,@BeforeEach和@AfterEach只会在这n个动态测试开始前和结束后各执行一次,不会为每一个单独的动态测试都执行。
在JUnit Jupiter5.4.0 中,动态测试必须始终由工厂方法创建;不过,在后续的发行版中,这可能会得到注册工具的补充。
class DynamicTestsDemo { private final Calculator calculator = new Calculator(); // This will result in a JUnitException! @TestFactory List<String> dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterator<DynamicTest> dynamicTestsFromIterator() { return Arrays.asList( dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ).iterator(); } @TestFactory DynamicTest[] dynamicTestsFromArray() { return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) }; } @TestFactory Stream<DynamicTest> dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); } @TestFactory Stream<DynamicTest> dynamicTestsFromIntStream() { // Generates tests for the first 10 even integers. return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); } @TestFactory Stream<DynamicTest> generateRandomNumberOfTests() { // Generates random positive integers between 0 and 100 until // a number evenly divisible by 7 is encountered. Iterator<Integer> inputGenerator = new Iterator<Integer>() { Random random = new Random(); int current; @Override public boolean hasNext() { current = random.nextInt(100); return current % 7 != 0; } @Override public Integer next() { return current; } }; // Generates display names like: input:5, input:37, input:85, etc. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input; // Executes tests based on the current input value. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0); // Returns a stream of dynamic tests. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); } @TestFactory Stream<DynamicNode> dynamicTestsWithContainers() { return Stream.of("A", "B", "C") .map(input -> dynamicContainer("Container " + input, Stream.of( dynamicTest("not null", () -> assertNotNull(input)), dynamicContainer("properties", Stream.of( dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), dynamicTest("not empty", () -> assertFalse(input.isEmpty())) )) ))); } @TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); } @TestFactory DynamicNode dynamicNodeSingleContainer() { return dynamicContainer("palindromes", Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); } } |
2.18 并行执行测试
默认情况下,JUnit Jupiter测试在单个线程中按顺序运行。要并行运行测试,例如 加速执行,自5.3版本开始作为可选择的功能被加入进来。要启用并行执行,只需将junit.jupiter.execution.parallel.enabled配置参数设置为true,例如 在junit-platform.properties中(请参阅其他选项的 配置参数)。
启用后,JUnit Jupiter引擎将根据提供的 配置 完全并行地在所有级别的测试树上执行测试,同时观察声明性 同步机制。 请注意,捕获标准输出/错误 功能需要单独开启。
⚠️ 并行测试执行目前是一项实验性功能。 你被邀请尝试并向JUnit团队提供反馈,以便他们可以 改进 并最终推广此功能。
2.18.1 配置
可以使用 ParallelExecutionConfigurationStrategy 配置所需并行度和最大池大小等属性。 JUnit平台提供了两种开箱即用的实现:dynamic和fixed。 当然,你也可以实现一个custom的策略。
要选择策略,只需将junit.jupiter.execution.parallel.config.strategy配置参数设置为以下选项之一:
dynamic
根据可用处理器/核心数乘以junit.jupiter.execution.parallel.config.dynamic.factor配置参数(默认为1)计算所需的并行度。
fixed 强制使用junit.jupiter.execution.parallel.config.fixed.parallelism配置参数作为所需的并行度。
custom 允许通过强制junit.jupiter.execution.parallel.config.custom.class配置参数指定自定义 ParallelExecutionConfigurationStrategy 实现,以确定所需的配置。
如果未设置配置任何策略,则JUnit Jupiter使用因子为1的动态配置策略,即所需的并行度将等于可用处理器/核心的数量。
2.18.2 同步
在org.junit.jupiter.api.parallel包中,JUnit Jupiter提供了两种基于注解的声明性机制,用于在不同测试中使用共享资源时更改执行模式并允许同步。
如果启用了并行执行,默认情况下会同时执行所有类和方法。你可以使用 @Execution 注解更改带注解的元素及其子元素(如果有)的执行模式。有以下两种模式:
SAME_THREAD
强制执行父级使用的同一线程。例如,在测试方法上使用时,测试方法将在与包含测试类的任何@BeforeAll或@AfterAll方法相同的线程中执行。
CONCURRENT
除非存在资源约束要强制在同一线程中执行,否则执行并发。
此外,@ResourceLock 注解允许声明测试类或测试方法使用需要同步访问的特定共享资源,以确保可靠的测试执行。
如果你并行运行下面示例中的测试,你会发现它们很不稳定,即有时通过而其他时间失败。因为它们所读取的资源在写入是存在竞争。
class DynamicTestsDemo { private final Calculator calculator = new Calculator(); // This will result in a JUnitException! @TestFactory List<String> dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterator<DynamicTest> dynamicTestsFromIterator() { return Arrays.asList( dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ).iterator(); } @TestFactory DynamicTest[] dynamicTestsFromArray() { return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) }; } @TestFactory Stream<DynamicTest> dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); } @TestFactory Stream<DynamicTest> dynamicTestsFromIntStream() { // Generates tests for the first 10 even integers. return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); } @TestFactory Stream<DynamicTest> generateRandomNumberOfTests() { // Generates random positive integers between 0 and 100 until // a number evenly divisible by 7 is encountered. Iterator<Integer> inputGenerator = new Iterator<Integer>() { Random random = new Random(); int current; @Override public boolean hasNext() { current = random.nextInt(100); return current % 7 != 0; } @Override public Integer next() { return current; } }; // Generates display names like: input:5, input:37, input:85, etc. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input; // Executes tests based on the current input value. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0); // Returns a stream of dynamic tests. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); } @TestFactory Stream<DynamicNode> dynamicTestsWithContainers() { return Stream.of("A", "B", "C") .map(input -> dynamicContainer("Container " + input, Stream.of( dynamicTest("not null", () -> assertNotNull(input)), dynamicContainer("properties", Stream.of( dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), dynamicTest("not empty", () -> assertFalse(input.isEmpty())) )) ))); } @TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); } @TestFactory DynamicNode dynamicNodeSingleContainer() { return dynamicContainer("palindromes", Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); } } |
当使用该注解声明对共享资源的访问时,JUnit Jupiter引擎会使用此信息来确保不会并行运行冲突的测试。
除了用于唯一标记已使用资源的字符串之外,你还可以指定访问模式。需要对资源进行READ访问的两个测试可以彼此并行运行,除非有其他READ_WRITE访问模式的测试正在运行。
2.19 固有的扩展
JUnit团队鼓励开发者开发扩展,这里介绍一个JUnit已经含有的一个扩展。
@TempDir,实现这个注解具体功能的类网址为https://github.com/junit-team/junit5/blob/r5.4.0/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java
(可以通过实现的代码看到他继承了ParameterResolver,这个又是参数注入的内容,我们实现自定义注解都要这个样子哦),这个注解的作用是获取当前工作目录。
@Test void writeItemsToFile(@TempDir Path tempDir) throws IOException { System.out.println(tempDir); Path file = tempDir.resolve("/home/user/Coding/JavaCode/SSM/src/test/java/TestJunit5/test.txt"); // new ListWriter(file).write("a", "b", "c"); assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); } |
@TempDir不能放在构造方法上。
下边是一个例子,他还可以标注在静态属性上。
class SharedTempDirectoryDemo { @TempDir static Path sharedTempDir; @Test void writeItemsToFile() throws IOException { Path file = sharedTempDir.resolve("test.txt"); new ListWriter(file).write("a", "b", "c"); assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); } @Test void anotherTestThatUsesTheSameTempDir() { // use sharedTempDir } } |
4 运行测试
参数配置在原用户手册的4.5章节中https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params。
可以通过给jvm传参或者在根目录添加junit-platform.properties文件等方法实现JUnit的参数配置。
5 扩展模型
这一章节的内容就是如何为Junit添加自己定义的注解,自己定义的参数解析,实现更多的自己需要的功能。
5.2 注册扩展
JUnit Jupiter中的扩展可以通过 @ExtenWith 注解进行声明式注册,或者通过 @RegisterExtension 注解进行编程式注册,再或者通过Java的 ServiceLoader 机制自动注册。
这一章节所有的类都是与 Extension接口有关,可以发现继承它的一些接口是分别对应不同的扩展,通过实现这些接口来实现我们需要的功能。
5.2.1 声明式注册扩展
可以通过@ExtendWith传入类来进行扩展。可以传入多个的方法如下。可以看到这个注解的功能应该是可以引入扩展类,然后就可以获得额外的功能,@Random注解是框架所没有的。
//@ExtendWith({ FooExtension.class, BarExtension.class }) //@ExtendWith(FooExtension.class) //@ExtendWith(BarExtension.class) @ExtendWith(RandomParametersExtension.class) @Test void test(@Random int i) { // ... } |
5.2.2 编程式扩展
开发人员可以通过编程的 方式来注册扩展,只需要将测试类中的属性字段使用 @RegisterExtension 注解标注即可。
当一个扩展通过 @ExtenWith 声明式注册后,它就只能通过注解配置。相比之下,当通过@RegisterExtension注册扩展时,我们可以通过编程 的方式来配置扩展 – 例如,将参数传递给扩展的构造函数、静态工厂方法或构建器API。
@RegisterExtension 字段不能为private或null (在评估阶段) ,但可以是static或非静态。
静态字段
如果一个@RegisterExtension字段是static的,该扩展会在那些在测试类中通过@ExtendWith进行注册的扩展之后被注册。这种静态扩展 在扩展API的实现上没有任何限制。因此,通过静态字段注册的扩展可能会实现类级别和实例级别的扩展API,例如BeforeAllCallback、AfterAllCallback和TestInstancePostProcessor,同样还有方法级别的扩展API,例如BeforeEachCallback等等。
在下面的例子中,测试类中的server字段通过使用WebServerExtension所支持的构建器模式以编程的方式进行初始化。已经配置的WebServerExtension将在类级别自动注册为一个扩展 - 例如,要在测试类中所有测试方法运行之前启动服务器,以及在所有测试完成后停止服务器。此外,使用@BeforeAll或@AfterAll标注的静态生命周期方法以及@BeforeEach、@AfterEach和@Test标注的方法可以在需要的时候通过server字段访问该扩展的实例。
一个通过静态字段注册的扩展:
class WebServerDemo { @RegisterExtension static WebServerExtension server = WebServerExtension.builder() .enableSecurity(false) .build(); @Test void getProductList() { WebClient webClient = new WebClient(); String serverUrl = server.getServerUrl(); // Use WebClient to connect to web server using serverUrl and verify response assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus()); } } |
实例字段
如果@RegisterExtension字段是非静态的(例如,一个实例字段),那么该扩展将在测试类实例化之后被注册,并且在每个已注册的TestInstancePostProcessor被赋予后处理测试实例的机会之后(可能给被标注的字段注入要使用的扩展实例)。因此,如果这样的实例扩展实现了诸如BeforeAllCallback、AfterAllCallback或TestInstancePostProcessor这些类级别或实例级别的扩展API,那么这些API将不会正常执行。默认情况下,实例扩展将在那些通过@ExtendWith在方法级别注册的扩展之后被注册。但是,如果测试类是使用了@TestInstance(Lifecycle.PER_CLASS)配置,实例扩展将在它们之前被注册。
在下面的例子中,通过调用自定义lookUpDocsDir()方法并将结果提供给DocumentationExtension中的静态forPath()工厂方法,从而以编程的方式初始化测试类中的docs字段。配置的DocumentationExtension将在方法级别自动被注册为扩展。另外,@BeforeEach、@AfterEach和@Test方法可以在需要的时候通过docs字段访问扩展的实例。
一个通过静态字段注册的扩展:
class DocumentationDemo { static Path lookUpDocsDir() { // return path to docs dir } @RegisterExtension DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir()); @Test void generateDocumentation() { // use this.docs ... } } |
5.2.3 自动注册扩展
除了 声明式扩展注册 和 编程式扩展注册 支持使用注解,JUnit Jupiter还支持通过Java的java.util.ServiceLoader机制进行全局扩展注册,采用这种机制后会自动的检测classpath下的第三方扩展,并自动完成注册。
具体来说,自定义扩展可以通过在org.junit.jupiter.api.extension.Extension文件中提供其全类名来完成注册,该文件位于其封闭的JAR文件中的/META-INF/services目录下。
启用自动扩展检测
自动检测是一种高级特性,默认情况下它是关闭的。要启用它,只需要在配置文件中将 junit.jupiter.extensions.autodetection.enabled的配置参数 设置为 true即可。该参数可以作为JVM系统属性、或作为一个传递给Launcher的LauncherDiscoveryRequest中的配置参数、再或者通过JUnit Platform配置文件(详情请参阅 配置参数)来提供。
例如,要启用扩展的自动检测,你可以在启动JVM时传入如下系统参数。
-Djunit.jupiter.extensions.autodetection.enabled=true
启用自动检测功能后,通过ServiceLoader机制发现的扩展将在JUnit Jupiter的全局扩展(例如对TestInfo,TestReporter等的支持)之后被添加到扩展注册表中。
5.3 条件测试
ExecutionCondition 定为程序化的条件测试执行定义了ExtensionAPI。
这里为了搞明白怎么自定义扩展,特别是条件扩展,可以参考如下的DisabledOnOs是如何实现的。在注解中通过@ExtendWith()添加判断情况的类,DisabledOnOsCondition是继承了ExecutionCondition用以实现条件判断,根据代码可以看出,在方法中首先尝试获取注解,然后判断注解在不在,在的情况下在判断输入的值(操作系统)是否符合要求。
简单说明一下@Disabled注解的注解判断条件类DisabledCondition类在junit-jupiter-engine中。
5.4 测试实例工厂
TestInstanceFactory 为希望创建测试类实例的Extensions定义了API。
5.5 测试实例后期处理
TestInstancePostProcessor 为希望发布流程测试实例的Extensions定义了API。
关于具体示例,请查阅 MockitoExtension 和 SpringExtension 的源代码。
5.6 参数解析
ParameterResolver 定义了用于在运行时动态解析参数的ExtensionAPI。
为了实现传入自定义的参数,我们要实现对应的参数解析器。可以参照TestInfoParameterResolver这个类来写参数解析器,这个解析器解析的是在之前章节提到过的三种Junit自带的参数解析器,对应解析TestInfo类,可以在测试方法中传入TestInfo参数获取对应测试的相关信息。
5.7 测试结果处理
TestWatcher提供API以实现扩展用以处理测试方法的结果。通过看这个类的注释,大概知道这个接口用来提供测试方法被跳过或者被执行等结果进行报告。
其中四种方法分别对应
testDisabled: 被@Disabled注释的方法跳过后
testSuccessful: 成功测试
testAborted:测试终止
testFailed: 测试失败
5.8 测试生命周期回调
下列接口定义了用于在测试执行生命周期的不同阶段来扩展测试的API。关于每个接口的详细信息,可以参考后续章节的示例,也可以查阅 org.junit.jupiter.api.extension 包中的Javadoc。
AfterAllCallback
扩展开发人员可以选择在单个扩展中实现任意数量的上述接口。
BeforeTestExecutionCallback 和 AfterTestExecutionCallback 分别为Extensions定义了添加行为的API,这些行为将在执行测试方法之前 和之后立即执行。因此,这些回调非常适合于定时器、跟踪器以及其他类似的场景。如果你需要实现围绕@BeforeEach和@AfterEach方法调用的回调,实现BeforeEachCallback和AfterEachCallback即可。
以下示例展示了如何使用这些回调来统计和记录测试方法的执行时间。TimingExtension同时实现了BeforeTestExecutionCallback和AfterTestExecutionCallback接口,从而给测试执行进行计时和记录。
一个为测试方法执行计时和记录的扩展
import java.lang.reflect.Method; import java.util.logging.Logger; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger LOG = Logger.getLogger(TimingExtension.class.getName()); @Override public void beforeTestExecution(ExtensionContext context) throws Exception { getStore(context).put(context.getRequiredTestMethod(), System.currentTimeMillis()); } @Override public void afterTestExecution(ExtensionContext context) throws Exception { Method testMethod = context.getRequiredTestMethod(); long start = getStore(context).remove(testMethod, long.class); long duration = System.currentTimeMillis() - start; LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); } private Store getStore(ExtensionContext context) { return context.getStore(Namespace.create(getClass(), context)); } } |
由于TimingExtensionTests类通过@ExtendWith注册了TimingExtension,所以,测试将在执行时应用这个计时器。
一个使用示例TimingExtension的测试类
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(TimingExtension.class) class TimingExtensionTests { @Test void sleep20ms() throws Exception { Thread.sleep(20); } @Test void sleep50ms() throws Exception { Thread.sleep(50); } } |
测试结果如下图所示。
5.9 异常处理
TestExecutionExceptionHandler 为Extensions定义了异常处理的API,从而可以处理在执行测试时抛出的异常。
5.10 为测试模板提供调用上下文
当至少有一个 TestTemplateInvocationContextProvider 被注册时,标注了 @TestTemplate 的方法才能被执行。每个这样的provider负责提供一个 TestTemplateInvocationContext 实例的Stream。每个上下文都可以指定一个自定义的显示名称和一个额外的扩展名列表,这些扩展名仅用于下一次调用 @TestTemplate 方法。
以下示例展示了如何编写测试模板以及如何注册和实现一个 TestTemplateInvocationContextProvider。
public class MyTestTemplateInvocationContextProviderTest { final List<String> fruits = Arrays.asList("apple", "banana", "lemon"); //这里的作用是想将字符串对象传入String fruit中 //所谓测试模板肯定是要能传入一系列的参数,所以在具体实现中是要返回一个stream对象 @TestTemplate @ExtendWith(MyTestTemplateInvocationContextProvider.class) void testTemplate(String fruit) { assertTrue(fruits.contains(fruit)); } } |
//TestTemplateInvocationContextProvider继承这个接口以实现测试模板 public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider { //是否支持测试模板,true为开启支持 @Override public boolean supportsTestTemplate(ExtensionContext context) { return true; }
@Override public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts( ExtensionContext context) { return Stream.of(invocationContext("apple"), invocationContext("banana")); } //这里选择含两个元素的stream流,需要将对应的String类型转换为TestTemplateInvocationContext类型 private TestTemplateInvocationContext invocationContext(String parameter) { return new TestTemplateInvocationContext() { //这里将传入的String参数设置为测试中的DisplayName @Override public String getDisplayName(int invocationIndex) { return parameter; }
@Override public List<Extension> getAdditionalExtensions() { return Collections.singletonList(new ParameterResolver() { //判断是否是//判断是否是String类型String类型 @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.getParameter().getType().equals(String.class); } //把String类型参数直接返回,我们需要解析的就是String类型,如果是别的类型肯定要涉及转换 @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameter; } }); } }; } } |
之前讲过测试模板的例子,@RepeatedTest和@ParameterizedTest就是两个典型,我们如果要写自己的测试模板可以参照这两个例子的实现,简单看下。
可以看到@RepeatedTest的确是包含了@TestTemplate。
然后具体的注解处理类在junit-platform-engine中的RepeatedTestExtension类中,可以看到熟悉的要重写的方法,第一个supportsTestTemplate方法中的逻辑是“方法是否被RepeatedTest注解标注”,第二个provideTestTemplateInvocationContexts方法中的逻辑是“返回IntStream流,因为@RepeatedTest的注解里边参数是传的int类型”。
5.11 在扩展中保持状态
通常,扩展只实例化一次。随之而来的相关问题是:开发者如何能够在两次调用之间保持扩展的状态?ExtensionContext API提供了一个Store用来解决这一问题(参见测试生命周期回调那个例子)。扩展可以将值放入Store中供以后检索。请参阅 TimingExtension 了解如何使用具有方法级作用域的Store。要注意,在测试执行期间,被存储在一个ExtensionContext中的值在周围其他的ExtensionContext中是不可用的。由于ExtensionContexts可能是嵌套的,因此内部上下文的范围也可能受到限制。请参阅相应的Javadoc来了解有关通过 Store 存储和检索值的方法的详细信息。
5.12 在扩展中支持的实用程序
junit-platform-commons公开了一个名为 的包,它包含了用于处理注解、类、反射和类路径扫描任务且正在维护中的实用工具方法。TestEngine和Extension开发人员(authors)应该被鼓励去使用这些方法,以便与JUnit Platform的行为保持一致。
5.12.1 注解支持
AnnotationSupport提供对注解元素(例如包、注解、类、接口、构造函数、方法和字段)进行操作的静态实用工具方法。这些方法包括检查元素是否使用特定注释进行注解或元注解,搜索特定注解以及如何在类或界面中查找注解的方法和字段。其中一些方法搜索已实现的接口和类层次结构以查找注解。有关更多详细信息,请参阅JavaDoc的 AnnotationSupport。
5.12.2. 类支持
ClassSupport提供静态工具方法来处理类(即java.lang.Class的实例)。有关详细信息,请参阅JavaDoc的 ClassSupport。
5.12.3 反射支持
ReflectionSupport提供了静态实用工具方法,以增强标准的JDK反射和类加载机制。这些方法包括扫描类路径以搜索匹配了指定谓词的类,加载和创建类的新实例以及查找和调用方法。其中一些方法可以遍历类层次结构以找到匹配的方法。有关更多详细信息,请参阅JavaDoc的 ReflectionSupport。
5.12.4. Modifier Support
ModifierSupport provides static utility methods for working with member and class modifiers — for example, to determine if a member is declared as public, private, abstract, static, etc. Consult the Javadoc for ModifierSupport for further details.
5.13 用户代码和扩展的相对执行顺序
5.13.1 用户和扩展代码
当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法外,还会调用大量的回调函数。 下图说明了用户提供的代码和扩展代码的相对顺序。
用户代码和扩展代码
用户提供的测试和生命周期方法以橙色表示,扩展提供的回调代码由蓝色显示。灰色框表示单个测试方法的执行,并将在测试类中对每个测试方法重复执行。
下表进一步解释了 用户代码和扩展代码 图中的十二个步骤。
步骤 |
接口/注解 |
描述 |
1 |
接口org.junit.jupiter.api.extension.BeforeAllCallback |
执行所有容器测试之前执行的扩展代码 |
2 |
注解org.junit.jupiter.api.BeforeAll |
执行所有容器测试之前执行的用户代码 |
3 |
接口org.junit.jupiter.api.extension.BeforeEachCallback |
每个测试执行之前执行的扩展代码 |
4 |
注解org.junit.jupiter.api.BeforeEach |
每个测试执行之前执行的用户代码 |
5 |
接口org.junit.jupiter.api.extension.BeforeTestExecutionCallback |
测试执行之前立即执行的扩展代码 |
6 |
注解org.junit.jupiter.api.Test |
真实测试方法的用户代码 |
7 |
接口org.junit.jupiter.api.extension.TestExecutionExceptionHandler |
用于处理测试期间抛出的异常的扩展代码 |
8 |
接口org.junit.jupiter.api.extension.AfterTestExecutionCallback |
测试执行后立即执行的扩展代码 |
9 |
注解org.junit.jupiter.api.AfterEach |
每个执行测试之后执行的用户代码 |
10 |
接口org.junit.jupiter.api.extension.AfterEachCallback |
每个执行测试之后执行的扩展代码 |
11 |
注解org.junit.jupiter.api.AfterAll |
执行所有容器测试之后执行的用户代码 |
12 |
接口org.junit.jupiter.api.extension.AfterAllCallback |
执行所有容器测试之后执行的扩展代码 |
在最简单的情况下,只有实际的测试方法被执行(步骤6); 所有其他步骤都是可选的,具体包含的步骤将取决于是否存在用户代码或对相应生命周期回调的扩展支持。有关各种生命周期回调的更多详细信息,请参阅每个注解和扩展各自的JavaDoc。
5.13.2 回调的包装行为
对应https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-wrapping-behavior 章节,大致就是讲不同自定义扩展被加入之后的执行顺序和涉及类继承关系时的测试执行顺序。