JUnit5
maven依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
编写单测
标准案例
- 方法级别必须为
public或 默认 - 返回值必须为
void @Test修饰不能有入参@ParameterizedTest修饰必须有参数
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.fail;
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() {
}
DiaplayName
package org.example.unittest;
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() {
}
}
断言
package org.example.unittest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsDemo {
@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "The optional assertion message is now the last parameter.");
assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and any
// failures will be reported together.
assertAll("address",
() -> assertEquals("John", "John"),
() -> assertEquals("User", "User")
);
}
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
}
假设
assumeTrue- 条件成立,执行第二个参数逻辑
- 条件不成立,抛出异常,后续代码不执行;
assumingThat- 条件成立执行第二个参数逻辑;
- 无论条件是否成立都不影响后续代码执行
System.getenv("ENV")- 获取环境变量中key为ENV的变量值
System.getProperty- 获取JVM启动参数
- -D设置的参数
- 获取JVM启动参数
package org.example.unittest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
class AssumptionsDemo {
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, 2);
});
// perform these assertions in all environments
assertEquals("a string", "a string");
System.out.println(System.getenv("JAVA_HOME"));
}
}
禁用测试
@Disabled
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
标签
package org.example.unittest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
- 当前用不上,后续章节前置
#### 嵌套测试
package org.example.unittest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.EmptyStackException;
import java.util.Stack;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, () -> stack.pop());
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, () -> stack.peek());
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
- 用作后续子逻辑,如新增栈后使用新增的栈对象作测试
TestInfo和TestReporter
package org.example.unittest;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TestInfoDemo {
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("tag"));
}
@Test
void test2() {
}
}
package org.example.unittest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
import java.util.HashMap;
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);
}
}
测试接口默认方法
- JUnit Jupiter允许将@Test、@RepeatedTest、@ParameterizedTest、@TestFactory、TestTemplate、@BeforeEach和@AfterEach注解声明在接⼝的default⽅法上。如果 测试接⼝或测试类使⽤了@TestInstance(Lifecycle.PER_CLASS)注解(请参阅 ),则可以在测试接⼝中的static⽅法或接⼝的default⽅法上声明@BeforeAll和@AfterAll。可以在测试接⼝上声明@ExtendWith和@Tag,以便实现了该接⼝的类⾃动继承它的tags和扩展。
- JUnit5用户文档提供了案例,但本人目前没跑通
动态测试
- 一个测试方法批量测试多个用例
package org.example.unittest;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
import java.util.*;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
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)))
));
}
public static boolean isPalindrome(String str) {
//test for end of recursion
if (str.length() < 2) {
return true;
}
//check first and last character for equality
if (str.charAt(0) != str.charAt(str.length() - 1)) {
return false;
}
//recursion call
return isPalindrome(str.substring(1, str.length() - 1));
}
}
重复测试
package org.example.unittest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RepeatedTestDemo {
@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() {
// ...
}
}
测试执行顺序
package org.example.unittest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
// perform assertions against null values
System.out.println(1);
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
System.out.println(2);
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
System.out.println(3);
}
}
- 包括注解排序、随机排序、方法名排序、DisplayName排序
@TestClassOrder结合ClassOrderer.OrderAnnotation类,定义嵌套测试类顺序
参数化测试
package org.example.unittest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class NumberHelperTest {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testIsPositiveTrue(Integer value) {
assertTrue(NumberHelper.isPositive(value), value + " is not positive");
}
@ParameterizedTest
@ValueSource(ints = {0, -1, -3, -4, -5})
void testIsPositiveFalse(Integer value) {
assertFalse(NumberHelper.isPositive(value), value + " is positive");
}
@DisplayName("test is positive number")
@ParameterizedTest(name = "actual {0}, expected {1}")
@CsvSource({"1,true", "-2,false", "3,true", "-4,false", "0,false", "6,true"})
void testCsvIsPositiveFalse(Integer value, Boolean expected, TestReporter testReporter) {
assertEquals(expected, NumberHelper.isPositive(value));
testReporter.publishEntry("hello", "world");
}
}
参数源
@ValueSource@NullSource@EmptySource@NullAndEmptySource
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@EnumSource
class EnumSourceDemo {
@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = {"DAYS", "HOURS"})
void testWithEnumSourceInclude(TimeUnit timeUnit) {
assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}
//mode匹配
@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);
}
//mode匹配
@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"));
}
}
@MethodSource
package org.example.unittest;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class ParameterizedTestDemo {
@Nested
class MethodSourceDemo {
//默认搜索与当前方法同名的工厂方法
@ParameterizedTest
@MethodSource("org.example.unittest.ParameterizedTestDemo#stringProvider")
void testWithSimpleMethodSource(String argument) {
assertNotNull(argument);
}
//多参数示例
@ParameterizedTest
@MethodSource("org.example.unittest.ParameterizedTestDemo#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<String> stringProvider() {
return Stream.of("foo", "bar");
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
Arguments.of("foo", 1, Arrays.asList("a", "b")),
Arguments.of("bar", 2, Arrays.asList("x", "y"))
);
}
}
@CvsSource
class CsvSourceDemo {
@ParameterizedTest
@CsvSource({"foo, 1", "bar, 2", "'baz, qux', 3", ",4"})
void testWithCsvSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
}
@CsvFileSource
class CsvFileSourceDemo {
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv")
void testWithCsvFileSource(String first, int second) {
assertNotNull(first);
assertNotEquals(0, second);
}
}
@ArgumentsSource
class ArgumentsSourceDemo {
@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);
}
}
参数转换
class ParamCovertDemo {
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(TimeUnit.valueOf(argument));
}
@ParameterizedTest
@ValueSource(strings = {"01.01.2017", "31.12.2017"})
void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
}
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);
}
}
参数聚合
- 参数访问器
- 自定义聚合
class ArgumentsAccessorDemo {
//参数访问器
@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());
}
//自定义聚合
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
}
package org.example.unittest;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
import java.time.LocalDate;
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(PersonAggregator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
- 自定义显示名称
//自定义显示名称
@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) {
}
与内置参数解析扩展混用
- 参数源和单测报告的混用,TestReporter等需在最后
//⽣命周期和互通性
@ParameterizedTest
@ValueSource(strings = "foo")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
####
扩展模型
注册扩展
注解注册
- 类上注册
- 方法上注册
- 注册多个
@ExtendWith(TimingExtension.class)
@ExtendWith({MockitoExtension.class})
class RegisterExtension {
@ExtendWith({MockitoExtension.class})
@Test
void mockTest() {
// ...
}
@Test
@ExtendWith({FooExtension.class, BarExtension.class})
void extensionTest() {
}
}
编程注册
public class ExtensionDemo {
@RegisterExtension
static TimingExtension extension = new TimingExtension();
@Test
void test1() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Test 1");
}
}
@RegisterExtension如果用在非静态字段上,由于JUnit默认生命周期为方法级别,此时被注册的扩展如果是BeforeAllCallback、AfterAllCallback或TestInstancePostProcessor等类级别的扩展,注入的扩展将不能正常工作
自动注册
- 开启自动注册开关
- 启动参数:-Djunit.jupiter.extensions.autodetection.enabled=true
- 配置junit-platform.properties文件:添加junit.jupiter.extensions.autodetection.enabled=true
- resources/WEB-INF/services目录下新建文件org.junit.jupiter.api.extension.Extension
- 内容需要注册扩展的全类名
条件测试
停用条件测试
- -Djunit.conditions.deactivate=org.junit.*DisabledCondition 停用@Disable
模式匹配语法
*: 停用所有条件org.junit.*: 停用org.junit包及其子包下的所有条件*.MyCondition: 停用简单类名为为MyCondition的条件*System*: deactivates every condition whose simple class name containsSystem.org.example.MyCondition: 停用全类名为org.example.MyCondition的条件
JUnit5需要升级maven-surefire-plugin插件2.22.0 or higher
常用注解
Java环境
@EnabledOnJre:指定多个 JRE 版本,只有当前测试环境 JRE 版本在此范围内才执行测试。@DisabledOnJre:指定多个 JRE 版本,只有当前测试环境 JRE 版本不在此范围内才执行测试。`@EnabledForJreRange:指定一个 JRE 版本范围,只有当前测试环境 JRE 版本在此范围内才执行测试。@DisabledForJreRange:指定一个 JRE 版本范围,只有当前测试环境 JRE 版本不在此范围内才执行测试。
OS环境
EnabledOnOs:当前系统为指定操作系统时执行测试。DisabledOnOs:当前系统为指定操作系统时不执行测试。
系统属性
@EnabledIfSystemProperty:当前系统匹配指定的系统属性名称和期望值时执行测试。@DisabledIfSystemProperty:当前系统匹配制定的系统属性名称和期望值时不执行测试。
环境变量
EnabledIfEnvironmentVariable:当前系统匹配指定的环境变量名称和期望值时执行测试。DisabledIfEnvironmentVariable:当前系统匹配制定的环境变量名称和期望值时不执行测试。
自定义
@EnableIf@DisableIf- 若条件方法不再本类中,需用全类名
class Custom{ @Test @EnabledIf("customCondition") public void enabledIf() { System.out.println("Method[enabledIf] executed."); } @Test @DisabledIf("customCondition") public void disabledIf() { System.out.println("Method[disabledIf] executed."); } private boolean customCondition() { return true; } }
测试实例预构造回调
org.junit.jupiter.api.extension.TestInstancePreConstructCallback- 单元测试实例化之前调用
测试实例工厂
org.junit.jupiter.api.extension.TestInstanceFactory- 测试实例创建实例时调用
测试实例后置处理
TestInstancePostProcessor:测试实例后置处理扩展,测试实例初始化后执行
class TestInstancePostProcessorDemo {
@Test
@ExtendWith(MyTestInstancePostProcessor.class)
void testTestInstancePostProcessor() {
System.out.println("test");
}
static class MyTestInstancePostProcessor implements TestInstancePostProcessor {
@Override
public void postProcessTestInstance(Object o, ExtensionContext extensionContext) throws Exception {
System.out.println("postProcessTestInstance");
}
}
}
测试实例预销毁回调
org.junit.jupiter.api.extension.TestInstancePreDestroyCallback
参数解析器
内置参数解析器
- TestInfoParameterResolver 参数类型为TestInfo
- RepetitionInfoParameterResolver 参数类型为RepetitionInfo
- TestReporterParameterResolver 参数类型为TestReporter
自定义参数解析器
class ParameterResolverDemo {
@Test
@ExtendWith(IntRandomParameterResolver.class)
void injectsInteger(Integer i, Integer j) {
System.out.println(i + "->" + j);
}
static class IntRandomParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType().isAssignableFrom(Integer.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
boolean assignableFrom = parameterContext.getParameter().getType().isAssignableFrom(Integer.class);
if (assignableFrom) {
return (int) (Math.random() * 100);
}
return null;
}
}
}
测试结果处理
- TestWatcher
- testDisabled: 被@Disabled注释的⽅法跳过后
- testSuccessful: 成功测试
- testAborted:测试终⽌ 概念不理解
- testFailed: 测试失败
测试生命周期回调
BeforeAllCallbackBeforeEachCallbackBeforeTestExecutionCallbackAfterTestExecutionCallbackAfterEachCallbackAfterAllCallback
拦截调用
org.junit.jupiter.api.extension.InvocationInterceptor#interceptTestClassConstructor#interceptBeforeAllMethod#interceptBeforeEachMethod#interceptTestMethod#interceptTestFactoryMethod#interceptTestTemplateMethod#interceptDynamicTest#interceptAfterEachMethod#interceptAfterAllMethod
public class MyInvocationInterceptor implements InvocationInterceptor {
@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
AtomicReference<Throwable> throwable = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
}
catch (Throwable t) {
throwable.set(t);
}
});
Throwable t = throwable.get();
if (t != null) {
throw t;
}
}
}
异常处理
TestExecutionExceptionHandler
内置扩展@TempDir
package org.example.unittest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class TempDirDemo {
@TempDir
File file;
@Test
void testTempDirAnnotation(@TempDir(cleanup = CleanupMode.ALWAYS) Path tempDir)
throws IOException {
Path numbers = tempDir.resolve("numbers.txt");
List<String> lines = Arrays.asList("1", "2", "3");
Files.write(numbers, lines);
assertAll(
() -> assertTrue(Files.exists(numbers), "File should exist"),
() -> assertLinesMatch(lines, Files.readAllLines(numbers)));
File file = new File("haha.txt");
file.createNewFile();
assertTrue(file.exists());
}
}
扩展与用户代码
- 部分扩展点与注解的执行顺序
- org.junit.jupiter.api.extension.BeforeAllCallback
- @BeforeAll
- org.junit.jupiter.api.extension.BeforeEachCallback
- @BeforeEach
- org.junit.jupiter.api.extension.BeforeTestExecutionCallback
- @Test
- org.junit.jupiter.api.extension.TestExecutionExceptionHandler 异常后处理代码
- org.junit.jupiter.api.extension.AfterTestExecutionCallback
- @AfterEach
- org.junit.jupiter.api.extension.AfterEachCallback
- @AfterAll
- org.junit.jupiter.api.extension.AfterAllCallback
- 其他扩展点
- org.junit.jupiter.api.extension.TestInstanceFactory
- org.junit.jupiter.api.extension.TestInstancePostProcessor
- org.junit.jupiter.api.extension.ParameterResolver
- org.junit.jupiter.api.extension.ConditionEvaluationResultAggregator
- org.junit.jupiter.api.extension.TestExecutionExceptionHandler
- org.junit.jupiter.api.extension.TestWatcher
@ExtendWith(TimingExtension.class)
public class ExtensionDemo {
@Test
void test1() {
System.out.println("Test 1");
}
@Test
void test2() {
System.out.println("Test 2");
}
}
package org.example.unittest.extension;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private long startTime;
@Override
public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
startTime = System.currentTimeMillis();
}
@Override
public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
long duration = System.currentTimeMillis() - startTime;
System.out.println("Test took " + duration + "ms");
}
}
测试模板
package org.example.unittest;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.*;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.IntStream;
import java.util.stream.Stream;
class TestTemplateDemo {
@TestTemplate
@ExtendWith(SimpleTestTemplateExtension.class)
void test(Integer val1, Integer val2) {
System.out.println("value 1: " + val1 + " || value 2: " + val2);
}
@TestTemplate
@ExtendWith(SimpleTestTemplateExtension.class)
void test(Integer val1, Integer val2, String val3) {
System.out.println("value 1: " + val1 + " || value 2: " + val2 + " || value 3: " + val3);
}
static class SimpleTestTemplateExtension implements TestTemplateInvocationContextProvider {
// 是否支持该模板方法,返回false将不会执行provideTestTemplateInvocationContexts
// (ExtensionContext context)方法,你当然可以根据上下文做些额外判断
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext>
provideTestTemplateInvocationContexts(ExtensionContext context) {
return IntStream.rangeClosed(1, 50) // 将会产生50次参数,意味着模板方法会被执行50次
.mapToObj(n -> new TestTemplateInvocationContext() { // 实例化
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() { // 实例化
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
// 支持Integer和String类型的参数
return type.isAssignableFrom(Integer.class)
|| type.isAssignableFrom(String.class);
}
@Override // 产生随机整数
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
if (type.isAssignableFrom(String.class)) {
return UUID.randomUUID().toString();
} else if (type.isAssignableFrom(Integer.class)) {
return (int) (Math.random() * 100);
}
return null;
}
});
}
});
}
}
}
在扩展中保存状态
ExtensionContext提供StoreAPI支持该功能
测试实例生命周期
@TestInstance和TestInstance.LifeStyle给测试类定义生命周期- PER_CLASS
- PER_METHOD
- 在src/test/resources下新增文件为junit-platform.properties,写入junit.jupiter.testinstance.lifecycle.default = per_class
高级
套件
@Suite
@SelectClasses(value = {RectangleTest.class})
@IncludeTags("tag")
public class SuiteDemo {
}
文档信息
- 本文作者:Ling He
- 本文链接:https://GoggleHe.github.io/2023/10/08/JUnit5/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)