Loading

JUnit5 一 编写测试

概述

JUnit5包含三个模块,JUnit Platform、JUnit Jupiter、JUnit Vintage

  1. JUnit Platform 提供JVM上的基础启动测试框架,定义TestEngine API,提供命令行启动,技工基于JUnit4的Runner
  2. JUnit Jupiter 对于编写测试和扩展,提供JUnit 5上的新的编程模型和扩展模型
  3. JUnit Vintage 提供基于JUnit3和JUnit4的测试用例的运行

大致意思就是JUnit Platform提供测试引擎,然后JUnit Jupiter用于运行JUnit 5,JUnit Vintage用于运行JUnit3和4,它们最终都会在JUnit Platform提供的测试引擎上运行

编写测试

一个最简单的测试用例

import static org.junit.jupiter.api.Assertions.assertEquals;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class MyFirstJUnitJupiterTests {

    private final Calculator calculator = new Calculator();

    @Test
    void addition() {
        assertEquals(2, calculator.add(1, 1));
    }

}

组合注解

可以对注解进行组合,编写更有表现力的注解名

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast { }
@Test
@Fast
public void addition() {
    assertEquals(2, calculator.add(1, 1));
}

甚至可以一步到位

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {}
@FastTest
public void addition() {
    assertEquals(2, calculator.add(1, 1));
}

测试类和方法

  • 测试类:有至少一个测试方法的任何顶层类,静态成员类或@Nested类,测试类必须不是抽象的必须有单一的构造器
  • 测试方法:任何一个被@Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate直接或间接标注的实例方法
  • 生命周期方法:任何直接或间接标注@BeforeAll, @AfterAll, @BeforeEach, or @AfterEach任意方法

测试方法和生命周期方法必须是定义在本类中、从父类或接口继承的非抽象方法,而且除了@TestFactory必须返回值外,其它的都不能返回值。

测试类和方法不必须是public,但不能是private

除非是有有技术原因,否则不推荐在测试方法上显式编写public。比如需要跨包或模块。

一个使用了测试方法和生命周期方法的小例子

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() {
    }

}

DisplayName

@DisplayName可以给测试类和测试方法取名,其中可以包括空格,特殊字符甚至Emoji

Assertions

JUnit Jupiter支持所有JUnit 4中的断言方法并添加了一些新的,适用于Java8 Lambda表达式的断言方法。所有断言方法都是org.junit.jupiter.api.Assertions的静态方法。

失败消息

@Test
void standardAssertions() {
    assertEquals(2, calculator.add(1, 1));
    assertEquals(4, calculator.multiply(2,2),
            "The optional failure message is now the last parameter");
    assertTrue(1 < 2, () -> "Lazy evaluated message");
}

这个示例表明assert可以有一个失败时的消息,如果这个消息需要复杂的计算,那么可以通过一个Lambda表达式来进行惰性计算,就是只有当断言失败时才会计算这个值。

成组断言

@Test
void groupedAssertions() {
    assertAll("test calculator",
            () -> assertEquals(2, calculator.add(1, 1)),
            () -> assertEquals(4, calculator.multiply(2, 2))
    );
}

依赖断言

@Test
void dependentAssertions() {
    assertAll(
            () -> {
                System.out.println("1");
                assertEquals(3, calculator.add(1, 1));
                System.out.println("2");
                assertEquals(2, calculator.sub(6, 4));
            },
            () -> {
                System.out.println("3");
                assertEquals(1, calculator.multiply(1, 1));
                System.out.println("4");
            }
    );
}

输出134,在一个断言块中,如果其中一个断言失败,那么块中随后出现的代码都会被跳过。

异常断言

@Test
void exceptionAssertions() {
    Exception exception = assertThrows(ArithmeticException.class,
            () -> calculator.divide(1, 0));
    assertEquals("/ by zero", exception.getMessage());
}

超时断言

@Test
void timeoutNotExceeded() {
    assertTimeout(ofMinutes(2), () -> {});
}

@Test
void timeoutNotExceededWithResult() {
    String actualResult = assertTimeout(ofMinutes(2), () -> "a result");
    assertEquals("a result", actualResult);
}

@Test
void timeoutExceeded() {
    assertTimeout(ofMillis(10), () -> Thread.sleep(100));
}

