当前位置:   article > 正文

springboot 之 单元测试

springboot 之 单元测试

一、JUnit5 的变化

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

<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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

加入test依赖后:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
在springboot2.4后,创建项目会自动生成如下的测试类:

@SpringBootTest
class WebAdminApplicationTests {


    @Test
    void contextLoads() {

    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在springboot2.4后,只需要@SpringBootTest标明是测试类,在通过@Test标明在方法上即可。

在springboot2.4以前,需要通过@SpringBootTest + @RunWith才能实现测试方法,比如以下代码:

@RunWith(SpringRunner.class)
@SpringBootTest
class TaskRepositoryTest2 {

    @Autowired
    private TaskRepository taskRepository;

    @Test
    void test() {
        Optional<Task> task = taskRepository.findById((long) 1);
        System.out.println("Task:: " + task.get());
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

SpringBoot整合Junit以后,写测试类的方法:

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚。

二、JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化,详细可浏览官方文档https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

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

注意:

  • BeforeAll 、AfterAll 因为只会在所有的测试方法之前或之后执行一次,因此必须是static的方法。
  • Junit5没有使用springboot的测试驱动,因此不包含自动注入的功能。如果要使用自动注入,即使用springboot的容器功能,必须加上@SpringBootTest注解
import org.junit.jupiter.api.Test; //注意这里使用的是jupiter的Test注解!!


public class TestDemo {

  @Test
  @DisplayName("第一次测试")
  public void firstTest() {
      System.out.println("hello world");
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

@DisplayName

在这里插入图片描述

@Timeout

在这里插入图片描述

@BeforeEach 、@AfterEach 、@BeforeAll 、@AfterAll

在这里插入图片描述

@Disabled

在这里插入图片描述
该注解标注的方法不执行

@RepeatedTest

在这里插入图片描述
在这里插入图片描述
重复执行几次。

三、断言(assertions)

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。也就是说,我们认为在测试过程中必须出现的现象,没有出现则意味着程序出错。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。

断言的优点:

  • 检查业务逻辑返回的数据是否合理。
  • 所有的测试运行结束以后,会有一个详细的测试报告;

在项目上线之前跑一下测试,我们可以看到会有一个详细的测试报告
在这里插入图片描述
在这里插入图片描述

JUnit 5 内置的断言可以分成如下几个类别:

3.1、简单断言

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

方法说明
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
@Test
@DisplayName("simple assertion")
public void simple() {
     assertEquals(3, 1 + 2, "simple math");
     assertNotEquals(3, 1 + 1);

     assertNotSame(new Object(), new Object());
     Object obj = new Object();
     assertSame(obj, obj);

     assertFalse(1 > 2);
     assertTrue(1 < 2);

     assertNull(null);
     assertNotNull(new Object());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

assertEquals :判断两个对象或两个原始类型是否相等。该方法需要导入static org.junit.jupiter.api.Assertions.*包,static说明这个包里面的方法是静态的方法,可以直接调用里面的方法,不用包名。该方法一般有两个参数,一个是期望的值,一个是实际计算结果的值,他会判断至两个是否相等,若不相等则会报错,我们可以给该方法在添加一个message的参数,该参数会在报错信息中展现出来。要注意,断言执行失败,后面的代码都不会执行。
在这里插入图片描述
assertSame:判断两个对象引用是否指向同一个对象
在这里插入图片描述

3.2、数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

@Test
@DisplayName("array assertion")
public void array() {
 assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
可以通过测试方法的名字判断出该测试方法已经执行了,没有报错说明期望和实际的数组一样。

3.3、组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

@Test
@DisplayName("assert all")
public void all() {
 assertAll("Math",
    () -> assertEquals(2, 1 + 1),
    () -> assertTrue(1 > 0)
 );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述
声明的断言所有都成功才会成功,如果有一个失败后面的代码都不会执行。

在这里插入图片描述

3.4、异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。(因为异常一定会抛出异常)
在这里插入图片描述
咦,是不是很好奇,怎么执行成功了?这是因为我们在这断言他会出现算术运算异常(ArithmeticException.class),因此执行成功。如果业务逻辑没有算术异常的情况的话,则会报错。
在这里插入图片描述

3.5、超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

@Test
@DisplayName("超时测试")
public void timeoutTest() {
    //如果测试方法时间超过1s将会异常
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.6、快速失败

通过 fail 方法直接使得测试失败
在这里插入图片描述

四、前置条件(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")
    );
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述
在这里插入图片描述

注意:

  • 前置条件如果不符合条件的话是不执行的,他相当于跳过了后面的方法,而不是直接执行失败,这个可以通过测试报告得知确实是跳过。
  • assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

五、嵌套测试

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

package com.xzz.web_admin;


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 org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.EmptyStackException;
import java.util.Stack;
import java.util.stream.Stream;

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

@DisplayName("嵌套测试")
public class TestingAStackDemo {

    Stack<Object> stack;



    @ParameterizedTest
    @DisplayName("参数化测试")
    @ValueSource(ints = {1,2,3,4,5})
    void testParameterized(int i){
        System.out.println(i);
    }


    @ParameterizedTest
    @DisplayName("参数化测试")
    @MethodSource("stringProvider")
    void testParameterized2(String i){
        System.out.println(i);
    }


    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana","atguigu");
    }

    @Test
    @DisplayName("new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
        //嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/之后运行
        assertNull(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可以驱动外层的Before(After)Each/All之类的方法提前/之后运行
             */
            @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());
            }
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

注意:

  • 嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/之后运行
  • 内层的Test可以驱动外层的Before(After)Each/All之类的方法提前/之后运行

六、参数化测试

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

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

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

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

在这里插入图片描述
@ParameterizedTest表示的是参数化测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上面这些测试的方法都是基于Junit5进行测试的,如果之前是用Junit4进行测试,那怎么迁移到Junit5呢?官方文档给出了明确的答案https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4

另外,我们在从Junit5迁移到Junit4的时候还需注意如下的变化:

  • 注解在 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。

这些仅仅是Junit5的冰山一角,如果要对Junit5进行更深入的学习可以参考官方文档https://junit.org/junit5/docs/current/user-guide/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/653882
推荐阅读
相关标签
  

闽ICP备14008679号