JUnit 5
最后修改于 2024 年 1 月 27 日
JUnit 5 教程展示了如何使用 JUnit 在 Java 中进行测试。
Java JUnit 5
JUnit 5 是 Java 中最流行的测试框架。 JUnit 5 由三个模块组成:JUnit Platform、JUnit Jupiter 和 JUnit Vintage。
JUnit Platform 是启动 JUnit 5 测试的基础。它提供了与各种 IDE 和构建工具的集成。 JUnit Jupiter 是编写测试的核心 API。 JUnit Vintage 用于向后兼容 JUnit 3 和 JUnit 4 测试。
JUnit 5 Maven 依赖项和插件
在本教程的示例中,我们使用以下 Maven 依赖项
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.3.2</version>
<scope>test</scope>
</dependency>
我们使用 junit-jupiter-engine 和 junit-jupiter-params 依赖项。
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
</plugin>
Surefire 插件在构建生命周期的测试阶段用于执行应用程序的单元测试。
我们可以使用 Maven 或 IDE 的工具来运行测试。 IDE 有快捷方式来为我们创建测试类;例如,IntelliJ IDEA 有 Ctrl + Shift + T 来为我们生成一个测试类。
JUnit 5 注解
JUnit 5 API 的核心是注解。 例如,@Test 注解用于标记测试方法。 @ParameterizedTest 表示一个方法是一个参数化测试。 @RepeatedTest 表示一个方法是一个重复测试的测试模板。
JUnit 5 @BeforeAll
@BeforeAll 注解表示带注解的方法应在当前类中的所有 @Test、@RepeatedTest、@ParameterizedTest 和 @TestFactory 方法之前执行。 它只执行一次。
package com.zetcode.utils;
import java.util.List;
import java.util.stream.Collectors;
public class MathUtils {
public static Integer sum(List<Integer> vals) {
var sum = vals.stream().reduce(Integer::sum);
return sum.get();
}
public static List<Integer> positive(List<Integer> vals) {
return vals.stream().filter(val -> val > 0).collect(Collectors.toList());
}
public static List<Integer> negative(List<Integer> vals) {
return vals.stream().filter(val -> val < 0).collect(Collectors.toList());
}
}
我们有一个带有三个方法的 MathUtils 帮助类;我们为这个帮助类创建测试用例。
package com.zetcode.utils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MathUtilsTest {
private static List<Integer> vals;
@BeforeAll
static void setup() {
vals = List.of(2, 1, -2, 3, -1, 0, -1);
}
@Test
@DisplayName("testing sum of values")
void sumTest() {
var sum = MathUtils.sum(vals);
assertEquals(Integer.valueOf(2), sum);
}
@Test
@DisplayName("should get positive values")
void positiveTest() {
var positiveValues = MathUtils.positive(vals);
assertEquals(positiveValues, List.of(2, 1, 3));
}
@Test
@DisplayName("should get negative values")
void negativeTest() {
var negativeValues = MathUtils.negative(vals);
assertEquals(negativeValues, List.of(-2, -1, -1));
}
}
MathUtilsTest 是 MathUtils 帮助类的测试类。
@BeforeAll
static void setup() {
vals = List.of(2, 1, -2, 3, -1, 0, -1);
}
在三个测试方法之前,执行 setup 方法。 它为我们的测试创建值列表。
@Test
@DisplayName("testing sum of values")
void sumTest() {
var sum = MathUtils.sum(vals);
assertEquals(Integer.valueOf(2), sum);
}
在 sumTest 方法中,我们测试 MathUtils.sum 方法是否计算出正确的值。 使用 assertEquals 方法,我们断言计算出的值等于 2。 @DisplayName 注解在 IntelliJ IDEA 等 IDE 中使用,以便在其工具中为测试命名。
$ mvn test ... [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.zetcode.utils.MathUtilsTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.07 s - in com.zetcode.utils.MathUtilsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
这是测试的示例输出。
JUnit 5 @BeforeEach
@BeforeEach 注解表示带注解的方法应在当前类中的每个 @Test、@RepeatedTest、@ParameterizedTest 和 @TestFactory 方法之前执行。 它只执行一次。
package com.zetcode.bag;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ColorBag {
private Set<String> colors = new HashSet<>();
public void add(String color) {
colors.add(color);
}
public void remove(String color) {
colors.remove(color);
}
public List<String> toList() {
return new ArrayList<>(colors);
}
public boolean contains(String color) {
return colors.contains(color);
}
public int size() {
return colors.size();
}
}
我们有一个 ColorBag 要测试。
package com.zetcode.bag;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ColorBagTest {
private ColorBag colorBag;
@BeforeEach
void setupEach() {
colorBag = new ColorBag();
colorBag.add("red");
colorBag.add("green");
colorBag.add("yellow");
colorBag.add("blue");
colorBag.add("magenta");
colorBag.add("brown");
}
@Test
@DisplayName("added color value should be in bag")
void add() {
var newColor = "pink";
colorBag.add(newColor);
assertTrue(colorBag.contains(newColor),
"failure - added colour not it the bag");
}
@Test
@DisplayName("removed color should not be in bag")
void remove() {
var color = "green";
colorBag.remove(color);
assertFalse(colorBag.contains(color),
"failure - removed color still in bag");
}
@Test
@DisplayName("color bag should be transformed into List")
void toList() {
var myList = Arrays.asList("red","green", "yellow",
"blue", "magenta", "brown");
var colorList = colorBag.toList();
Collections.sort(myList);
Collections.sort(colorList);
assertEquals(colorList, myList,
"failure - color bag not transformed into List");
}
@Test
@DisplayName("size of a color bag should match")
void size() {
int bagSize = colorBag.size();
assertEquals(6, bagSize,
"failure - size of bag does not match");
}
}
带有 @BeforeEach 注解的方法将在每个方法之前运行。 对于不同的上下文,我们使用不同的断言:assertEquals、assertTrue 和 assertFalse。
@BeforeEach
void setupEach() {
colorBag = new ColorBag();
colorBag.add("red");
colorBag.add("green");
colorBag.add("yellow");
colorBag.add("blue");
colorBag.add("magenta");
colorBag.add("brown");
}
由于我们在测试方法中修改了 ColorBag 对象,因此我们在每个方法之前创建一个新的 ColorBag。
JUnit 5 @ParameterizedTest
参数化测试是使用 @ParameterizedTest 创建的。
package com.zetcode.utils;
public class StringUtils {
public static boolean isPalindrome(String text) {
var cleaned = text.replaceAll("\\s+", "").toLowerCase();
var plain = new StringBuilder(cleaned);
var reversed = plain.reverse().toString();
return reversed.equals(cleaned);
}
}
我们有 StringUtils 的 isPalindrome 方法要测试。
package com.zetcode.utils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class StringUtilsTest {
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "level", "refer", "deified", "civic" })
void isPalindrome(String word) {
assertTrue(StringUtils.isPalindrome(word));
}
}
@ValueSource 注解指定一个字符串数组作为测试方法的参数源。
$ mvn test ... [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.zetcode.utils.StringUtilsTest [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.107 s - in com.zetcode.utils.StringUtilsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 [INFO]
Maven 报告六个测试运行。 测试方法针对源中的每个单词运行。
JUnit 5 @RepeatedTest
重复测试使用 @RepeatedTest 注解运行。 我们指定所需的重复总数。
package com.zetcode.sort;
public class MySelectionSort {
public static int[] doSort(int[] arr){
for (int i = 0; i < arr.length; i++)
{
int idx = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[idx]) {
idx = j;
}
}
int smallerNumber = arr[idx];
arr[idx] = arr[i];
arr[i] = smallerNumber;
}
return arr;
}
}
我们有一个选择排序算法要测试。
package com.zetcode.sort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import java.util.Arrays;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MySelectionSortTest {
private final int N = 10;
private int[] vals = new int[N];
@BeforeEach
void beforeEach(RepetitionInfo info) {
System.out.printf("Test #%d%n", info.getCurrentRepetition());
var r = new Random(System.nanoTime());
for (int i=0; i < N; i++) {
vals[i] = r.nextInt(100);
}
}
@RepeatedTest(value = 40, name = "#{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("should sort values")
void doSort() {
System.out.println(Arrays.toString(vals));
var sorted = MySelectionSort.doSort(vals);
Arrays.sort(vals);
System.out.println(Arrays.toString(sorted));
assertEquals(sorted, vals);
}
}
我们正在运行四十次测试。
@BeforeEach
void beforeEach(RepetitionInfo info) {
System.out.printf("Test #%d%n", info.getCurrentRepetition());
var r = new Random(System.nanoTime());
for (int i=0; i < N; i++) {
vals[i] = r.nextInt(100);
}
}
在每次测试运行之前,我们随机创建四十个值并将它们放入数组中。
@RepeatedTest(value = 40, name = "#{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("should sort values")
void doSort() {
System.out.println(Arrays.toString(vals));
var sorted = MySelectionSort.doSort(vals);
Arrays.sort(vals);
System.out.println(Arrays.toString(sorted));
assertEquals(sorted, vals);
}
在测试方法中,我们将我们的排序方法的结果与 Arrays.sort 中的现有算法进行比较。 @RepeatedTest 注解有三个占位符:{displayName} 是方法的显示名称; {currentRepetition} 是当前重复计数; {totalRepetitions} 是重复的总数。
来源
在本文中,我们介绍了 JUnit 5 测试框架的基础知识。
作者
列出所有Java教程。