@Test
void timeoutExceededWithPreemptiveTermination() {
    // 在新线程执行,注意这个测试方法中不要有任何依赖ThreadLocal存储的东西,否则可能出现副作用。
    assertTimeoutPreemptively(ofMillis(10), () -> new CountDownLatch(1).await());
}

条件测试

@EnabledOnOs/@DisabledOnOs

@Test
@EnabledOnOs(OS.WINDOWS)
void testOnWindows() {
    System.out.println("WINDOWS!!");
}

@Test
@EnabledOnOs(OS.MAC)
void testOnMac() {
    System.out.println("MAC!!");
}

@EnabledOnJre/...

还有一些条件测试针对当前Java系统

@Test
@EnabledOnJre(JRE.JAVA_8)
void testOnJava8() {
    System.out.println("JAVA8!!");
}

@Test
@EnabledOnJre(JRE.JAVA_11)
void testOnJava11() {
    System.out.println("JAVA11!!");
}

@Test
@DisabledForJreRange(max = JRE.JAVA_8)
void testOnAfterJava8() {
    System.out.println("After Java 8");
}

// java系统属性
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void testOnly64BitArchitecture() {
    System.out.println("64Bit Architecture...");
}

// 本机系统环境变量
@Test
@EnabledIfEnvironmentVariable(named = "username", matches = "lilpig")
void testOnlyLilpig() {
    System.out.println("LILPIG!!");
}

测试顺序

方法测试顺序

默认情况下JUnit使用一个明确但意义不明显的排序顺序,这保证了每次运行测试用例得到的结果是相同的。

编写单元测试时是不依赖测试执行顺序的,因为单元测试中的每一个测试用例时无关的,但当编写集成测试时,可能就需要指定顺序。

@TestMethodOrder可以指定测试方法的顺序,其中传入一个MethodOrder类型的类,用于进行排序,我们可以使用默认的几个也可以自定义。

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AssertionsDemo {
    // ...
}

如上代码,使用测试方法上@Order注解定义的顺序进行排序,所以我们可以给测试方法加上@Order注解

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AssertionsDemo {
    private Calculator calculator = new Calculator();

    @Test
    @Order(1)
    void standardAssertions() {
    }

    @Test
    @Order(2)
    void groupedAssertions() {
    }

    @Test
    @Order(3)
    void dependentAssertions() {
    }
}

类测试顺序

大部分情况下都不会有一个需求要改类测试顺序,但有时候我们可能需要一个随机顺序确保被测试代码在生产中不会因为隐含的依赖关系而出错,或者其他什么需求。

类测试顺序没法使用注解来定义了,需要用配置文件告诉JUnit:src/test/resource/junit-platform.properties

junit.jupiter.testclass.order.default = \
    org.junit.jupiter.api.ClassOrderer$OrderAnnotation

上面使用@Order注解进行排序。

@Nested类测试顺序

上面的顺序仅对于顶层类,对于@Nested测试类,它们的测试顺序相对于父类,可以在父类上加入@TestClassOrder来指定其顺序。

@Nested嵌套测试类

嵌套测试类可以让我们构建测试的层级结构,像如下示例

@DisplayName("A stack")
public class StackTest {
    Stack<String> 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("isEmpty")
        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 isNotEmpty() {
                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());
            }

        }
    }
}

IDEA中的结果如下

依赖注入

JUnit5有了依赖注入功能,可以在测试类构造器、生命周期方法和测试方法中加入参数,实现依赖注入。

JUnit5的依赖注入由ParameterResolver来完成,它是一个接口,用于在运行时动态解析要注入的参数。如下是一些内建解析器。

TestInfoParameterResolver

TestInfo是当前运行的测试的一些信息,它作用在构造器上,就是当前测试类的信息,作用在生命周期方法上,就是触发生命周期方法的测试方法信息,作用在测试方法上,那么就是测试方法的信息。由TestInfoParameterResolver注入。

@DisplayName("TestInfo Demo")
public class TestInfoTest {
    TestInfoTest(TestInfo testInfo) {
        assertEquals("TestInfo Demo", testInfo.getDisplayName());
    }

    @BeforeEach
    void beforeEach(TestInfo testInfo) {
        String displayName = testInfo.getDisplayName();
        assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
    }

    @Test
    @DisplayName("TEST 1")
    @Tag("my-tag")
    void test1(TestInfo testInfo) {
        assertEquals("TEST 1", testInfo.getDisplayName());
        assertTrue(testInfo.getTags().contains("my-tag"));
    }

    @Test
    void test2() {

    }
}

