51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 695|回复: 0

[转贴] 如何使用mock测试web应用?实战篇Spring Boot 2

[复制链接]
  • TA的每日心情
    无聊
    11 小时前
  • 签到天数: 919 天

    连续签到: 1 天

    [LV.10]测试总司令

    发表于 2022-8-10 13:09:25 | 显示全部楼层 |阅读模式
    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 层。
    1. package cn.felord.mockspringboot.service;
    2.   import cn.felord.mockspringboot.entity.Book;
    3.   /**
    4.    * The interface Book service.
    5.    *
    6.    * [url=home.php?mod=space&uid=267564]@Author[/url] Dax
    7.    * [url=home.php?mod=space&uid=295386]@since[/url] 14 :54  2019-07-23
    8.    */
    9.   public interface BookService {
    10.       /**
    11.        * Query by title book.
    12.        *
    13.        * @param title the title
    14.        * [url=home.php?mod=space&uid=26358]@return[/url] the book
    15.        */
    16.       Book queryByTitle(String title);
    17.   }
    复制代码
    其实现类如下,为了简单明了没有测试持久层,如果持久层需要测试注意增删改需要Spring事务注解  @Transactional  支持以达到测试后回滚的目的。
    1. package cn.felord.mockspringboot.service.impl;
    2.   import cn.felord.mockspringboot.entity.Book;
    3.   import cn.felord.mockspringboot.service.BookService;
    4.   import org.springframework.stereotype.Service;
    5.   import java.time.LocalDate;
    6.   /**
    7.    * @author Dax
    8.    * @since 14:55  2019-07-23
    9.    */
    10.   @Service
    11.   public class BookServiceImpl implements BookService {
    12.       @Override
    13.       public Book queryByTitle(String title) {
    14.           Book book = new Book();
    15.           book.setAuthor("dax");
    16.           book.setPrice(78.56);
    17.           book.setReleaseTime(LocalDate.of(2018, 3, 22));
    18.           book.setTitle(title);
    19.           return book;
    20.       }
    21.   }
    复制代码
    controller层如下:
    1.  package cn.felord.mockspringboot.api;
    2.   import cn.felord.mockspringboot.entity.Book;
    3.   import cn.felord.mockspringboot.service.BookService;
    4.   import org.springframework.web.bind.annotation.GetMapping;
    5.   import org.springframework.web.bind.annotation.RequestMapping;
    6.   import org.springframework.web.bind.annotation.RestController;
    7.   import javax.annotation.Resource;
    8.   /**
    9.    * @author Dax
    10.    * @since 10:24  2019-07-23
    11.    */
    12.   @RestController
    13.   @RequestMapping("/book")
    14.   public class BookApi {
    15.       @Resource
    16.       private BookService bookService;
    17.       @GetMapping("/get")
    18.       public Book getBook(String title) {
    19.           return bookService.queryByTitle(title);
    20.       }
    21.   }
    复制代码
    我们在Spring Boot maven项目的 单元测试包    test  下对应的类路径 编写自己的测试类:
    1. package cn.felord.mockspringboot;
    2.   
    3.    import cn.felord.mockspringboot.entity.Book;
    4.    import cn.felord.mockspringboot.service.BookService;
    5.    import org.assertj.core.api.Assertions;
    6.    import org.junit.Test;
    7.    import org.junit.runner.RunWith;
    8.    import org.mockito.BDDMockito;
    9.    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    10.    import org.springframework.boot.test.context.SpringBootTest;
    11.    import org.springframework.boot.test.mock.mockito.MockBean;
    12.    import org.springframework.test.context.junit4.SpringRunner;
    13.    import org.springframework.test.web.servlet.MockMvc;
    14.    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    15.    import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
    16.    import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
    17.   
    18.    import javax.annotation.Resource;
    19.    import java.time.LocalDate;
    20.   
    21.    /**
    22.     * The type Mock springboot application tests.
    23.     */
    24.    @RunWith(SpringRunner.class)
    25.    @SpringBootTest
    26.    @AutoConfigureMockMvc
    27.    public class MockSpringbootApplicationTests {
    28.        @Resource
    29.        private MockMvc mockMvc;
    30.        @MockBean
    31.        private BookService bookService;
    32.   
    33.        @Test
    34.        public void bookApiTest() throws Exception {
    35.            String title = "java learning";
    36.            // mockbean 开始模拟
    37.            bookServiceMockBean(title);
    38.            // mockbean 模拟完成
    39.            String expect = "{\"title\":\"java learning\",\"author\":\"dax\",\"price\":78.56,\"releaseTime\":\"2018-03-22\"}";
    40.            mockMvc.perform(MockMvcRequestBuilders.get("/book/get")
    41.                    .param("title", title))
    42.                    .andExpect(MockMvcResultMatchers.content()
    43.                            .json(expect))
    44.                    .andDo(MockMvcResultHandlers.print());
    45.            // mockbean 重置
    46.        }
    47.      
    48.        @Test
    49.        public void bookServiceTest() {
    50.   
    51.            String title = "java learning";
    52.            bookServiceMockBean(title);
    53.   
    54.   
    55.            Assertions.assertThat(bookService.queryByTitle("ss").getTitle()).isEqualTo(title);
    56.   
    57.        }
    58.          /**
    59.           * Mock打桩
    60.           * @param title the title
    61.           */   
    62.        private void bookServiceMockBean(String title) {
    63.   
    64.            Book book = new Book();
    65.            book.setAuthor("dax");
    66.            book.setPrice(78.56);
    67.            book.setReleaseTime(LocalDate.of(2018, 3, 22));
    68.            book.setTitle(title);
    69.   
    70.            BDDMockito.given(bookService.queryByTitle(title)).willReturn(book);
    71.        }
    72.     }
    复制代码
    测试类前两个注解不用说,第三个注解  @AutoConfigureMockMvc  可能你们很陌生。这个是用来开启Mock Mvc测试的自动化配置的。
      然后我们编写一个测试方法  bookApiTest()  来测试  BookApi#getBook(String title)  接口。
      逻辑是   MockMvc    执行一个模拟的get请求然后期望结果是  expect    Json字符串并且将相应对象打印了出来(下图1标识)。一旦请求不通过将抛出  java.lang.AssertionError   错误, 会把期望值(  Expected  )跟实际值打印出来(下图2标识)。如果跟预期相同只会出现下图。

    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启用集成测试。 对测试环境的搭建,测试代码的编写进行了实战操作,基本能满足日常开发测试需要,相信你能从本文学到不少东西。




    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-3-29 20:37 , Processed in 0.065307 second(s), 25 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表