zoukankan      html  css  js  c++  java
  • 08-单元测试

    1. JUnit5 简述

    Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。

    作为最新版本的 JUnit 框架,JUnit5 与之前版本的 Junit 框架有很大的不同。由 3 个不同子项目的几个不同模块组成。

    JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

    • 【JUnit Platform】Junit Platform 是在 JVM 上启动测试框架的基础,不仅支持 JUnit 自制的测试引擎,其他测试引擎也都可以接入;
    • 【JUnit Jupiter】JUnit Jupiter提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心。内部包含了一个测试引擎,用于在 Junit Platform 上运行;
    • 【JUnit Vintage】由于 JUint 已经发展多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x、JUnit3.x 的测试引擎。

    导入 test-starter,本人学习使用的 2.4.3 的 SpringBoot:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    查看 Maven-Dependencies:

    https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes

    【JUnit 5’s Vintage Engine Removed from spring-boot-starter-test】If you upgrade to Spring Boot 2.4 and see test compilation errors for JUnit classes such as org.junit.Test, this may be because JUnit 5’s vintage engine has been removed from spring-boot-starter-test. The vintage engine allows tests written with JUnit 4 to be run by JUnit 5. If you do not want to migrate your tests to JUnit 5 and wish to continue using JUnit 4, add a dependency on the Vintage Engine, as shown in the following example for Maven:

    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.hamcrest</groupId>
                <artifactId>hamcrest-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    

    2. 常用注解

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

    注解 介绍
    @Test 表示方法是测试方法;但是与 JUnit4 的 @Test 不同,他的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试
    @Timeout 表示测试方法运行如果超过了指定时间将会返回错误
    @Disabled 表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore
    @DisplayName 为测试类或者测试方法设置展示名称
    @ExtendWith 为测试类或测试方法提供扩展类引用 @SpringBootTest=xxx+@ExtendWith(SpringExtension.class)
    @BeforeEach 表示在每个单元测试之前执行
    @AfterEach 表示在所有单元测试之前执行
    @BeforeAll 表示在所有单元测试之前执行
    @AfterAll 表示在所有单元测试之后执行
    @Tag 表示单元测试类别,类似于 JUnit4 中的 @Categories
    @RepeatedTest 表示方法可重复执行
    @ParameterizedTest 表示方法是参数化测试,下方会有详细介绍

    3. 断言

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions

    断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。

    • 简单断言
    • 数组断言:通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等;
    • 组合断言:assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言;
    • 异常断言:Assertions.assertThrows() 测试方法的异常情况,配合函数式编程就可以进行使用;
    • 超时断言:Assertions.assertTimeout() 为测试方法设置了超时时间;
    • 快速失败:通过 fail 方法直接使得测试失败。
    class AssertionsDemo {
    
        private final Calculator calculator = new Calculator();
    
        private final Person person = new Person("Jane", "Doe");
    
        @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('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
                    + "to avoid constructing complex messages unnecessarily.");
        }
    
        @Test
        void groupedAssertions() {
            // In a grouped assertion all assertions are executed, and all
            // failures will be reported together.
            assertAll("person",
                () -> assertEquals("Jane", person.getFirstName()),
                () -> assertEquals("Doe", person.getLastName())
            );
        }
    
        @Test
        void dependentAssertions() {
            // Within a code block, if an assertion fails the
            // subsequent code in the same block will be skipped.
            assertAll("properties",
                () -> {
                    String firstName = person.getFirstName();
                    assertNotNull(firstName);
    
                    // Executed only if the previous assertion is valid.
                    assertAll("first name",
                        () -> assertTrue(firstName.startsWith("J")),
                        () -> assertTrue(firstName.endsWith("e"))
                    );
                },
                () -> {
                    // Grouped assertion, so processed independently
                    // of results of first name assertions.
                    String lastName = person.getLastName();
                    assertNotNull(lastName);
    
                    // Executed only if the previous assertion is valid.
                    assertAll("last name",
                        () -> assertTrue(lastName.startsWith("D")),
                        () -> assertTrue(lastName.endsWith("e"))
                    );
                }
            );
        }
    
        @Test
        void exceptionTesting() {
            Exception exception = assertThrows(ArithmeticException.class, () ->
                calculator.divide(1, 0));
            assertEquals("/ by zero", exception.getMessage());
        }
    
        @Test
        void timeoutNotExceeded() {
            // The following assertion succeeds.
            assertTimeout(ofMinutes(2), () -> {
                // Perform task that takes less than 2 minutes.
            });
        }
    
        @Test
        void timeoutNotExceededWithResult() {
            // The following assertion succeeds, and returns the supplied object.
            String actualResult = assertTimeout(ofMinutes(2), () -> {
                return "a result";
            });
            assertEquals("a result", actualResult);
        }
    
        @Test
        void timeoutNotExceededWithMethod() {
            // The following assertion invokes a method reference and returns an object.
            String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
            assertEquals("Hello, World!", actualGreeting);
        }
    
        @Test
        void timeoutExceeded() {
            // The following assertion fails with an error message similar to:
            // execution exceeded timeout of 10 ms by 91 ms
            assertTimeout(ofMillis(10), () -> {
                // Simulate task that takes more than 10 ms.
                Thread.sleep(100);
            });
        }
    
        @Test
        void timeoutExceededWithPreemptiveTermination() {
            // The following assertion fails with an error message similar to:
            // execution timed out after 10 ms
            assertTimeoutPreemptively(ofMillis(10), () -> {
                // Simulate task that takes more than 10 ms.
                new CountDownLatch(1).await();
            });
        }
    
        private static String greeting() {
            return "Hello, World!";
        }
    
    }
    

    4. 前置条件

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-assumptions

    JUnit 5 中的前置条件 「assumptions(假设)」类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

    class AssumptionsDemo {
    
        private final Calculator calculator = new Calculator();
    
        @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, calculator.divide(4, 2));
                });
    
            // perform these assertions in all environments
            assertEquals(42, calculator.multiply(6, 7));
        }
    
    }
    

    5. 嵌套测试

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested

    补充:内层可以驱动外层的 @Before/After|Each/All,但外层不能驱动内层的。

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

    6. 参数化测试

    https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

    参数化注解 简述
    @ValueSource 为参数化测试指定入参来源,支持 8 大基础类以及 String、Class 类型
    @NullSource 表示为参数化测试提供一个 null 的入参
    @EnumSource 表示为参数化测试提供一个枚举入参
    @CsvFileSource 表示读取指定 CSV 文件内容作为参数化测试入参
    @MethodSource 表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

    The following example demonstrates a parameterized test that uses the @ValueSource annotation to specify a String[] as the source of arguments.

    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    void palindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }
    

    When executing the above parameterized test method, each invocation will be reported separately. For instance, the ConsoleLauncher will print output similar to the following.

    palindromes(String) ✔
    ├─ [1] candidate=racecar ✔
    ├─ [2] candidate=radar ✔
    └─ [3] candidate=able was I ere I saw elba ✔
    

    If you only need a single parameter, you can return a Stream of instances of the parameter type as demonstrated in the following example.

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithExplicitLocalMethodSource(String argument) {
        assertNotNull(argument);
    }
    
    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana");
    }
    

    7. 迁移指南

    在进行迁移的时候需要注意如下的变化:

    • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中;
    • @Before@After 替换成 @BeforeEach@AfterEach
    • @BeforeClass@AfterClass 替换成 @BeforeAll@AfterAll
    • @Ignore 替换成 @Disabled
    • @Category 替换成 @Tag
    • @RunWith@Rule@ClassRule 替换成 @ExtendWith
  • 相关阅读:
    hdu 4027 Can you answer these queries? 线段树
    ZOJ1610 Count the Colors 线段树
    poj 2528 Mayor's posters 离散化 线段树
    hdu 1599 find the mincost route floyd求最小环
    POJ 2686 Traveling by Stagecoach 状压DP
    POJ 1990 MooFest 树状数组
    POJ 2955 Brackets 区间DP
    lightoj 1422 Halloween Costumes 区间DP
    模板 有源汇上下界最小流 loj117
    模板 有源汇上下界最大流 loj116
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/15269840.html
Copyright © 2011-2022 走看看