RepetitionInfoParameterResolver

当一个@RepeatedTest@BeforeEach@AfterEach方法被调用并且参数中存在RepetitionInfo类型的参数时,这个实例会被注入。其中包含一些测试重复次数和总次数的信息。

TestReporterParameterResolver

当方法参数中存在TestReport类型的参数时,会由TestReporterParameterResolver注入这个参数。

TestReport用来发布一些关于测试附加数据,用于生成测试报表。

class TestReporterDemo {

    @Test
    void reportSingleValue(TestReporter testReporter) {
        testReporter.publishEntry("a status message");
    }

    @Test
    void reportKeyValuePair(TestReporter testReporter) {
        testReporter.publishEntry("a key", "a value");
    }

    @Test
    void reportMultipleKeyValuePairs(TestReporter testReporter) {
        Map<String, String> values = new HashMap<>();
        values.put("user name", "dk38");
        values.put("award year", "1974");

        testReporter.publishEntry(values);
    }

}

其它类型的ParameterResolver都需要通过@ExtendWith注解才能使用。

RandomParametersExtension

复制RandomParametersExtension的代码,然后使用这个Resolver。

@ExtendWith(RandomParametersExtension.class)
public class MyRandomParameterTest {
    Calculator calculator = new Calculator();
    @Test
    void testAdd(@RandomParametersExtension.Random int n) {
        assertEquals(n * n, calculator.multiply(n, n));
    }
}

Repeated Tests

@RepeatedTests可以重复执行一个测试方法若干次,每次调用,测试方法都像一个普通@Test方法一样,它具有一样的生命周期回调和扩展支持。

@RepeatedTest(10)
void repeatedTest() {
    // ...
}

@RepeatedTest标注的测试方法的DisplayName可能需要引用到目前的重复次数,所以可以从@RepeatedTest的name属性中设置当前测试方法的DisplayName。

@DisplayName("LALALA")
@RepeatedTest(value = 10, name = "{displayName} {currentRepetition} of {totalRepetitions}")
void repeatedTest() {
    assertEquals(1, calculator.divide(1, 1));
}

占位符列表如下:

String DISPLAY_NAME_PLACEHOLDER = "{displayName}";
String CURRENT_REPETITION_PLACEHOLDER = "{currentRepetition}";
String TOTAL_REPETITIONS_PLACEHOLDER = "{totalRepetitions}";

@ParameterizedTest

@ParamterizedTest用于给测试方法提供一些参数,这个注解会消费掉Source中配置的那些参数,第一种参数就是下标参数(indexed argument),遵循Source中的变量和测试方法参数中的变量的一对一关系,第二种就是也可以通过一个聚合对象(aggregator)来从Source中获取数据,然而其它参数也可能通过ParameterResolver传递进来,比如TestInfo,它们之间的顺序关系如下:

  1. 0个或多个具有下标的Source参数需要定义在方法列表最前
  2. 0个或多个聚合对象Source参数要定义在第一条的后面
  3. 0个或多个ParameterResolver的参数定义在最后

indexed argumentArgumentsProvider提供,而一个aggregator则需要通过一个ArgumentsAccessor类型的参数来接收,或者是任意一个打了@AggregateWith的参数。

继承自java.lang.AutoCloseable的参数(或java.io.Cloaseable,它继承自AutoCloseable),将会自动再@AfterEach和所有@AfterEachCallback扩展被调用后被关闭。如果这个AutoCloseable资源横跨多个测试方法,那么你可能不希望它被关闭,可以设置@ParameterizedTest(autoCloseArguments = false)

@ValueSource

ValueSource中的参数会一个一个的被注入到测试方法中。

@ParameterizedTest
@ValueSource(strings = {"racecar", "radar"})
void palindromes(String candidate) {
    assertFalse(StringUtils.isBlank(candidate));
}

ValueSource提供一个原始数组,可以是如下类型

short[] shorts() default {};
byte[] bytes() default {};
long[] longs() default {};
float[] floats() default {};
double[] doubles() default {};
char[] chars() default {};
boolean[] booleans() default {};
String[] strings() default {};
Class<?>[] classes() default {};

@NullSource

提供一个单一null值

@ParameterizedTest
@NullSource
void isNull(String arg1) {
    assertNull(arg1);
}

@EmptySource

提供空值,可以是java.lang.String, java.util.List, java.util.Set, java.util.Map和原始数组,但不能是它们的子类型。

