[跟着官方文档学JUnit5][五][WritingTests][学习笔记]

举报
John2021 发表于 2022/04/25 22:51:57 2022/04/25
【摘要】 1.测试实例生命周期为了允许单独执行各个测试方法,并避免由于可变测试实例状态而导致的意外,JUnit在执行每个测试方法之前为每个测试类创建一个新实例(参见测试类和方法)。这种"按方法"测试实例生命周期是JUnit Jupiter中的默认行为,类似于所有以前版本的JUnit。如果你希望JUnit Jupiter在同一测试实例上执行所有测试方法,使用@TestInstance(Lifecycl...

1.测试实例生命周期

为了允许单独执行各个测试方法,并避免由于可变测试实例状态而导致的意外,JUnit在执行每个测试方法之前为每个测试类创建一个新实例(参见测试类和方法)。这种"按方法"测试实例生命周期是JUnit Jupiter中的默认行为,类似于所有以前版本的JUnit。
如果你希望JUnit Jupiter在同一测试实例上执行所有测试方法,使用@TestInstance(Lifecycle.PER_CLASS)。使用此模式时,每个测试类将创建一个新的测试实例。因此,如果测试方法依赖于存储在实例变量中的状态,则可能需要在@BeforeEach或@AfterEach方法中重置该状态。
与默认的"per-method"模式相比,"per-class"模式有一些额外的好处。具体来说,使用"per-class"模式,可以在非静态方法和接口默认方法上声明@BeforeAll和@AfterAll。因此,"per-class"模式还可以在@Nested测试类中使用@BeforeAll和@AfterAll方法。

1.1.改变默认测试实例生命周期

如果测试类或测试接口未使用@TestInstance进行注解,则JUnit Jupiter将使用默认的生命周期模式。标准默认模式为PER_METHOD;但是,可以更改执行整个测试计划的默认值。若要更改默认测试实例生命周期模式,将junit.jupiter.testinstance.lifecycle.default配置参数设置为TestInstance.Lifecycle中定义的枚举常量的名称,忽略大小写。这可以作为JVM系统属性提供,也可以作为触发器发现请求中的配置参数提供给启动器,也可以通过JUnit平台配置文件提供。
例如,要将缺省测试实例生命周期模式设置为Lifecycle.PER_CLASS,可以使用以下系统属性启动JVM。

-Djunit.jupiter.testinstance.lifecycle.default=per_class

但请注意,通过JUnit platform配置文件设置默认的测试实例生命周期模式是一种更可靠的解决方案,因为配置文件可以与项目一起嵌入到版本控制系统,因此可以在IDE和构建软件中使用。
要将缺省测试实例生命周期模式设置为通过JUnit Platform配置文件Lifecycle.PER_CLASS,请在类路径(如src/test/resources)的根目录中创建一个名为junit-platform.properties的文件

junit.jupiter.testinstance.lifecycle.default = per_class

2.嵌套测试

@Nested测试为测试编写者提供了更多功能来表达几组测试之间的关系。这种嵌套测试利用Java的嵌套类,并促进对测试结构的分层思考。如下实例:

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 static org.junit.jupiter.api.Assertions.*;

import java.util.EmptyStackException;
import java.util.Stack;

@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 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());
            }
        }
    }
}

输出结果

在此示例中,通过为设置代码定义分层生命周期方法,在内部测试中使用外部测试的前提条件。例如,createNewStack()是一种@BeforeEach生命周期方法,用于定义它的测试类以及定义它的类下方嵌套树中的所有级别。
来自外部测试的设置代码在执行内部测试之前运行,这样能使你能够独立运行所有测试。甚至可以单独运行内部测试,而无需运行外部测试,因为始终执行外部测试中的设置代码。
注意:只有非静态嵌套类(即内部类)可以用作@Nested测试类。嵌套可以是任意深度的,并且这些内部类受完整生命周期支持的约束,但有一个例外:默认情况下,@BeforeAll和@AfterAll方法不起作用。原因是Java不允许内部类中的静态成员。但是,可以通过使用@TestInstance(Lifecycle.PER_CLASS)为@Nested测试类添加注解来规避(参见测试实例生命周期)。

3.为构造器和方法依赖注入

在所有以前的JUnit版本中,不允许测试构造函数或方法具有参数(至少在标准Runner实现中不允许)。作为JUnit Jupiter的主要变化之一,测试构造函数和方法现在都允许具有参数。这允许更大的灵活性,并为构造函数和方法启用依赖关系注入。
ParameterResolver为希望在运行时动态解析参数的测试拓展定义API。如果测试类构造函数、测试方法或生命周期方法(参见测试类和方法)接收参数,则该参数必须在运行时由已注册的ParameterResolver解析。
目前有三个内置的解析程序会自动注册。

  • TestInfoParameterResolver:如果构造函数或方法参数的类型为TestInfo,则TestInfoParameterResolver将提供与当前容器或test对应的TestInfo实例作为参数的值。然后,可以使用TestInfo检索有关当前容器或测试的信息,如显示名称、测试类、测试方法和关联的标记。显示名称可以是技术名称(如测试类或测试方法的名称),也可以是通过@DisplayName配置的自定义名称。TestInfo充当JUnit4中TestName规则的直接替代。下面演示如何将TestInfo注入到测试构造函数中,@BeforeEach方法和@Test方法。
