刷了多道 leetcode 后发觉每次写 main 测试很麻烦,因此稍微对项目进行了调整,转为使用 junit 进行测试。
封面《ハミダシクリエイティブ》

前言

在 leetcode 上进行 DEBUG 需要充值 VIP,因此我一般都是在线下进行 DEBUG,然而如果为每道题都 DEBUG 则需要编写不同的 main 函数,且切换数据的时候比较麻烦,因此使用 junit 来进行测试,方便每次 debug。

JUnit5 介绍

JUnit 是基于 Java 的一个单元测试框架,JUnit5 一共由以下几个三部分组成

  • JUnit Platform: 运行在 JVM 上的一个测试框架,定义了测试的 API,可以接入其他测试引擎
  • JUnit Jupiter: 全新的编程和拓展的测试模型的组合,并为 JUnit Platform 提供测试引擎
  • JUnit Vintage: 提供了 JUnit3、JUnit4 的测试引擎

Maven 中导入

参考官方给出的 JUnit5 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.8.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>

注解

以下是一些常见的测试注解,更多的测试注解以及其使用方法请翻阅文档

Annotation Description
@Test 描述一个方法是测试方法,与 JUnit4 不同的是此方法不申明任何属性
@ParameterizedTest 表示该方法为参数化测试
@RepeatedTest 表示该方法为重复测试
@TestFactory 表示该方法为动态测试
@TestTemplate 表示该方法为测试模板
@TestClassOrder 表示 @Nested 标注的测试类的执行顺序
@TestMethodOrder 表示测试方法的执行顺序
@TestInstance 配置测试实例的生命周期
@DisplayName 自定义测试的名称
@DisplayNameGeneration 自定义测试名的生成方法
@BeforeEach 在每个测试方法测试前执行
@AfterEach 在每个测试方法执行后执行
@BeforeAll 在所有测试方法测试前执行,注解的方法必须是 static
@AfterAll 在所有测试方法测试后执行,注解的方法必须是 static
@Nested 描述一个非静态的嵌套测试类
@Tag 使用定义的标签去过滤一些测试
@Disabled 忽略一些测试方法
@Timeout 运行超过了时间会报错
@ExtendWith 声明的方式拓展测试类
@RegisterExtension 编程的方式拓展测试类
@TempDir 使用临时目录进行测试

生命周期

一个标准的测试的生命周期如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

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
}

@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}

@AfterEach
void tearDown() {
}

@AfterAll
static void tearDownAll() {
}

}

参数化测试

以目前的需求来说,刷 leetcode 最需要是参数化测试,因此本次重点就是几种常用的参数化测试

@ValueSource

@ValueSource 是最简单的数据来源之一,可以使用简单的数组来提供测试数据。支持的类型有 short,byte,int,long,float,double,char,boolean,java.lang.String,java.lang.Class

1
2
3
4
5
6
7
8
public class ValueSourceTest {

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3, 4 })
public void testLargerThanZero(int num) {
assertTrue(num > 0);
}
}

对于 NULL 或者空数据则可以使用 @NullSource@EmptySource@NullAndEmptySource

@EnumSource

1
2
3
4
5
6
7
8
public class EnumSourceTest {

@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
public void test(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
}

@MethodSource

@MethonSource 允许你使用一个 Method 工厂来提供源,需要注意的是提供者必须是 static,返回的类型需要是 Stream 或者 Arguments,或者是可以被 JUnit 转化为 Stream 的类型,比如 Collection,DoubleStream 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MethodSourceTest {

@ParameterizedTest
@MethodSource("stringProvider")
public void test(String got, String expect) {
assertEquals(got, expect);
}

static Stream<Arguments> stringProvider() {
return Stream.of(
Arguments.of("apple","apple"),
Arguments.of("banana","banana"),
Arguments.of("pea","pea")
);
}
}

@CsvSource

@CsvSource 允许以分隔符分隔输入列表中的参数。默认的分隔是 ,,但是可以通过 delimiterString 进行改变。

1
2
3
4
5
6
7
8
9
10
11
public class CsvSourceTest {
@ParameterizedTest
@CsvSource({
"1,1,2",
"2,2,4",
"3,3,6"
})
public void test(int add1,int add2,int expect){
assertEquals(add1+add2, expect);
}
}

@CsvFileSource

@CsvFileSource 可以使用 csv 文件进行测试。创建 test.csv 文件,如下填充

1
2
3
4
5
add1,add2,expect
1,1,2
2,2,4
3,2,5
5,4,9
1
2
3
4
5
6
7
8
public class CsvFileSourceTest {

@ParameterizedTest
@CsvFileSource(resources = "./test.csv", numLinesToSkip = 1)
public void test(int add1, int add2, int expect) {
assertEquals(add1 + add2, expect);
}
}

@ArgumentsSource

@ArgumentsSource 可以使用 ArgumentsProvider 进行自定义。

1
2
3
4
5
6
7
8
public class ArgumentsSourceTest {

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
public void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
}
1
2
3
4
5
6
7
8
public class MyArgumentsProvider implements ArgumentsProvider {

@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
return Stream.of("apple", "banana").map(Arguments::of);
}

}

参数转换与增强

对于参数化测试的输入参数,JUnit 还能对输入参数的转化很增强进行定制,详细的可以翻阅文档,此处不再多做说明。

总结

JUnit 是一个很好的测试框架,能非常便利的编写测试,配合 leetcode 进行 DEBUG。

参考资料

JUnit

廖雪峰的 Java 教程