• 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

  • 相关阅读:
    chrome浏览器中安装以及使用Elasticsearch head 插件
    windows10 升级并安装配置 jmeter5.3
    linux下部署Elasticsearch6.8.1版本的集群
    【Rollo的Python之路】Python 爬虫系统学习 (八) logging模块的使用
    【Rollo的Python之路】Python 爬虫系统学习 (七) Scrapy初识
    【Rollo的Python之路】Python 爬虫系统学习 (六) Selenium 模拟登录
    【Rollo的Python之路】Python 爬虫系统学习 (五) Selenium
    【Rollo的Python之路】Python 爬虫系统学习 (四) XPath学习
    【Rollo的Python之路】Python 爬虫系统学习 (三)
    【Rollo的Python之路】Python sys argv[] 函数用法笔记
  • 原文地址:https://www.cnblogs.com/felordcn/p/12142555.html
Copyright © 2020-2023  润新知