ZetCode

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-enginejunit-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 方法之前执行。 它只执行一次。

main/java/com/zetcode/utils/MathUtils.java
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 帮助类;我们为这个帮助类创建测试用例。

test/java/com/zetcode/utils/MathUtilsTest.java
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));
    }
}

MathUtilsTestMathUtils 帮助类的测试类。

@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 方法之前执行。 它只执行一次。

main/java/com/zetcode/bag/ColorBag.java
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 要测试。

test/java/com/zetcode/bag/ColorBagTest.java
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 注解的方法将在每个方法之前运行。 对于不同的上下文,我们使用不同的断言:assertEqualsassertTrueassertFalse

@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 创建的。

main/java/com/zetcode/utils/StringUtils.java
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);
    }
}

我们有 StringUtilsisPalindrome 方法要测试。

test/java/com/zetcode/utils/StringUtilsTest.java
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 注解运行。 我们指定所需的重复总数。

main/java/com/zetcode/sort/MySelectionSort.java
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;
    }
}

我们有一个选择排序算法要测试。

test/java/com/zetcode/sort/MySelectionSortTest.java
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 用户指南

在本文中,我们介绍了 JUnit 5 测试框架的基础知识。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程