JUnit5入门案例

2023/10/08 单元测试 共 27047 字,约 78 分钟

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设置的参数
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@TestFactoryTestTemplate@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默认生命周期为方法级别,此时被注册的扩展如果是BeforeAllCallbackAfterAllCallbackTestInstancePostProcessor等类级别的扩展,注入的扩展将不能正常工作
自动注册
  • 开启自动注册开关
    • 启动参数:-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 contains System.
    • 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: 测试失败

测试生命周期回调

  • BeforeAllCallback
  • BeforeEachCallback
  • BeforeTestExecutionCallback
  • AfterTestExecutionCallback
  • AfterEachCallback
  • AfterAllCallback

拦截调用

  • 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支持该功能

测试实例生命周期

  • @TestInstanceTestInstance.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 {
}

文档信息

Search

    Table of Contents