@ParameterizedTest
@EmptySource
void isEmpty(int arr[]) {
    assertTrue(arr.length == 0);
}

@NullAndEmptySource

只是NullSource和EmptySource的组合

像如下这种情况,想确定null、空值和空白字符串对方法的影响

@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}

可以改写成

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })

@EnumSource

提供一个枚举参数。

enum Gender {
    MALE, FEMALE
}

@ParameterizedTest
@EnumSource
void testEnumSource(Gender gender) {
    assertNotNull(gender);
}

这种情况下,会将对应枚举类中的所有枚举项都用来调用一次测试方法,可以通过names指定哪些枚举被用来测试。

@ParameterizedTest
@EnumSource(names = "MALE")
void testEnumSource(Gender gender) {
    assertNotNull(gender);
}

name还可以是正则表达式,匹配的项目名被用来测试

mode用来指定粒度,是排除还是选择

@MethodSource

使用一个工厂方法来生成测试参数

@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("apple", "banana");
}
  1. 工厂方法必须是static,除非测试类标注了@TestInstance(Lifecycle.PER_CLASS)
  2. 返回值必须是一个Stream值,这个值的定义是所有JUnit可以转换成Stream的值,比如Stream,DoubleStream,LongStream等,还有对象或原始数组。

如果工厂方法具有和测试方法一样的名字,那么可以不用指定@MethodSource的参数。

如果测试方法具有多个参数,可以返回Stream<Arguments>对象。Arguments对象是JUnit定义的一个代表多个参数的类型,可以使用arguments方法生成。

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultipleArgMethodSource(String name, int count, List<String> list) {
    assertEquals(5, name.length());
    assertTrue(count >= 1 && count <= 10);
    assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
            Arguments.arguments("apple", 1, Arrays.asList("a","b")),
            Arguments.arguments("lemon", 5, Arrays.asList("x","y"))
    );
}

通过全限定名寻找Provider方法

@CsvSource/@CsvFileSource

通过Csv文件作为参数源

@ArgumentsSource

自定义ArgumentsProvier来提供参数。前面说过测试方法中的参数都是@ParameterizedTestArgumentsProvider中拿的,其实前面使用的每一个@XXXSoruce其中都是通过@ArgumentsProvider提供了一个ArgumentsProvider

比如这个

@ArgumentsSource(EnumArgumentsProvider.class)
public @interface EnumSource {}

自定义的ArgumentsProvider必须是一个顶级类或者一个嵌套静态类。

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

参数转换

像@CsvSource这种东西,只能传递String类型的参数,尽管有时候我们希望这个字段是整数或者小数。这时候需要有参数转换。

隐式转换

这个代码竟然能用。

@ParameterizedTest
@CsvSource({
        "apple, 9.99",
        "banana, 2"
})
void testConverter(String fruit, double price) {
    Assertions.assertNotNull(fruit);
    Assertions.assertTrue(price > 0);
}

这证明JUnit提供了内置的隐转参数转换功能。String类型的数据会被转换成各种类型,这依赖测试方法中定义的参数类型。

下面是具体的转换方式表。

隐式对象转换

除了上面定义的类型,JUnit Jupiter允许将指定的Source中的数据转换成一个测试方法参数中的对象类型参数。

  1. 使用工厂方法,在对应类中创建一个非private的静态方法,这个方法只有一个字符串参数,返回对应类的实例。
  2. 使用构造器,需要一个非private构造器接受一个字符串参数,对应的类必须是顶层类或一个嵌套static类。
@ParameterizedTest
@ValueSource(strings = {"MYSQL", "JAVA"})
void testConvertToObjectWithConstructor(Book book) {
    Assertions.assertNotNull(book);
}
public class Book {
    private String title;

    public Book(String title) {
        this.title = title;
    }
}

显式转换

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testConverter(
        @ConvertWith(ToStringArgumentConverter.class) String arg
) {
    Assertions.assertNotNull(ChronoUnit.valueOf(arg));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
    @Override
    protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
        assertEquals(String.class, targetType, "Can only convert to String");
        if (source instanceof Enum<?>)
            return ((Enum<?>) source).name();
        return String.valueOf(source);
    }
}

TypedArgumentConverter使用泛型进行特定类型到特定类型的转换。

显式转换可以使用注解进行简化。

@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConvertWith(ToStringArgumentConverter.class)
public @interface ToStringPattern {

}


