1. JUnit5模块组成
JUnit5包含三个模块: JUnit Platform
, JUnit Jupiter
和 JUnit Vintage
.
-
JUnit Platform:
-
junit-platform-common
: JUnit基础工具包. -
junit-platform-engine
: 提供TestEngine相关基础类. -
junit-platform-launcher
: 提供让客户端执行Test和收集测试结果的入口.
-
-
JUnit Jupiter:
-
junit-jupiter-api
: JUnit Jupiter测试相关基础类/注解以及一些生命周期接口. -
junit-jupiter-params
: 提供参数化测试的扩展功能. -
junit-jupiter-engine
: 提供JUnit Jupiter的TestEngine实现.
-
-
JUnit Vintage: 用来兼容运行JUnit3和4的Test用例.
总之, JUnit Platform
模块抽象出了一个单元测试框架的上层API, JUnit Jupiter
模块则是JUnit5针对 JUnit Platform
包具体的实现.
而JUnit Vintage模块是为了向前兼容, 让JUnit5也能执行老版本的测试代码.
2. JUnit依赖导入
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.6.2</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>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
JUnit5提供了自己各个模块版本的pom包, 所以直接引入junit-bom来管理JUnit版本就行了.
3. JUnit测试方法和测试类
标有 @Test/@RepeatedTest/@ParameterizedTest/@TestFactory/@TestTemplate
注解的方法为测试方法, JUnit会在启动的时候执行这些方法. 测试方法访问修饰符可以为default.
测试类就是包含了一系列测试方法的Java类, 测试类可以有三种表现形式:
-
标准的Java Class.
-
静态内部类.
-
标有@Nested注解的成员内部类.
测试类只能有一个构造方法, 且不能为抽象类. 测试类访问修饰符可以为default.
测试类中除了待执行的测试方法, 还可以包含一些测试生命周期的方法, 如标有 @BeforeEach
的方法会在每个方法执行前被执行.
4. JUnit使用
4.1. @Test
标有 @Test
注解的方法即为一个标准的测试方法.
测试方法执行抛出异常或者与断言预期不一致, 则测试方法不通过.
@Slf4j
class JUnitHelloWorld {
@Test
void succeedingTest() {
Assertions.assertTrue(true);
log.info("succeedingTest");
}
}
4.2. @ParameterizedTest
@ParameterizedTest用来给测试方法注入预定义的一些参数去执行, 实现了测试参数与测试代码的职责分离.
参数有三种注入形式:
-
ArgumentsProvider
: 字面量和参数属于一对一的映射关系, JUnit预先提供了@xxxSource
类注解来配和@ParameterizedTest
使用. -
ArgumentsAccessor
: 测试方法里手动根据字面量和其索引来创建参数需要的对象. -
ParameterResolver
: 运行时动态注入参数需要的对象.
4.2.1. ArgumentsProvider
@ValueSource
@ValueSource
可以声明以下类型的字面量参数:
-
基本数据类型:
byte/short/int/long/boolean/char/float/double
-
String
-
Class
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6})
void testValueSource(int i) { // 该测试方法会执行6次, 依次传入ints数组的元素
assertTrue(i > 0);
}
@NullSource
@NullSource
可以赋值给引用类型的参数为null.
@ParameterizedTest
@NullSource
void testNullString(String nullableString) {
assertNull(nullableString);
}
@ParameterizedTest
@NullSource
void testZeroNumber(int zero) {
assertEquals(0, zero); // 错误, 无法将null复制给int类型的参数
}
@EmptySource
@EmptySource
可以为参数创建一个空的值, 具体表现为:
-
String: 初始化为一个空的字符串.
-
数组: 初始化为一个长度为0的数组.
-
集合: 初始化为Collections.emptyXXX()方法返回的空集合, 如
List
参数会被初始化为Collections.emptyList()
方法返回的对象.
@ParameterizedTest
@EmptySource
void testEmptyString(String str) {
assertEquals(0, str.length());
}
@ParameterizedTest
@EmptySource
void testEmptyList(List<String> list) {
assertSame(Collections.emptyList(), list);
assertEquals(0, list.size());
}
@ParameterizedTest
@EmptySource
void testEmptySet(Set<String> set) {
assertSame(Collections.emptySet(), set);
assertEquals(0, set.size());
}
@ParameterizedTest
@EmptySource
void testEmptyMap(Map<String, Object> map) {
assertSame(Collections.emptyMap(), map);
assertEquals(0, map.size());
}
@ParameterizedTest
@EmptySource
void testEmptyArray(int[] arr) {
assertEquals(0, arr.length);
}
@NullAndEmptySource
@NullAndEmptySource
注解是 @NullSource
和 @EmptySource
两个注解的组合: 会分别将方法参数注入一个null和一个空对象, 也就是说测试方法会被执行两次.
可以用来测试方法的鲁棒性👀.
@EnumSource
@EnumSource
用来注入枚举类参数.
@ParameterizedTest
@EnumSource
void testEnumSource(Gender gender) {
assertTrue(Arrays.stream(Gender.class.getEnumConstants()).anyMatch(e -> e == gender)); // 该方法会执行3次, 分别注入Gender的三个枚举值.
}
public enum Gender {
MALE, FEMALE, UNKNOWN
}
@EnumSource
也可以通过设置 names
和 mode
属性来过滤注入的枚举值.
@MethodSource
@MethodSource
用来通过方法返回值来注入参数.
methodSource方法需要为static.
方法的返回值需要为 Stream/Collection/Iterator/Iterable/数组
类型.
如果泛型为 Arguments
类型, 则可以同时注入多个参数.
@ParameterizedTest
@MethodSource("generateInts")
void testIntMethodSource(int i) { // 注入1到9
assertTrue(i > 0 && i < 10);
}
static IntStream generateInts() {
return IntStream.range(1, 10)
}
@ParameterizedTest
@MethodSource("generateArguments")
void testArgumentsMethodSource(String str, int i) {
assertEquals(i, str.length());
}
static Stream<Arguments> generateArguments() {
return Stream.of(
Arguments.of("a", 1),
Arguments.of("aa", 2),
Arguments.of("aaa", 3)
);
}
@CsvSource
@CsvSource
可以同时注入多个字面量参数.
@ParameterizedTest
@CsvSource({"a,1", "aa,2", "aaa,3"})
void testCsvSource(String str, int i) {
assertEquals(i, str.length());
}
@CsvFileSource
@CsvFileSource
可以读取csv文件, 然后注入字面量参数.
@ParameterizedTest
@CsvFileSource(resources = "/str.csv")
void testCsvFileSource(String str, int i) {
assertEquals(i, str.length());
}
a,1
aa,2
aaa,3
-
如果csv文件第一行为表头, 可以设置
numLinesToSkip = 1
来过滤掉第一行. -
如果某一列里面包含逗号, 会导致csv解析出现异常, 可以通过设置
delimiterString
来区分列.
@ArgumentsSource
@ArgumentsSource
可以指定一个 ArgumentsProvider
的实现类来注入参数.
@ParameterizedTest
@ArgumentsSource(SequenceArgumentProvider.class)
void testArgumentsSource(int i) {
assertTrue(i > 0 && i < 10);
}
public static class SequenceArgumentProvider implements ArgumentsProvider{
@Override
public Stream<Arguments> provideArguments(ExtensionContext context) {
return IntStream.range(1, 10).mapToObj(Arguments::of);
}
}
参数类型转换
字面量和参数的类型转换分为隐式类型转换和显示类型转换.
-
隐式类型转换: JUnit内置的转换机制.
-
字面量的转换: 支持的类型见文档: https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-argument-conversion-implicit
-
如果目标类型不在支持范围内, JUnit会尝试调用类型里的static方法/构造方法(方法有且仅有一个String参数)来创建实例.
-
-
显示类型转换:
-
实现
ArgumentConverter
接口, 方法参数加上ConvertWith
注解来指定ArgumentConverter
即可.
-
@ParameterizedTest
@CsvSource({"'1,3,2', '1,2,3'"})
void testConverter(@ConvertWith(ToArrayArgumentConverter.class) int[] arr,
@ConvertWith(ToArrayArgumentConverter.class) int[] expect) {
Arrays.sort(arr);
assertArrayEquals(expect, arr);
}
public static class ToArrayArgumentConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
Class<?> type = context.getParameter().getType();
String[] strings = source.toString().split("\\s*,\\s*");
if (int[].class == type) {
return Arrays.stream(strings).mapToInt(Integer::valueOf).toArray();
}
return strings;
}
}
4.2.2. ArgumentAccessor
ArgumentAccessor
可以通过获取指定位置的参数来在方法内部获取参数值.
有两种使用方式:
-
将
ArgumentAccessor
作为参数, 然后在方法内部使用. -
实现
ArgumentsAggregator
接口, 使用@AggregateWith
注解指定ArgumentsAggregator
来实现参数类型转换.
@ParameterizedTest
@CsvSource({"1", "2", "3", "4", "5", "6"})
void testWithArgumentsAccessor(ArgumentsAccessor argumentsAccessor) {
Integer i = argumentsAccessor.getInteger(0);
assertTrue(i > 0);
}
@ParameterizedTest
@CsvSource({"1", "2", "3", "4", "5", "6"})
void testWithArgumentsAccessor(@AggregateWith(ToIntArgumentsAggregator.class) int i) {
assertTrue(i > 0);
}
public static class ToIntArgumentsAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
return accessor.getInteger(0);
}
}
4.2.3. ParameterResolver
JUnit5
允许使用外部扩展的方式来注入参数值.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RandomInt {
}
public class DemoExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.isAnnotated(RandomInt.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return ThreadLocalRandom.current().nextInt();
}
}
@ExtendWith(DemoExtension.class)
public class DemoTest {
@Test
void testRandomInt(@RandomInt int i) {
// i被设置成随机数
}
}
4.3. @RepeatedTest
@RepeatedTest
可以让JUnit重复执行测试方法.
方法参数可以注入一个运行时的 RepetitionInfo
对象来让方法内部获取到重复执行的序号和总次数.
@RepeatedTest(3)
void testWithRepeatedTest() {
assertTrue(true); // 方法会执行3次
}
4.4. @TestFactory
@TestFactory
可以像 @MethodSource
一样以编程式的方式执行测试用例.
定义一个方法, 返回Stream/Collection/Iterator/Iterable/数组类型, 泛型类型需要为 DynamicTest
.
@TestFactory
Stream<DynamicTest> dynamicTestStream() {
return Stream.of("a", "b", "c")
.map(text -> DynamicTest.dynamicTest(text, () -> assertTrue(true)));
}
4.5. @TestMethodOrder
@TestMethodOrder
可以指定测试方法的执行顺序:
-
Alphanumeric
: 按照测试方法名和参数列表字母排序执行. -
OrderAnnotation
: 按照测试方法上的@Order
注解指定的顺序执行, 如果没有注解则默认为Integer.MAX_VALUE / 2
. -
Random
: 随机顺序执行.
4.6. @TestInstance
默认情况下, 每次执行测试方法时都会新创建测试类的一个实例, 等同于 @TestInstance(PER_METHOD)
class JUnitApiTest {
private int i;
@RepeatedTest(10)
void testPerMethod1() {
i++;
// 每次都会在一个新的实例中执行该方法, 所以i++均为1.
assertEquals(1, i);
}
}
@TestInstance(PER_CLASS)
情况下, 测试类只会实例化一次.
此外 PER_CLASS 下 `@BeforeAll
和 @AfterAll
注解可以用在类的实例方法或者接口的default方法上.
4.7. @Execution
@Execution
可以设置测试方法在同一个线程中执行, 还是使用ForkJoin线程池并行执行.
除了使用 @Execution
注解, 还可以在 junit-platform.properties
中全局配置.
4.7.1. 测试类并行执行但同一类方法顺序执行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
4.7.2. 测试类顺序执行但同一类方法并行执行
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
4.8. 条件执行
可以指定测试方法在特定环境下才执行.
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// Java8 才执行
}
本质上是执行了 ExecutionCondition::evaluateExecutionCondition
方法来判断是否执行.
5. Extension机制
JUnit 提供了扩展接口, 来在测试方法执行前后执行一些自定义的回调.
Extension的功能主要包括:
-
对实例化测试类对象时后置处理.
-
测试类执行条件判断.
-
生命周期回调.
-
自定义参数解析.
-
异常处理.
5.1. 注册Extension的方式
5.1.1. 注解
@ExtendWith(DemoExtension.class)
class ExtensionTest {
}
5.1.2. SPI
-
/META-INF/services/org.junit.jupiter.api.extension.Extension
文件里添加自定义的Extension类的全限定名. -
junit-platform.properties
里添加junit.jupiter.extensions.autodetection.enabled=true
.
5.1.3. @RegisterExtension注解
-
@RegisterExtension
注解static字段. -
@RegisterExtension
注解实例字段.
5.2. Extension的生命周期
-
BeforeAllCallback
-
@BeforeAll
-
TestInstancePostProcessor
-
BeforeEachCallback
-
@BeforeEach
-
BeforeTestExecutionCallback
-
@Test
-
AfterTestExecutionCallback
-
@AfterEach
-
AfterEachCallback
-
@AfterAll
-
AfterAllCallback