• 「SpringBoot」06 单元测试


    SpringBoot——单元测试

    笔记整理自【尚硅谷】雷神SpringBoot2视频教程

    1. JUnit5 的变化

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

    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的测试引擎。

    img

    注意

    • SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)

    • JUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:

      
      <dependency>
          <groupId>org.junit.vintagegroupId>
          <artifactId>junit-vintage-engineartifactId>
          <scope>testscope>
          <exclusions>
              <exclusion>
                  <groupId>org.hamcrestgroupId>
                  <artifactId>hamcrest-coreartifactId>
              exclusion>
          exclusions>
      dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      image.png

    现在版本

    @SpringBootTest
    class Boot05WebAdminApplicationTests {
    
        @Test
        void contextLoads() {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    创建SpringBoot项目 依赖已经自动引入好了

    <dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-starter-testartifactId>
       <scope>testscope>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    以前

    @SpringBootTest + @RunWith(SpringTest.class)
    
    • 1

    SpringBoot整合Junit以后:

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

    2. JUnit5常用注解

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

    JUnit5的所有注解

    • @Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试

    • @ParameterizedTest:表示方法是参数化测试。

    • @RepeatedTest:表示方法可重复执行。

      @RepeatedTest(2) // 再重复测试2次
      @Test
      void testRepeated() {
          System.out.println("haha");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      image-20220725202446449

    • @DisplayName:为测试类或者测试方法设置展示名称。

      @DisplayName("junit5功能测试类")
      public class Junit5Test {
      
          @DisplayName("测试DisplayName注解")
          @Test
          void testDisplayName() {
              System.out.println(1);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      image-20220725194358231

    • @BeforeEach:表示在每个单元测试之前执行。

    • @AfterEach:表示在每个单元测试之后执行。

      @BeforeEach
      void testBeforeEach() {
          System.out.println("测试就要开始了......");
      }
      
      @AfterEach
      void testAfterEach() {
          System.out.println("测试结束了......");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      image-20220725194627756

    • @BeforeAll:表示在所有单元测试之前执行。

    • @AfterAll:表示在所有单元测试之后执行。

      /**
       * 因为在所有方法执行之前需要调用此方法 所以这两个方法必须声明为static
       */
      @BeforeAll
      static void testBeforeAll() {
          System.out.println("所有测试就要开始了...");
      }
      
      @AfterAll
      static void testAfterAll() {
          System.out.println("所有测试以及结束了...");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      image-20220725195130602

    • @Tag:表示单元测试类别,类似于JUnit4中的@Categories。

    • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore。

      @Disabled
      @DisplayName("测试方法2")
      @Test
      void test2() {
          System.out.println(2);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      image-20220725195318989

    • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误。

      @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) // 规定方法的超时时间
      @Test
      void testTimeOut() throws InterruptedException {
          Thread.sleep(500);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      image-20220725200801786

    • @ExtendWith:为测试类或测试方法提供扩展类引用。

      @BootstrapWith(SpringBootTestContextBootstrapper.class)
      @ExtendWith(SpringExtension.class) // 是@SpringBootTest中的注解 意味着以下所有测试都是以Spring驱动测试的
      public @interface SpringBootTest {
      
      • 1
      • 2
      • 3

    3. 断言 (assertions)

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

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

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

    Ⅰ. 简单断言

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

    方法说明
    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
    • 17
    • 18
    • 19

    Ⅱ. 数组断言

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

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

    Ⅲ. 组合断言

    全部断言判定成功后,这个断言才算成功;有一个断言失败,都算失败。

    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

    Ⅳ. 异常断言

    在 JUnit4 时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而 JUnit5 提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。

    @Test
    @DisplayName("异常测试")
    public void exceptionTest() {
        assertThrows(ArithmeticException.class,() -> {
            // 扔出断言异常 有异常则正常运行 未出现异常则断言失败
            int i = 10 / 0;
        }, "业务逻辑居然正常运行?");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Ⅴ. 超时断言

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

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

    Ⅵ. 快速失败

    通过 fail() 方法直接使得测试失败

    @Test
    @DisplayName("fail")
    public void shouldFail() {
        fail("This should fail");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    断言的好处:

    • 使用 clean 然后 test,在项目上线之前,完整的跑一遍单元测试,就会有一个完整的单元测试汇总报告,哪个方法成功,哪个方法失败,失败的原因是什么,它会给我们一个汇总报告,方便我们来定位。
    • 一个大块业务逻辑开发完之后,一定要写它的单元测试类,来测试我们业务逻辑功能的正确性。

    image-20220725220539439

    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")
            );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • assumeTrue()assumFalse() 确保给定的条件为 truefalse,不满足条件会使得测试执行终止。
    • assumingThat() 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。
    • 只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

    5. 嵌套测试

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

    @DisplayName("A stack")
    class TestingAStackDemo {
    
        Stack<Object> stack;
    
        @Test
        @DisplayName("is instantiated with new Stack()")
        void isInstantiatedWithNew() {
            new Stack<>();
            // 嵌套测试情况下 外层的Test不能驱动内层的Before(After)Each(All)之类的方法提前/之后运行
            assertNotNull(stack); // 在下面嵌套类中 @BeforeEach方法执行了stack = 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可以驱动外层的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

    6. 参数化测试

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

    @Test => @ParameterizedTest

    利用@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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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
  • 相关阅读:
    五、MySQL的索引结构是什么样的?聚簇索引和非聚簇索引又是什么?
    NISP是什么?
    html02-标签继续学习
    基于C语言实现的足球信息查询系统 课程报告+项目源码+演示PPT+项目截图
    JavaCV开发详解之1:调用本机摄像头并预览摄像头图像画面视频(建议使用javaCV最新版本)
    GEE python——将GEE ASSETS中存储的影像或者矢量转化为数据格式XEE()
    性能测试包含哪些内容?
    Servlet执行流程&&Servlet 生命周期
    攻防世界web篇-backup
    技师学院物联网实训室建建设方案
  • 原文地址:https://blog.csdn.net/weixin_53407527/article/details/126021404