@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testConverterWithAnnotation(
        @ToStringPattern String arg
) {
    Assertions.assertNotNull(ChronoUnit.valueOf(arg));
}

对于这种方式还想传参数的,参考JUnit的@JavaTimeArgumentConverter

参数聚合

由于Source中的参数只能和测试方法中的参数一对一对应,如果需要携带大量参数,那么测试方法将有巨大的签名,需要使用一个办法将其聚合

@ParameterizedTest
@CsvSource({
        "Jane, Doe, FEMALE, 1990-05-20",
        "John, Doe, MALE, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor accessor) {
    Person person = new Person(
            accessor.getString(0),
            accessor.getString(1),
            accessor.get(2, Gender.class),
            accessor.get(3, LocalDate.class)
    );

    assertNotNull(person);
}

这能用,但是这个ArgumentsAccessor和测试方法中为了将其转换成Person所写的代码,和这个测试用例又有什么关系呢?放这碍眼。

@ParameterizedTest
@CsvSource({
        "Jane, Doe, FEMALE, 1990-05-20",
        "John, Doe, MALE, 1990-10-22"
})
void testWithArgumentsAccessor(
        @AggregateWith(PersonAggregation.class) Person person) {
    assertNotNull(person);
}

同样,如果你看@AggregateWith(PersonAggregation.class)不顺眼,也可以自己定义一个注解类。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
    // perform assertions against person
}

动态测试

@TestFactory
Stream<DynamicTest> randomNumberTest() {
    Iterator<Integer> inputGenerator = new Iterator<Integer>() {
        Random random = new Random();
        int cur = 0;
        @Override
        public boolean hasNext() {
            return cur < 10;
        }

        @Override
        public Integer next() {
            cur++;
            return random.nextInt(100);
        }
    };

    Function<Integer, String> displayNameGenerator = (input) -> "input: " + input;

    ThrowingConsumer<Integer> testExecutor = (input) -> assertEquals(input * 2, calculator.multiply(input, 2));

    return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}

更简单的写法,如果不需要迭代器生成测试参数时

@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
    Stream<Named<String>> inputStream = Stream.of(
            named("racecar is a palindrome", "racecar"),
            named("radar is also a palindrome", "radar")
    );

    return DynamicTest.stream(inputStream, text->assertNotNull(text));
}

DynamicNode代表一个动态测试节点,这个节点可以是一个DynamicTest和一个DynamicContainer,前者代表一个动态测试,后者代表一个测试容器,容器中可以包含其他容器和测试,这就构成了嵌套的测试。

@TestFactory
Stream<DynamicNode> dynamicTestWithContainers() {
    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()))
                    ))
            )));
}

@Timeout

在测试方法,测试工厂,测试模板和生命周期方法上添加@Timeout,可以让该方法在指定时间内若未完成则自动放弃执行。默认的时间单位是秒,但是可配置。

这个和之前的assertTimeoutPreemptively()不同,这个注解标注的测试方法运行在主线程,当超时时,由其他线程来打断主线程。这能保证JUnit与类似Spring这种对线程敏感的框架一起工作。

如在类上添加@Timeout,那么类内部的所有测试方法和嵌套测试类中的测试方法都将应用同一个超时时间(除非在内部类和测试方法上也有@Timeout)。

类层级上的@Timeout将不会作用到生命周期方法上

当它作用于@TestFactory上时,它对整个工厂中的测试执行奏效,但不会验证其中的每一个DynamicNode,请用assertTimeout等方法代替。

并行测试

默认情况下JUnit Jupiter在单线程中顺序测试每个方法,可以通过junit.jupiter.execution.parallel.enabled字段配置

配置之后,默认情况下还是顺序执行,还需要设置JUnit的执行模式。junit.jupiter.execution.parallel.mode.default

  • SAME_THREAD
    强制方法在父线程中执行。如果使用这个,所有测试方法会在与@BeforeAll@AfterAll执行的线程中执行
  • CONCURRENT
    并发执行除非被资源锁强制在单一线程执行

除此之外,也可以通过@Execution注解指定单一类的执行模式。

通过如下配置,可以让类并行测试,而类中的方法顺序执行

junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent

同步机制

在并行测试模式下,一遇到共享资源便需要一些同步机制。@ResourceLock注解为方法开启基于读写锁的同步机制。

posted @ 2021-10-09 16:05  yudoge  阅读(603)  评论(0编辑  收藏  举报