以下学习内容基于JUnit5英文文档和中文文档
目录
JUnit 5是什么
注解说明
元注解和组合注解
标准测试类
显示名称(DisplayName)
断言(Assertions)
假设(Assumptions)
条件测试执行(Conditional Test Execution)
标签和过滤(Tagging and Filtering)
测试执行顺序
测试方法执行顺序
测试类执行顺序
测试实例生命周期(Test Instance Lifecycle)
嵌套测试(Nested Tests)
对构造函数和方法的依赖注入(Dependency Injection for Constructors and Methods)
测试接口和默认方法(Test Interfaces and Default Methods)
重复测试(Repeated Tests)
参数化测试(Parameterized Tests)
@ValueSource
@NullSource
@EmptySource
@NullAndEmptySource
可以组合使用@NullSource, @EmptySource和@ValueSource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentsSource
参数转换(Argument Conversion)
参数聚合(Argument Conversion)
自定义显示的名字(Customizing Display Names)
生命周期和互操作性(Lifecycle and Interoperability)
测试模板(Test Templates)
动态测试(Dynamic Tests)
超时(Timeouts)
JUnit 5 = JUnit Platform(基础平台) + JUnit Jupiter(核心程序) + JUnit Vintage(老版本的支持)
JUnit Platform
在JVM上启动测试框架(launching testing frameworks)的基础。
它还定义了用于开发平台上运行的测试框架的测试引擎(TestEngine)API。
此外,该平台还提供了一个控制台启动器(Console Launcher),可以从命令行启动平台,并为 Gradle 和 Maven 构建插件,以及一个基于JUnit 4的运行器(JUnit 4 based Runner),用于在平台上运行任何 TestEngine 。
JUnit Jupiter
在JUnit 5中编写测试和扩展的新编程模型( programming model )和扩展模型( extension model )的组合。
另外,Jupiter子项目还提供了一个TestEngine,用于在平台上运行基于Jupiter的测试。
JUnit Vintage
提供了一个在平台上运行JUnit 3和JUnit 4的 TestEngine 。
@Test
表示方法是一种测试方法。
与JUnit 4的 @Test 注解不同,这个注解没有声明任何属性,因为JUnit Jupiter的测试扩展是基于它们自己的专用注解进行操作的。
这些方法可以被继承,除非它们被重写。
@ParameterizedTest
表示方法是 parameterized test(参数化测试)。
这些方法可以被继承,除非它们被重写。
@RepeatedTest
表示方法是 repeated test(重复测试)的测试模板。
这些方法可以被继承,除非它们被重写。
@TestFactory
表示方法是用于dynamic tests(动态测试)的测试工厂。
这些方法可以被继承,除非它们被重写。
@TestTemplate
表示方法是用来根据注册providers(提供者)返回的调用上下文多次调用的template for test cases(测试用例的模板)。
这些方法可以被继承,除非它们被重写。
@TestClassOrder
用于配置带有此注解的测试类中@Nested测试类的执行顺序。
这些注解可以被继承。
@TestMethodOrder 类似JUnit4的@FixMethodOrder
用于配置带有此注解的测试类中测试方法的执行顺序。
这些注解可以被继承。
@TestInstance
用于为带注解的测试类配置test instance lifecycle(测试实例生命周期)。
这些注解可以被继承。
@DisplayName
声明测试类或测试方法的自定义显示名称。
这样的注解不能被继承。
@DisplayNameGeneration
为测试类配置显示名称生成器。
这些注解可以被继承。
@BeforeEach 类似JUnit4的@Before
表示在当前类中每个 @Test, @RepeatedTest, @ParameterizedTest或 @TestFactory 方法执行前都要执行这个方法;
这些方法可以被继承,除非它们被重写。
@AfterEach 类似JUnit4的@After
表示在当前类中每个@Test, @RepeatedTest, @ParameterizedTest或 @TestFactory方法执行后都要执行这个方法;
这些方法可以被继承,除非它们被重写。
@BeforeAll 类似JUnit4的@BeforeClass
表示在当前类中只运行一次,在所有@Test, @RepeatedTest, @ParameterizedTest或 @TestFactory方法执行前运行;
这些方法可以被继承的(除非它们是隐藏的或覆盖的),并且必须是 static 的(除非使用“per-class”test instance lifecycle (测试实例生命周期))。
@AfterAll 类似JUnit4的@AfterClass
表示在当前类中只运行一次,在所有@Test, @RepeatedTest, @ParameterizedTest或 @TestFactory方法执行后运行;
这些方法可以被继承(除非它们是隐藏的或覆盖的),并且必须是静态的(除非使用“per-class”test instance lifecycle (测试实例生命周期))。
@Nested
表示带注解的类是内嵌的非静态测试类。
@BeforeAll 和 @AfterAll方法不能直接在 @Nested 测试类中使用,(除非使用“per-class”test instance lifecycle (测试实例生命周期))。
这样的注解不能被继承。
@Tag
用于在类或方法级别为过滤测试声明tags;类似于TestNG中的测试组或JUnit 4中的分类。
此注解只能用于类级别不能用在方法级别。
@Disabled 类似JUnit4的@Ignore
用于禁用测试类或测试方法;
这样的注解不能被继承。
@Timeout
用于在测试、测试工厂、测试模板或生命周期方法的执行超过给定持续时间时使其失败。
这些注解可以被继承。
@ExtendWith
以声明的方式注册自定义 extensions (扩展)。
这些注解不能被继承。
@RegisterExtension
在测试类中注释字段来以编程方式注册扩展。
当扩展通过@RegisterExtension注册时,可以通过编程方式配置它——例如,为了将参数传递给扩展的构造函数、静态工厂方法或构建器API。
@RegisterExtension 字段不能是 private 或 null(在评估时),但是可以是静态的,也可以是非静态的。
这些字段可以被继承,除非它们被隐藏。
@TempDir
用于在生命周期方法或测试方法中通过字段注入或参数注入提供临时目录;
位于org.unit.jupiter.api.io包中。
JUnit Jupiter注解可以用作元注解。这意味着您可以定义自己的组合注解,它将自动继承其元注解的语义。
比如下面定义了新注解@Fast,可以替代@Tag("fast")
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
可以在测试类或者测试方法上使用@DisplayName("A special test case")定义显示名称
可以在测试类上使用@DisplayNameGeneration设置显示名称生成器
自带可选的生成器:
Standard | 默认生成器 可以设置配置项目改变默认生成器 junit.jupiter.displayname.generator.default = org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores |
Simple | 去除不带参数的方法后面的"()" |
ReplaceUnderscores | 使用空格替换下划线 |
IndicativeSentences | 通过连接测试方法和封闭类的名称来生成完整名称 |
// 名称:DisplayNameGeneratorDemo
class DisplayNameGeneratorDemo {
// 名称:A year is not supported
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
// 名称:if it is zero()
@Test
void if_it_is_zero() {
}
// 名称:A negative value for year is not supported by the leap year computation.
// For example, year -1 is not supported.
// For example, year -4 is not supported.
@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) {
}
}
// 名称:A year is a leap year
@Nested
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_a_leap_year {
// 名称:A year is a leap year -> if it is divisible by 4 but not by 100.
@Test
void if_it_is_divisible_by_4_but_not_by_100() {
}
// 名称:A year is a leap year -> if it is one of the following years.
// Year 2016 is a leap year.
// Year 2020 is a leap year.
// Year 2048 is a leap year.
@ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints = { 2016, 2020, 2048 })
void if_it_is_one_of_the_following_years(int year) {
}
}
}
JUnit Jupiter 附带了许多JUnit 4所拥有的断言方法,并添加了一些可以很好地使用Java 8 lambdas的方法。
所有的JUnit Jupiter断言都是org.junit.jupiter.api.Assertions (断言类)中的静态方法。
// 断言 expected == actual
assertEquals(type expected, type actual)
assertEquals(type expected, type actual, String message)
assertEquals(type expected, type actual, Supplier
// 断言 delta > 0.0 && Math.abs(expected - actual) <= delta
assertEquals(type expected, type actual, type delta)
assertEquals(type expected, type actual, type delta, String message)
assertEquals(type expected, type actual, type delta, Supplier
// 相反断言
assertNotEquals,参数与assertEquals一样
// 断言 expected和actual数组元素个数一致 && 内容全相等
assertArrayEquals(type[] expected, type[] actual)
assertArrayEquals(type[] expected, type[] actual, String message)
assertArrayEquals(type[] expected, type[] actual, Supplier
// 断言 delta > 0.0 && expected和actual数组元素个数一致 && 每个元素符合Math.abs(expected[i] - actual[i]) <= delta
assertArrayEquals(type[] expected, type[] actual, type delta)
assertArrayEquals(type[] expected, type[] actual, type delta, String message)
assertArrayEquals(type[] expected, type[] actual, type delta, Supplier
// 断言 expected和actual包含的元素一致,支持嵌套的Iterable
assertIterableEquals(Iterable expected, Iterable actual)
assertIterableEquals(Iterable expected, Iterable actual, String message)
assertIterableEquals(Iterable expected, Iterable actual, Supplier
// 断言 expectedLines和actualLines都不为空 && 内容全相等
assertLinesMatch(List
assertLinesMatch(List
assertLinesMatch(List
assertLinesMatch(Stream
assertLinesMatch(Stream
assertLinesMatch(Stream
// 断言 expected和actual指向同一个对象
assertSame(Object expected, Object actual)
assertSame(Object expected, Object actual, String message)
assertSame(Object expected, Object actual, Supplier
// 相反断言
assertNotSame,参数与assertSame一样
// 断言 condition == true 或者 booleanSupplier返回true
assertTrue(boolean condition)
assertTrue(boolean condition, String message)
assertTrue(boolean condition, Supplier
assertTrue(BooleanSupplier booleanSupplier)
assertTrue(BooleanSupplier booleanSupplier, String message)
assertTrue(BooleanSupplier booleanSupplier, Supplier
// 相反断言
assertFalse,参数与assertTrue一样
// 断言 actual == null
assertNull(Object actual)
assertNull(Object actual, String message)
assertNull(Object actual, Supplier
// 相反断言
assertNotNull,参数与assertNull一样
// 断言 expectedType.isInstance(actualValue),并返回类型转换后的对象
// 断言 executable执行后抛出expectedType类型的异常
// 断言 executable执行后抛出expectedType精准类型的异常
// 断言 executable或supplier执行后不抛出任何异常
assertDoesNotThrow(Executable executable)
assertDoesNotThrow(Executable executable, String message)
assertDoesNotThrow(Executable executable, Supplier
assertDoesNotThrow(ThrowingSupplier
assertDoesNotThrow(ThrowingSupplier
assertDoesNotThrow(ThrowingSupplier
// 断言 executable或supplier在timeout之内执行完成,会在主线程等待执行完成
assertTimeout(Duration timeout, Executable executable)
assertTimeout(Duration timeout, Executable executable, String message)
assertTimeout(Duration timeout, Executable executable, Supplier
// 断言 executable或supplier在timeout之内执行完成,会重开线程执行,如果超时会对执行线程做打断处理
assertTimeoutPreemptively(Duration timeout, Executable executable)
assertTimeoutPreemptively(Duration timeout, Executable executable, String message)
assertTimeoutPreemptively(Duration timeout, Executable executable, Supplier
// 断言所有可执行代码不抛出异常
assertAll(String heading, Collection
assertAll(String heading, Stream
assertAll(String heading, Executable... executables)
assertAll(Collection
assertAll(Stream
assertAll(Executable... executables)
示例:
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
}
);
// 抛出失败
fail()
fail(String message)
fail(String message, Throwable cause)
fail(Throwable cause)
fail(Supplier
第三方断言库
尽管JUnit Jupiter提供的断言工具对于许多测试场景来说已经足够了,但有时还需要更多的和额外的功能,如matchers(匹配器)。
JUnit团队建议使用诸如AssertJ、Hamcrest、Truth等第三方断言库,因此开发人员可以自由使用他们选择的断言库。
例如,可以使用 matchers(匹配器)和一个 fluent(流式调用) API的组合使断言更具描述性和可读性。
然而,JUnit Jupiter的断言类没有像JUnit 4的 org.junit.Assert 那样提供一个assertThat() 方法,此方法接受Hamcrest Matcher。
鼓励开发人员使用第三方断言库提供的对 matchers(匹配器)的内置支持。
例如使用Hamcrest的库:
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
例如使用assertJ的库:
assertThat(res.getContent()).isEqualTo(valueList);
当然,可以继续使用基于JUnit 4遗留的编程模型 org.junit.Assert#assertThat 进行测试。
JUnit Jupiter 附带了JUnit 4提供的Assumptions(假设)方法的子集,并添加了一些可以很好地使用Java 8 lambdas的方法。
所有的JUnit Jupiter假设都是 org.junit.jupiter.api.Assumptions 类的静态方法。
与失败的断言形成直接对比的是,失败的假设不会导致测试失败;相反,一个失败的假设会导致测试中止。
// 假设 assumption或assumptionSupplier为true,如果成立则继续执行,否则退出测试方法。
assumeTrue(boolean assumption)
assumeTrue(boolean assumption, String message)
assumeTrue(boolean assumption, Supplier
assumeTrue(BooleanSupplier assumptionSupplier)
assumeTrue(BooleanSupplier assumptionSupplier, String message)
assumeTrue(BooleanSupplier assumptionSupplier, Supplier
// 相反假设
assumeFalse,参数与assumeTrue一致
// 假设 assumption或assumptionSupplier为true,如果成立则执行executable,否则什么都不做
assumingThat(boolean assumption, Executable executable)
assumingThat(BooleanSupplier assumptionSupplier, Executable executable)
// 退出测试方法
abort()
abort(String message)
abort(String message)
禁用
在测试类或者方法上使用@Disabled或@Disabled("some msg")
按操作系统和架构进行启用或禁用
@EnabledOnOs(MAC)
@EnabledOnOs({ LINUX, MAC })
@EnabledOnOs(architectures = "aarch64")
@EnabledOnOs(value = MAC, architectures = "aarch64")
@DisabledOnOs(WINDOWS)
@DisabledOnOs(architectures = "x86_64")
@DisabledOnOs(value = MAC, architectures = "aarch64")
按JAVA运行时启用或禁用
@EnabledOnJre(JAVA_8)
@EnabledOnJre({ JAVA_9, JAVA_10 })
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
@EnabledForJreRange(min = JAVA_9)
@EnabledForJreRange(max = JAVA_11)
@DisabledOnJre(JAVA_9)
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
@DisabledForJreRange(min = JAVA_9)
@DisabledForJreRange(max = JAVA_11)
按是否使用GraalVM本地镜像进行启用或禁用
@EnabledInNativeImage
@DisabledInNativeImage
按系统属性值进行启用或禁用
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
按环境变量值进行启用或禁用
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
使用自定义条件
@EnabledIf("customCondition")
@DisabledIf("example.ExternalCondition#customCondition")
boolean customCondition() {
return true;
}
class ExternalCondition {
static boolean customCondition() {
return true;
}
}
可以对测试类和方法进行标记。这些标记稍后可以用于过滤 test discovery and execution(测试发现和执行)。
标签的语法规则(Syntax Rules for Tags)
标记不能为null或blank
一个修剪标签(trimmed tag)不能包含空格
一个修剪标签(trimmed tag)不能包含ISO控制字符
一个修剪标签(trimmed tag)不能包含以下保留字符
,、(、)、&、|、!
标签表达式
product
catalog | shipping
catalog & shipping
product & !end-to-end
(micro | integration) & (product | shipping)
any() 表示注解了任意标签的类/方法
none() 表示未注解标签的类/方法
在测试类上使用@TestMethodOrder指定类中测试方法的执行顺序
MethodOrderer.DisplayName | 根据测试方法的显示名称对其进行字母数字排序 |
MethodOrderer.MethodName | 根据测试方法的名称和形式参数列表对测试方法进行字母数字排序 |
MethodOrderer.OrderAnnotation | 根据@Order注解指定的值对测试方法进行数字排序 |
MethodOrderer.Random | 伪随机订购测试方法,并支持自定义种子的配置 |
MethodOrderer.Alphanumeric | 根据测试方法的名称和形式参数列表对测试方法进行字母数字排序;5.7开始使用MethodName |
设置默认
junit.jupiter.testmethod.order.default = org.junit.jupiter.api.MethodOrderer$OrderAnnotation
@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
}
@Test
@Order(2)
void emptyValues() {
}
@Test
@Order(3)
void validValues() {
}
}
内置排序类型
ClassOrderer.ClassName | 根据测试类的完全限定类名对其进行字母数字排序 |
ClassOrderer.DisplayName | 根据测试类的显示名称对其进行字母数字排序 |
ClassOrderer.OrderAnnotation | 根据@Order注解指定的值对测试类进行数字排序 |
ClassOrderer.Random | 伪随机订购测试类,并支持自定义种子的配置 |
设置全局排序类型
junit.jupiter.testclass.order.default = org.junit.jupiter.api.ClassOrderer$OrderAnnotation
在一个类上使用@TestClassOrder注解,只会影响其@Nested类的执行顺序
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
}
}
}
为了隔离的执行单独的测试方法,并且为了避免由于可变测试实例状态而产生的意外副作用,JUnit在执行每个测试方法之前创建了一个新的测试类的实例。
这个“per-method”测试实例生命周期是JUnit Jupiter上的默认行为,类似于所有以前的JUnit版本。
如果您希望JUnit Jupiter在同一个测试实例上执行所有的测试方法,只需用@Testinstance(Lifecycle.PER_CLASS)注解您的测试类。
当使用此模式时,每个测试类将创建一个新的测试实例。
如果您的测试方法依赖于实例变量存储的状态,那么您可能需要在@BeforeEach 或 @AfterEach 方法中重置该状态。
设置默认
junit.jupiter.testinstance.lifecycle.default=per_class
嵌套测试赋予测试者更多的能力来表达几组测试之间的关系。
这样的嵌套测试利用了Java的嵌套类,并促进了对测试结构的分层思考。
只有非静态嵌套类(即内部类)可以充当@Nested测试类。
嵌套可以是任意深度的,而那些内部类被认为是测试类家族的完整成员,只有一个例外:@BeforeAll 和 @AfterAll 方法在默认情况下不工作。
原因是在Java16之前不允许内部类中的 static 成员。
但是,可以通过使用@TestInstance(Lifecycle.PER_CLASS)注解@Nested的测试类来规避这个限制。
@DisplayName("A stack")
class TestingAStackDemo {
Stack
作为JUnit Jupiter的主要变化之一,测试构造函数和方法现在都允许有参数。这允许更大的灵活性,并支持构造函数和方法的依赖注入。
ParameterResolver (参数解析器)定义了用于测试扩展的API,它希望在运行时动态解析参数。
如果测试构造函数或@Test, @TestFactory, @BeforeEach, @AfterEach, @BeforeAll 或者 @AfterAll方法接受一个参数,那么参数必须在运行时由注册的 ParameterResolver (参数解析器)解析。
目前有三个内置的解析器是自动注册的。
TestInfoParameterResolver(测试信息参数解析器)
如果一个方法的参数类型是 TestInfo , TestInfoParameterResolver将供应TestInfo对应当前测试的实例作为参数的值。
然后,TestInfo可以用来检索关于当前测试的信息,比如测试的显示名称、测试类、测试方法或相关的标记。
TestInfo作为一个从JUnit 4中替代TestName规则的替代程序。
@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实例。
然后可以使用重复信息检索关于当前重复的信息以及相应的@RepeatedTest的重复次数。
但是请注意,RepetitionInfoParameterResolver不能在@RepeatedTest的上下文以外注册。
TestReporterParameterResolver(测试报告参数解析器)
如果一个方法的参数类型是TestReporter, TestReporterParameterResolver将提供一个实例。
可以使用TestReporter发布关于当前测试运行的额外数据。
数据可以通过TestExecutionListener.reportingEntryPublished()消费,因此可以通过ide查看,也可以包括在报告中。
在JUnit Jupiter 中,您应该使用TestReporter。
在JUnit 4中您可以将信息打印到stdout或stderr。
使用@RunWith(JUnitPlatform.class)将输出所有已报告的条目到stdout。
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
其他参数解析器必须通过通过@ExtendWith注册适当的扩展(extensions)来显式启用。
@ExtendWith(MockitoExtension.class)
class MyMockitoTest {
@BeforeEach
void init(@Mock Person person) {
when(person.getName()).thenReturn("Dilbert");
}
@Test
void simpleTestWithInjectedMock(@Mock Person person) {
assertEquals("Dilbert", person.getName());
}
}
JUnit Jupiter 允许@Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach, @AfterEach在接口 default 方法上声明。
如果测试接口或测试类被用@TestInstance(Lifecycle.PER_CLASS)注解,@BeforeAll 和 @AfterAll可以在测试接口static方法或接口default 方法中声明。
在继承接口的测试类执行时,从接口继承的测试方法也会执行
@ExtendWith 和 @Tag可以在测试接口上声明,以便实现接口的类自动继承其标记和扩展。
JUnit Jupiter 通过使用 @RepeatedTest 来注解一个方法并指定所需重复的总数,从而提供了重复测试指定次数的能力。
重复测试的每次调用行为都类似于执行常规的@Test方法,完全支持相同的生命周期回调和扩展。
可以通过@RepeatedTest注解的name属性为每次重复配置一个自定义显示名称。
显示名称可以是由静态文本和动态占位符组合而成的模式。目前支持以下占位符。
{displayName}: 显示@RepeatedTest方法的名称
{currentRepetition}: 当前重复计数
{totalRepetitions}: 重复的总数
如果你想要显示的名称@RepeatedTest方法包含一种重复的名称,您可以定义自己的自定义模式或使用预定义的RepeatedTest.LONG_DISPLAY_NAME模式。
后者等于"{displayName} :: repetition {currentRepetition} of {totalRepetitions}"
为了检索关于当前重复的信息和编程的总重复次数,开发人员可以选择将 RepetitionInfo 的实例注入到 @RepeatedTest , @BeforeEach ,或 @AfterEach 方法。
class RepeatedTestsDemo {
private Logger logger = // ...
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
参数化测试可以用不同的参数多次运行测试。
它们像普通的@Test方法一样被声明,但是使用@ParameterizedTest注解。
此外,您必须声明至少一个参数源,它将为每次调用提供参数。
为了使用参数化测试,您需要添加 junit-jupiter-params构件的依赖项。
可能是最简单的来源之一。它允许您指定一组原始类型的文字,并且只能用于每次调用时提供一个参数。
支持的类型有short,byte,int,long,float,double,char,boolean,String,Class
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
为带注释的@ParameterizedTest方法提供一个null参数
不能用于基础类型,比如int
为带注释的@ParameterizedTest方法提供了一个空参数,
用于以下类型的参数:String、List、Set、Map、基元数组(例如int[]、char[]等)、对象数组(例如String[]、Integer[]等)。
不支持上述类型的子类型
组合了@NullSource和@EmptySource功能的组合注解
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
提供了一种方便的方法来使用Enum常量。
可选的value参数,允许指定使用Enum。默认将使用方法第一个入参的类型。
可选的names参数,允许指定使用哪些常量。默认使用全部。
可选的mode参数,允许更细的控制。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}
允许您引用测试类的一个或多个工厂方法。这些方法必须返回流(Stream)、迭代(Iterable)、迭代器(Iterator)或参数数组。
此外,这些方法不能接受任何参数。默认情况下,这些方法必须是静态的(static ),除非测试类被@TestInstance(Lifecycle.PER_CLASS)注解
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream stringProvider() {
return Stream.of("apple", "banana");
}
静态工厂方法可以通过提供其完全限定的方法名称来引用
@MethodSource("example.StringsProviders#tinyStrings")
如果未在@MethodSource内写方法名,则会查找和当前测试方法同名的工厂方法
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream testWithDefaultLocalMethodSource() {
return Stream.of("apple", "banana");
}
如果一个测试方法声明多个参数,您则需要返回一个Arguments实例的集合或流
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
允许您以逗号分隔值来表达参数列表
@CsvSource使用单引号作(')为其引用字符。
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
表达式
@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"
允许您从类路径中使用CSV文件。
CSV文件中的每一行都产生一个参数化测试的调用。
@CsvFileSource使用了双引号(")作为引用字符。
参数resources指定基于resources文件夹的路径
参数files指定基于项目根目录的路径
参数numLinesToSkip指定忽略行数
参数delimiter/delimiterString可以指定字段分隔符
参数useHeadersInDisplayName指定是否在显示名称中使用数据
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest(name = "[{index}] {arguments}")
@CsvFileSource(resources = "/two-column.csv", useHeadersInDisplayName = true)
void testWithCsvFileSourceAndHeaders(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
可以使用@ArgumentsSource指定自定义、可重用的ArgumentsProvider
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
隐式转换(Implicit Conversion)
为了支持像@CsvSource这样的用例,JUnit Jupiter 提供了一些内置隐式类型转换器。转换过程取决于每个方法参数的声明类型。
boolean/Boolean 例如:"true" → true
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
long/Long 例如:"15", "0xF", or "017" → 15L
float/Float 例如:"1.0" → 1.0f
double/Double 例如:"1.0" → 1.0d
Enum subclass 例如:"SECONDS" → TimeUnit.SECONDS
java.io.File 例如:"/path/to/file" → new File("/path/to/file")
java.lang.Class 例如:"java.lang.Integer" → java.lang.Integer.class (use $ for nested classes, e.g. "java.lang.Thread$State")
java.lang.Class 例如:"byte" → byte.class (primitive types are supported)
java.lang.Class 例如:"char[]" → char[].class (array types are supported)
java.math.BigDecimal 例如:"123.456e789" → new BigDecimal("123.456e789")
java.math.BigInteger 例如:"1234567890123456789" → new BigInteger("1234567890123456789")
java.net.URI 例如:"https://junit.org/" → URI.create("https://junit.org/")
java.net.URL 例如:"https://junit.org/" → URI.create("https://junit.org/").toURL()
java.nio.charset.Charset 例如:"UTF-8" → Charset.forName("UTF-8")
java.nio.file.Path 例如:"/path/to/file" → Paths.get("/path/to/file")
java.time.Duration 例如:"PT3S" → Duration.ofSeconds(3)
java.time.Instant 例如:"1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0)
java.time.LocalDateTime 例如:"2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
java.time.LocalDate 例如:"2017-03-14" → LocalDate.of(2017, 3, 14)
java.time.LocalTime 例如:"12:34:56.789" → LocalTime.of(12, 34, 56, 789_000_000)
java.time.MonthDay 例如:"--03-14" → MonthDay.of(3, 14)
java.time.OffsetDateTime 例如:"2017-03-14T12:34:56.789Z" → OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.OffsetTime 例如:"12:34:56.789Z" → OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.Period 例如:"P2M6D" → Period.of(0, 2, 6)
java.time.YearMonth 例如:"2017-03" → YearMonth.of(2017, 3)
java.time.Year 例如:"2017" → Year.of(2017)
java.time.ZonedDateTime 例如:"2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.time.ZoneId 例如:"Europe/Berlin" → ZoneId.of("Europe/Berlin")
java.time.ZoneOffset 例如:"+02:30" → ZoneOffset.ofHoursMinutes(2, 30)
java.util.Currency 例如:"JPY" → Currency.getInstance("JPY")
java.util.Locale 例如:"en" → new Locale("en")
java.util.UUID 例如:"d043e930-7b3b-48e3-bdbe-5a3ccfb833db" → UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")
如果参数类型中定义了接收单个字符串的构造方法或者工厂方法,也可以从字符串转对象。
如果发现多个工厂方法,它们将被忽略。
如果发现了工厂方法和工厂构造函数,则将使用工厂方法而不是构造函数。
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
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;
}
}
显式转换(Explicit Conversion)
可以显式地指定一个ArgumentConverter来使用@ConvertWith注解来使用某个参数
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
if (source instanceof Enum) {
return ((Enum) source).name();
}
return String.valueOf(source);
}
}
如果转换器仅用于将一种类型转换为另一种类型,则可以扩展TypedArgumentConverter以避免类型检查。
public class ToLengthArgumentConverter extends TypedArgumentConverter {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return (source != null ? source.length() : 0);
}
}
显式参数转换是由测试者实现的。因此,junit-jupiter-params只提供了一个显式参数转换类:JavaTimeArgumentConverter,也可以作为一种参考实现。
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
可以使用ArgumentsAccessor访问多个参数
@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指定自定义参数聚合类
@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));
}
}
@ParameterizedTest注解的name属性来定制调用显示名称
{displayName} 表示当前方法的显示名
{index} 表示当前条数,从1开始
{arguments} 表示全部参数值,使用逗号分隔
{argumentsWithNames} 表示包含参数名的全部参数值,使用逗号分隔
{0}, {1}, … 表示具体的第几个参数值
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> first=''{0}'', second={1}")
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
void testWithCustomDisplayNames(String first, int second) {
}
参数化测试的每次调用都有与常规的@Test方法相同的生命周期。例如,@BeforeEach方法将在每次调用之前执行。
与动态测试(Dynamic Tests)类似,调用将在IDE的测试树中逐一显示。
您可以在同一个测试类中混合常规的@Test方法和@ParameterizedTest方法。
可以使用@ParameterizedTest方法使用ParameterResolver扩展。但是,由参数源解决的方法参数需要首先出现在参数列表中。
由于测试类可能包含常规测试,以及带有不同参数列表的参数化测试,来自参数源的值不会在生命周期方法(例如@BeforeEach)和测试类构造函数中解析。
@BeforeEach
void beforeEach(TestInfo testInfo) {
// ...
}
@ParameterizedTest
@ValueSource(strings = "foo")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
// ...
}
@TestTemplate方法不是常规的测试用例,而是测试用例的模板。
根据注册提供者返回的调用上下文的数量,它被设计为多次调用。
它必须使用与注册TestTemplateInvocationContextProvider的扩展。
测试模板方法的每次调用都像一个常规的@Test方法的执行,完全支持相同的生命周期回调和扩展。
final List fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
assertTrue(fruits.contains(fruit));
}
public class MyTestTemplateInvocationContextProvider
implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return parameter;
}
@Override
public List getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter;
}
});
}
};
}
}
@TestFactory方法本身并不是一个测试用例,而是一个用于测试用例的工厂。因此,动态测试是工厂的产品。
@TestFactory方法必须返回动态节点实例的流(Stream)、集合(Collection)、迭代器(Iterable、Iterator)的DynamicNode实例,DynamicNode的实例子类是DynamicContainer和DynamicTest。
DynamicContainer实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。然后,DynamicTest实例将被延迟执行,从而支持动态甚至不确定的测试用例生成。
DynamicTest是在运行时生成的测试用例。它由一个显示名称组成。Executable是一个@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用。
@TestFactory返回的任何流(Stream)都将通过调用stream.close()来适当地关闭,这样就可以安全地使用诸如Files.lines()这样的资源。
@TestFactory方法不能是私有(private)的或静态的(static),也可以选择性地声明由参数解析器(ParameterResolvers)解析的参数。
动态测试的执行生命周期与标准@Test用例的执行生命周期完全不同。
对于单个动态测试没有生命周期回调。这意味着@BeforeEach和@AfterEach方法及其相应的扩展回调是为@TestFactory方法执行的,而不是为每个动态测试执行的。
class DynamicTestsDemo {
private final Calculator calculator = new Calculator();
// This will result in a JUnitException!
@TestFactory
List dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator 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 dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
@TestFactory
Stream 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 generateRandomNumberOfTestsFromIterator() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator inputGenerator = new Iterator() {
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 displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream dynamicTestsFromStreamFactoryMethod() {
// Stream of palindromes to check
Stream inputStream = Stream.of("racecar", "radar", "mom", "dad");
// Generates display names like: racecar is a palindrome
Function displayNameGenerator = text -> text + " is a palindrome";
// Executes tests based on the current input value.
ThrowingConsumer testExecutor = text -> assertTrue(isPalindrome(text));
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
}
@TestFactory
Stream dynamicTestsFromStreamFactoryMethodWithNames() {
// Stream of palindromes to check
Stream> inputStream = Stream.of(
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream,
text -> assertTrue(isPalindrome(text)));
}
@TestFactory
Stream 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)))
));
}
}
在测试、测试工厂、测试模板或生命周期方法上使用@Timeout注解,超时后会失败
默认单位为秒,可以配置
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds500Milliseconds() {
// fails if execution time exceeds 500 milliseconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = ThreadMode.SEPARATE_THREAD)
void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
// fails if execution time exceeds 500 milliseconds, the test code is executed in a separate thread
}
}
在类上使用@Timeout注解可以对类和@Nested类内的所有测试、测试工厂、测试模板方法起作用。
在类上使用@Timeout注解对生命周期方法不起作用。
在@TestFactory方法上声明@Timeout会检查工厂方法是否在指定的持续时间内返回,但不会验证工厂生成的每个DynamicTest的执行时间
如果@TestTemplate方法上存在@Timeout,例如@RepeatedTest或@ParameterizedTest,则每次调用都会应用给定的超时。
参数threadMode
SAME_THREAD | 在主线程执行测试,如果超时会有其他线程对主线程执行interrupt |
SEPARATE_THREAD | 在新的独立线程执行测试,如果执行的代码依赖于ThreadLocal存储,则这种行为可能会导致不希望的副作用。 |
INFERRED | 根据junit.jupiter.execution.timeout.thread.mode.default的值决定,如果没有设置默认为SAME_THREAD |
默认超时
junit.jupiter.execution.timeout.default 所有可测试方法和生命周期方法的默认超时
junit.jupiter.execution.timeout.testable.method.default 所有可测试方法的默认超时
junit.jupiter.execution.timeout.test.method.default @Test方法的默认超时
junit.jupiter.execution.timeout.testtemplate.method.default @TestTemplate方法的默认超时
junit.jupiter.execution.timeout.testfactory.method.default @TestFactory方法的默认超时
junit.jupiter.execution.timeout.lifecycle.method.default 所有生命周期方法的默认超时
junit.jupiter.execution.timeout.testfactory.method.default @TestFactory方法的默认超时
junit.jupiter.execution.timeout.beforeall.method.default @BeforeAll方法的默认超时
junit.jupiter.execution.timeout.beforeeach.method.default @BeforeEach方法的默认超时
junit.jupiter.execution.timeout.aftereach.method.default @AfterEach方法的默认超时
junit.jupiter.execution.timeout.afterall.method.default @AfterAll方法的默认超时
此类配置参数的值必须采用以下不区分大小写的格式:
使用@Timeout进行轮询测试
通过为轮询的异步测试配置超时,可以确保测试不会无限期执行。
@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
while (asynchronousResultNotAvailable()) {
Thread.sleep(250); // custom poll interval
}
// Obtain the asynchronous result and perform assertions
}
全局禁用@Timeout
JUnit Jupiter支持junit.jupiter.execution.timeout.mode配置参数,用于配置何时应用超时。
有三种模式:enabled、disabled和disabled_on_debug。默认模式enabled。
当VM运行时的一个输入参数以-agentlib:jdwp或-Xrungdwp开头时,它被认为是在调试模式下运行的。此启发式方法由disabled_on_debug模式查询。