zoukankan      html  css  js  c++  java
  • Spring Boot 2 实战:mock测试你的web应用

    在这里插入图片描述

    1. 概要

    软件测试是一个应用软件质量的保证。java开发者开发接口往往忽视接口单元测试。作为java开发如果会Mock单元测试,那么你的bug量将会大大降低。spring提供test测试模块,所以现在小胖哥带你来玩下springboot下的Mock单元测试,我们将对controller,service 的单元测试进行实战操作。

    2. 依赖引入

    ​​

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

    按照上面引入依赖而且scope为test。该依赖提供了一下类库

    • JUnit 4: 目前最强大的java应用单元测试框架
    • Spring Test & Spring Boot Test: Spring Boot 集成测试支持.
    • AssertJ: 一个java断言库,提供测试断言支持.
    • Hamcrest: 对象匹配断言和约束组件.
    • Mockito: 知名 Java mock 模拟框架.
    • JSONassert: JSON断言库.
    • JsonPath: JSON XPath 操作类库.

    以上都是在单元测试中经常接触的类库。有时间你最好研究一下。

    3. 配置测试环境

    一个Spring Boot 应用程序是一个Spring ApplicationContext ,一般测试不会超出这个范围。
    测试框架提供一个@SpringBootTest注解来提供SpringBoot单元测试环境支持。你使用的JUnit版本如果是JUnit 4不要忘记在测试类上添加@RunWith(SpringRunner.class)JUnit 5就不需要了。默认情况下,@SpringBootTest不会启动服务器。您可以使用其 webEnvironment 属性进一步优化测试的运行方式,webEnvironment 相关讲解:

    • MOCK(默认):加载Web ApplicationContext并提供模拟Web环境。该选择下不会启动嵌入式服务器。如果类路径上没有Web环境,将创建常规非Web的 ApplicationContext。你可以配合@AutoConfigureMockMvc@AutoConfigureWebTestClient模拟的Web应用程序。
    • RANDOM_PORT:加载 WebServerApplicationContext 并提供真实的Web环境,启用的是随机web容器端口。
    • DEFINED_PORT:加载 WebServerApplicationContext 并提供真实的Web环境 和 RANDOM_PORT 不同的是启用你激活的SpringBoot应用端口,通常都声明在application.yml配置文件中。
    • NONE:通过SpringApplication加载一个ApplicationContext。但不提供 任何 Web环境(无论是Mock或其他)。

    注意事项:如果你的测试带有@Transactional注解时,默认情况下每个测试方法执行完就会回滚事务。但是当你的 webEnvironment 设置为RANDOM_PORT或者 DEFINED_PORT,也就是隐式地提供了一个真实的servlet web环境时,是不会回滚的。这一点特别重要,请确保不会在生产发布测试中写入脏数据。

    4. 编写测试类测试你的api

    言归正传,首先我们编写了一个 BookService 作为Service 层
    ​​

    package cn.felord.mockspringboot.service;
    
    import cn.felord.mockspringboot.entity.Book;
    
    /**
     * The interface Book service.
     *
     * @author Dax
     * @since 14 :54  2019-07-23
     */
    public interface BookService {
    
        /**
         * Query by title book.
         *
         * @param title the title
         * @return the book
         */
        Book queryByTitle(String title);
    
    }
    

    其实现类如下,为了简单明了没有测试持久层,如果持久层需要测试注意增删改需要Spring事务注解@Transactional支持以达到测试后回滚的目的。

    package cn.felord.mockspringboot.service.impl;
    
    import cn.felord.mockspringboot.entity.Book;
    import cn.felord.mockspringboot.service.BookService;
    import org.springframework.stereotype.Service;
    
    import java.time.LocalDate;
    
    /**
     * @author Dax
     * @since 14:55  2019-07-23
     */
    @Service
    public class BookServiceImpl implements BookService {
    
    
        @Override
        public Book queryByTitle(String title) {
            Book book = new Book();
            book.setAuthor("dax");
            book.setPrice(78.56);
            book.setReleaseTime(LocalDate.of(2018, 3, 22));
            book.setTitle(title);
            return book;
        }
    }
    

    ​​

    controller层如下:

    ​​

    package cn.felord.mockspringboot.api;
    
    import cn.felord.mockspringboot.entity.Book;
    import cn.felord.mockspringboot.service.BookService;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * @author Dax
     * @since 10:24  2019-07-23
     */
    @RestController
    @RequestMapping("/book")
    public class BookApi {
        @Resource
        private BookService bookService;
    
        @GetMapping("/get")
        public Book getBook(String title) {
            return bookService.queryByTitle(title);
        }
    
    }
    

    我们在Spring Boot maven项目的单元测试包 test下对应的类路径 编写自己的测试类

    ​​

     package cn.felord.mockspringboot;
     
     import cn.felord.mockspringboot.entity.Book;
     import cn.felord.mockspringboot.service.BookService;
     import org.assertj.core.api.Assertions;
     import org.junit.Test;
     import org.junit.runner.RunWith;
     import org.mockito.BDDMockito;
     import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
     import org.springframework.boot.test.context.SpringBootTest;
     import org.springframework.boot.test.mock.mockito.MockBean;
     import org.springframework.test.context.junit4.SpringRunner;
     import org.springframework.test.web.servlet.MockMvc;
     import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
     import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
     import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
     
     import javax.annotation.Resource;
     import java.time.LocalDate;
     
     /**
      * The type Mock springboot application tests.
      */
     @RunWith(SpringRunner.class)
     @SpringBootTest
     @AutoConfigureMockMvc
     public class MockSpringbootApplicationTests {
         @Resource
         private MockMvc mockMvc;
         @MockBean
         private BookService bookService;
     
         @Test
         public void bookApiTest() throws Exception {
             String title = "java learning";
             // mockbean 开始模拟
             bookServiceMockBean(title);
             // mockbean 模拟完成
             String expect = "{"title":"java learning","author":"dax","price":78.56,"releaseTime":"2018-03-22"}";
             mockMvc.perform(MockMvcRequestBuilders.get("/book/get")
                     .param("title", title))
                     .andExpect(MockMvcResultMatchers.content()
                             .json(expect))
                     .andDo(MockMvcResultHandlers.print());
             // mockbean 重置
         }
       
         @Test
         public void bookServiceTest() {
     
             String title = "java learning";
             bookServiceMockBean(title);
     
     
             Assertions.assertThat(bookService.queryByTitle("ss").getTitle()).isEqualTo(title);
     
         }
           /**
            * Mock打桩
            * @param title the title
            */   
         private void bookServiceMockBean(String title) {
     
             Book book = new Book();
             book.setAuthor("dax");
             book.setPrice(78.56);
             book.setReleaseTime(LocalDate.of(2018, 3, 22));
             book.setTitle(title);
     
             BDDMockito.given(bookService.queryByTitle(title)).willReturn(book);
         }
      }
    

    测试类前两个注解不用说,第三个注解@AutoConfigureMockMvc可能你们很陌生。这个是用来开启Mock Mvc测试的自动化配置的。

    然后我们编写一个测试方法bookApiTest()来测试BookApi#getBook(String title)接口。
    ​​

    逻辑是 MockMvc 执行一个模拟的get请求然后期望结果是expect Json字符串并且将相应对象打印了出来(下图1标识)。一旦请求不通过将抛出java.lang.AssertionError错误, 会把期望值(Expected)跟实际值打印出来(下图2标识)。如果跟预期相同只会出现下图1。


    ​​

    5. 测试打桩

    有个很常见的情形,在开发中有可能你调用的其他服务没有开发完,比如你有个短信发送接口还在办理短信接口手续,但是你还需要短信接口来进行测试。你可以通过@MockBean 构建一个抽象接口的实现。拿上面的BookService来说,假如其实现类逻辑还没有确定,我们可以通过规定其入参以及对应的返回值来模拟这个bean的逻辑,或者根据某个情形下进行某个路由操作的选择(如果入参是A则结果为B,如果为C则D)。这种模拟也被成为测试打桩。 这里我们会用到Mockito

    测试场景描述如下:

    1. 指定打桩对象的返回值
    2. 判断某个打桩对象的某个方法被调用及调用的次数
    3. 指定打桩对象抛出某个特定异常

    一般有以下几种组合:

    • do/when:包括doThrow(…).when(…) / doReturn(…).when(…) / doAnswer(…).when(…)
    • given/will:包括given(…).willReturn(…) / given(…).willAnswer(…)
    • when/then: 包括when(…).thenReturn(…) / when(…).thenAnswer(…)

    其他都好理解,着重介绍一下Answer , Answer 正是为了解决如果入参是A则结果为B,如果为C则D这种路由操作的。接下来我们实操一下 ,跟最开始基本一样,只是更换成@MockBean

    ​​

    然后利用Mockito编写打桩方法void bookServiceMockBean(String title),模拟上面BookServiceImpl 实现类。不过模拟的bean每次测试完都会自动重置。而且不能用于模拟在应用程序上下文刷新期间运行的bean的行为。

    ​​

    然后把这个方法注入controller 测试方法就可以测试了。

    ​​

    6. 其他

    内置的assertj也是常用的断言,api非常友好,这里也通过bookServiceTest()简单演示了一下

    ​​

    7. 总结

    本文中实现了一些简单的Spring Boot启用集成测试。 对测试环境的搭建,测试代码的编写进行了实战操作,基本能满足日常开发测试需要,相信你能从本文学到不少东西。

    相关的讲解代码可以从gitee获取。

    也可通过我 个人博客 及时获取更多的干货分享。

    关注公众号:Felordcn获取更多资讯

    个人博客:https://felord.cn

  • 相关阅读:
    Spring Boot 自定义starter
    jvm中的年轻代 老年代 持久代 gc
    nginx反向代理服务器端口问题
    ACE Editor在线代码编辑器简介及使用引导
    Linux下MySQL 5.6.24的编译安装与部署
    C3p0的参数
    Mysql 查看连接数,状态
    linux下mysql定时备份数据库
    Mysql中存储方式的区别
    mysql常用语句
  • 原文地址:https://www.cnblogs.com/felordcn/p/12142555.html
Copyright © 2011-2022 走看看