SpringBoot单元测试

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    目录

            1、JUnit5 的变化

            2、JUnit5常用注解

            3、断言assertions

            4、前置条件assumptions

            5、嵌套测试

            6、参数化测试

            7、迁移指南


    1、JUnit5 的变化

    官网JUnit 5 User Guide


    Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
    作为最新版本的JUnit框架JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
    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的测试引擎。


    注意
    SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入不能使用junit4的功能 @Test
    JUnit 5’s Vintage Engine Removed from spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage

    <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>

     导入依赖

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

    以前

    @SpringBootTest + @RunWith(SpringTest.class)

    现在版本 

    @SpringBootTest //该注解可以使用注入功能
    @DisplayName("Junit5测试")
    public class Junit5Test {
    
        @Test
        void contextLoads() {
    
        }
    
    }

    @SpringBoot是@BootstrapWith和@ExtendWith的复合注解在junit5中@RunWith被替换为@ExtendWith

     

     

    2、JUnit5常用注解

    JUnit5的注解与JUnit4的注解有所变化

    JUnit 5 User Guide

    • @Test :表示方法是测试方法。但是与JUnit4的@Test不同他的职责非常单一不能声明任何属性拓展的测试将会由Jupiter提供额外测试
    • @ParameterizedTest :表示方法是参数化测试下方会有详细介绍
    • @RepeatedTest :表示方法可重复执行下方会有详细介绍
    • @DisplayName :为测试类或者测试方法设置展示名称
    • @BeforeEach :表示在每个单元测试之前执行
    • @AfterEach :表示在每个单元测试之后执行
    • @BeforeAll :表示在所有单元测试之前执行
    • @AfterAll :表示在所有单元测试之后执行
    • @Tag :表示单元测试类别类似于JUnit4中的@Categories
    • @Disabled :表示测试类或测试方法不执行类似于JUnit4中的@Ignore
    • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
    • @ExtendWith :为测试类或测试方法提供扩展类引用
    @SpringBootTest //该注解可以使用注入功能
    @DisplayName("Junit5测试")
    public class Junit5Test {
    
    
        @RepeatedTest(value = 5) //重复运行五次
        @Test
        @DisplayName("测试方法1")
        void test1() {
            System.out.println(1);
        }
    
        @Disabled //忽略(不测试)该方法相当与之前@ignore
        @Test
        @DisplayName("测试方法2") //给方法命名
        void test2() {
            System.out.println(2);
        }
    
        @BeforeEach
        void testbeforeEach() {
            System.out.println("测试开始");
        }
    
        @AfterEach
        void testafterEach() {
            System.out.println("测试结束");
        }
    
        @BeforeAll
        static void testBeforeAll() {
            System.out.println("所有测试要开始了");
        }
    
        @AfterAll
        static void testAfterAll() {
            System.out.println("所有测试要结束了");
        }
    }

     

    3、断言assertions

    断言assertions是测试方法中的核心部分用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别

    检查业务逻辑返回的数据是否合理。

    所有的测试运行结束以后会有一个详细的测试报告

    1、简单断言

    用来对单个值进行简单的验证。如

    方法

    说明

    assertEquals

    判断两个对象或两个原始类型是否相等

    assertNotEquals

    判断两个对象或两个原始类型是否不相等

    assertSame

    判断两个对象引用是否指向同一个对象

    assertNotSame

    判断两个对象引用是否指向不同的对象

    assertTrue

    判断给定的布尔值是否为 true

    assertFalse

    判断给定的布尔值是否为 false

    assertNull

    判断给定的对象引用是否为 null

    assertNotNull

    判断给定的对象引用是否不为 null

     

    @SpringBootTest //该注解可以使用注入功能
    @DisplayName("Junit5测试")
    public class Junit5Test {
    
    
        /*
         * 前面断言失败后面的代码不会执行
         */
        @Test
        @DisplayName("测试简单断言")
        void testAssertion() {
            int cal = cal(3, 4);
            assertEquals(6, cal, "计算错误");
            Object o1 = new Object();
            Object o2 = new Object();
            assertSame(o1,o2,"对象不同");
        }
    
        @Test
        @DisplayName("测试数组断言")
        void testArrayAssertion() {
            assertArrayEquals(new int[]{1,2}, new int[]{1,2});
        }
    
        @Test
        @DisplayName("测试组合断言")
        void testAllAssertion() {
            assertAll("test",() ->assertTrue(true && true,"结果不为true"),
                    () -> assertEquals(1,2,"结果不为1"));
        }
    
        @Test
        @DisplayName("测试快速失败")
        void testFailAssertion() {
            if(1 == 1)
                fail("测试失败");
        }
    
        @Test
        @DisplayName("测试异常断言") //断定一定会抛出异常抛出即断言成功
        void testException() {
            assertThrows(ArithmeticException.class, ()->{int i = 1/0;},"代码居然没出错");
        }
    
        int cal(int a, int b) {
            return a + b;
        }
    
    }

    4、前置条件assumptions

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

    @DisplayName("前置条件")
    public class AssumptionsTest {
     private final String environment = "DEV";
     
     @Test
     @DisplayName("simple")
     public void simpleAssume() {
        assumeTrue(Objects.equals(this.environment, "DEV"));
        assumeFalse(() -> Objects.equals(this.environment, "PROD"));
     }
     
     @Test
     @DisplayName("assume then do")
     public void assumeThenDo() {
        assumingThat(
           Objects.equals(this.environment, "DEV"),
           () -> System.out.println("In DEV")
        );
     }
    }

    assumeTrue 和 assumFalse 确保给定的条件为 true 或 false不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时Executable 对象才会被执行当条件不满足时测试执行并不会终止。

    5、嵌套测试

    JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解而且嵌套的层次没有限制。

    @DisplayName("A stack")
    class StackTest {
    
        Stack<Object> stack;
    
        @Test
        @DisplayName("is instantiated with new Stack()")
        void isInstantiatedWithNew() {
            new Stack<>();
            //报错外层类不能驱动内层类的Before/After(All/Each)方法
            assertNotNull(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() {
                    //不报错内层类会驱动外层类的Before/After(All/Each)方法提前创建new一个stack
                    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、参数化测试

    参数化测试是JUnit5很重要的一个新特性它使得用不同的参数多次运行测试成为了可能也为我们的单元测试带来许多便利。

    利用@ValueSource等注解指定入参我们将可以使用不同的参数进行多次单元测试而不需要每新增一个参数就新增一个单元测试省去了很多冗余代码。

    @ValueSource: 为参数化测试指定入参来源支持八大基础类以及String类型,Class类型

    @NullSource: 表示为参数化测试提供一个null的入参

    @EnumSource: 表示为参数化测试提供一个枚举入参

    @CsvFileSource表示读取指定CSV文件内容作为参数化测试入参

    @MethodSource表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

    当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口任何外部文件都可以作为它的入参。

    @ParameterizedTest
    @ValueSource(strings = {"one", "two", "three"})
    @DisplayName("参数化测试1")
    public void parameterizedTest1(String string) {
        System.out.println(string);
        Assertions.assertTrue(StringUtils.isNotBlank(string));
    }
    
    
    @ParameterizedTest
    @MethodSource("method")    //指定方法名
    @DisplayName("方法来源参数")
    public void testWithExplicitLocalMethodSource(String name) {
        System.out.println(name);
        Assertions.assertNotNull(name);
    }
    
    static Stream<String> method() { //需要是静态方法不然会报错
        return Stream.of("apple", "banana");
    }

    7、迁移指南

    从junit4迁移到junit5的迁移指导JUnit 5 User Guide
    在进行迁移的时候需要注意如下的变化
    ●注解在 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。

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
    标签: Spring