import org.junit.jupiter.api.*;

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

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

    @BeforeEach
    void init(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,则RepetitionInfoParameterResolver将提供RepetitionInfo的实例。可以使用RepetitionInfo来检索有关当前重复和相应@RepeatedTest的重复总数的信息。注意,RepetitionInfoParameterResolver不会在@RepeatedTest上下文之外注册。参见重复测试示例。
  • TestReporterParameterResolver:如果构造函数或方法参数的类型为TestReporter,则TestReporterParameterResolver将提供TestReporter的实例。TestReporter可用于发布有关当前测试运行的其他数据。数据可以通过TestExecutionListener中的reportingEntryPublished()方法使用,从而允许在IDE中查看数据或将其包含在报告中。在JUnit Jupiter中,应该使用TestReporter,在JUnit4中,使用stdout或stderr打印信息。使用@RunWith(JUnitPlatform.class)会将所有报告的条目输出到stdout。
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 status message");
    }

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

    @Test
    void reportMultipleKeyValuePairs(TestReporter testReporter) {
        HashMap<String, String> values = new HashMap<>();
        values.put("user name", "hello");
        values.put("age", "18");
        testReporter.publishEntry(values);
    }
}

输出结果

查看RandomParameterExtension了解自定义参数解析器的示例。下面示例演示了如何将随机值注入到@Test方法中。

todo

4.测试接口和默认方法

JUnit Jupiter允许在接口默认方法上声明@Test、@RepeatedTest、@ParameterizedTest、@TestFactory、@TestTemplate、@BeforeEach和@AfterEach。@BeforeAll和@AfterAll可以在测试接口的静态方法上声明,也可以在接口默认方法上声明(如果测试接口或测试类使用@TestInstance(Lifecycle.PER_CLASS)进行注解)。

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        logger.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        logger.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("About to execute [%s]",
            testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        logger.info(() -> String.format("Finished executing [%s]",
            testInfo.getDisplayName()));
    }

}
interface TestInterfaceDynamicTestsDemo {

    @TestFactory
    default Stream<DynamicTest> dynamicTestsForPalindromes() {
        return Stream.of("racecar", "radar", "mom", "dad")
            .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
    }

}

可以在测试接口上声明@ExtendWith和@Tag,以便实现接口的类自动继承其标记和扩展。

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}

然后,在测试类中,您可以实现这些测试接口以应用它们。

class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

    @Test
    void isEqualValue() {
        assertEquals(1, "a".length(), "is always equal");
    }

}

运行 TestInterfaceDemo 将产生类似于以下内容的输出:

INFO  example.TestLifecycleLogger - Before all tests
INFO  example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()]
INFO  example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms.
INFO  example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()]
INFO  example.TestLifecycleLogger - About to execute [isEqualValue()]
INFO  example.TimingExtension - Method [isEqualValue] took 1 ms.
INFO  example.TestLifecycleLogger - Finished executing [isEqualValue()]
INFO  example.TestLifecycleLogger - After all tests

此功能的另一个可能的应用是为接口协定编写测试。例如,可以编写Object.equals或Comparable.compareTo的实现如下

public interface Testable<T> {
    T createValue();
}
import org.junit.jupiter.api.Test;

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

public interface EqualsContract<T> extends Testable<T> {
    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertFalse(value.equals(null));
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }
}
import org.junit.jupiter.api.Test;

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

public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {
    T createSmallValue();

    @Test
    default void returnsZeroWhenComparedToItself() {
        T value = createValue();
        assertEquals(0, value.compareTo(value));
    }

    @Test
    default void returnsPositiveNumberWhenComparedToSmallerValue() {
        T value = createValue();
        T smallerValue = createSmallValue();
        assertTrue(value.compareTo(smallerValue) > 0);
    }

    @Test
    default void returnsNegativeNumberWhenComparedToLargerValue() {
        T value = createValue();
        T smallerValue = createSmallValue();
        assertTrue(smallerValue.compareTo(value) < 0);
    }
}

在测试类中,可以实现两个协定接口,从而继承相应的测试。当然,必须实现抽象方法。

public class StringTests implements ComparableContract<String>, EqualsContract<String> {

    @Override
    public String createSmallValue() {
        return "apple";// 'a' < 'b' in "banana"
    }

    @Override
    public String createNotEqualValue() {
        return "cherry";
    }

    @Override
    public String createValue() {
        return "banana";
    }
}

输出结果

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。