重学SpringBoot系列之Mockito测试

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学SpringBoot系列之Mockito测试相关的知识,希望对你有一定的参考价值。


mock中文文档

https://github.com/hehonghui/mockito-doc-zh#0

csdn


使用Mockito编码完成接口测试

编码实现接口测试

为什么要写代码做测试?使用接口测试工具Postman很方便啊

因为在做系统的自动化持续集成的时候,会要求自动的做单元测试,只有所有的单元测试都跑通了,才能打包构建。比如:使用maven在打包之前将所有的测试用例执行一遍。这里重点是自动化,所以postman这种工具很难插入到持续集成的自动化流程中去。


junit测试框架

在开始书写测试代码之前,我们先回顾一下JUnit常用的测试注解。在junit4和junit5中,注解的写法有些许变化。


Mockito测试框架

Mockito是GitHub上使用最广泛的Mock框架,并与JUnit结合使用.Mockito框架可以创建和配置mock对象.使用Mockito简化了具有外部依赖的类的测试开发。Mockito测试框架可以帮助我们模拟HTTP请求,从而达到在服务端测试目的。因为其不会真的去发送HTTP请求,而是模拟HTTP请求内容,从而节省了HTTP请求的网络传输,测试速度更快。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
   <exclusions>
      <exclusion>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
   </exclusions>
</dependency>

spring-boot-starter-test(Spring Boot 2.3.0.RELEASE)自动包含Junit 5 和Mockito框架,以下测试代码是基于Junit5,使用Junit4的同学请自行调整代码

@Slf4j
public class ArticleRestControllerTest 

    //mock对象
    private static MockMvc mockMvc;

    //在所有测试方法执行之前进行mock对象初始化
    @BeforeAll
    static void setUp() 
        mockMvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
    

    //测试方法
    @Test
    public void saveArticle() throws Exception 



        String article = "\\n" +
                "        \\"name\\": \\"大忽悠\\",\\n" +
                "        \\"age\\": 18,\\n" +
                "        \\"friend\\": \\n" +
                "            \\"name\\": \\"xpy\\",\\n" +
                "            \\"createTime\\": \\"2021-5-21 00:00:00\\"\\n" +
                "        \\n" +
                "    ";



        //测试返回的结果
        MvcResult result =
                //测试执行
                mockMvc.perform(
                        //请求对象的构建
            MockMvcRequestBuilders
                    //请求的方式和请求路径
                .request(HttpMethod.POST, "/rest/selfs")
                    //请求类型
                    .contentType("application/json")
                //内容
                    .content(article)
        )
         //期望的返回结果---断言
        .andExpect(MockMvcResultMatchers.status().isOk())  //HTTP:status 200
        //可以取出返回结果中的值和期望值进行比较
        .andExpect(MockMvcResultMatchers.jsonPath("$.data.name").value("大忽悠"))
        .andExpect(MockMvcResultMatchers.jsonPath("$.data.age").value(18))
        //打印结果
        .andDo(MockMvcResultHandlers.print())
        //返回结果
        .andReturn();
        //设置响应的字符编码格式
        result.getResponse().setCharacterEncoding("UTF-8");
       //日志打印响应的数据结果
        log.info("============================================");
        log.info(result.getResponse().getContentAsString());

    

MockMvc对象有以下几个基本的方法:

  • perform : 模拟执行一个RequestBuilder构建的HTTP请求,会执行SpringMVC的流程并映射到相应的控制器Controller执行。
  • contentType:发送请求内容的序列化的格式,"application/json"表示JSON数据格式
  • andExpect:添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确,或者说是结果是否与我们期望(Expect)的一致。
  • andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
  • andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理
上面的整个过程,我们都没有使用到Spring Context依赖注入、也没有启动tomcat web容器。整个测试的过程十分的轻量级,速度很快。

真实servlet容器环境下的测试

上面的测试执行速度非常快,但是有一个问题:它没有启动servlet容器和Spring 上下文,自然也就无法实现依赖注入(不支持@Resource@AutoWired注解)。这就导致它在从控制层到持久层全流程测试中有很大的局限性。


换一种写法:看看有没有什么区别。在测试类上面额外加上这样两个注解,并且mockMvc对象使用@Resource自动注入,删掉Before注解及setUp函数


@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(SpringExtension.class)


启动测试一下,看看和之前有没有什么区别.


看到上面这个截图,是不是已经明白了!该测试方法真实的启动了一个tomcat容器、以及Spring 上下文,所以我们可以进行依赖注入(@Resource)。实现的效果和使用MockMvcBuilders构建MockMVC对象的效果是一样的,但是有一个非常明显的缺点:每次做一个接口测试,都会真实的启动一次servlet容器,Spring上下文加载项目里面定义的所有的Bean,导致执行过程很缓慢。


@SpringBootTest 注解

是用来创建Spring的上下文ApplicationContext,保证测试在上下文环境里运行。单独使用@SpringBootTest不会启动servlet容器。所以只是使用SpringBootTest 注解,不可以使用@Resource@Autowired等注解进行bean的依赖注入。(准确的说是可以使用,但被注解的beannull)。

//指明启动类
//随机端口启动
@SpringBootTest
(classes = Application.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class test

    //注入启动的端口
    @LocalServerPort
    private Integer port;
......


@ExtendWith(@RunWith注解)

  • RunWith方法为我们构造了一个的Servlet容器运行运行环境,并在此环境下测试。然而为什么要构建servlet容器?因为使用了依赖注入,注入了MockMvc对象,而在上一个例子里面是我们自己new的。
  • 而@AutoConfigureMockMvc注解,该注解表示mockMvc对象由spring 依赖注入构建,你只负责使用就可以了。这种写法是为了让测试在servlet容器环境下执行。

简单的说:如果你单元测试代码使用了“依赖注入@Resource”就必须加上@ExtendWith,如果你不是手动new MockMvc对象就加上@AutoConfigureMockMv

实际上@SpringBootTest 注解注解已经包含了 @ExtendWith注解,如果使用了前者,可以忽略后者!


@Transactional

该注解加在方法上可以使单元测试进行事务回滚,以保证数据库表中没有因测试造成的垃圾数据,因此保证单元测试可以反复执行;
但是笔者不建议这么做,使用该注解会破坏测试真实性。

核心问题:

在单元测试时,测试类中 @Transactional 注解,会导致测试中 Entity 数据的操作都是在内存中完成,最终并不会进行 commit 操作,也就是不会将 Entity 数据进行持久化操作,从而导致测试的行为和真实应用的行为不一致。

详解

事务管理在应用开发中是种不可或缺的设计,它是数据库持久化处理的一种标准。我们知道,应用程序开发离不开对数据的CRUD(增删改查),事务的ACID性可以更好保证数据的完整性,保证相关数据的同生共死。单个事务生命周期主要分为三个阶段,BEGIN TRANSACTION -> COMMIT TRANSACTION -> ROLLBACK TRANSACTION。

Spring Boot事务的使用分为命令式和声明式常用的方式是声明式注解(@Transactional)。事务管理既可以在应用层使用,也可以在测试中使用。

为了保证测试之间的相互独立,测试之间数据不会被相互影响。也许你写过这样的测试:

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class UserControllerTest  

@Transactional 通过将数据持久化操作截断,来解决测试之间相互独立,数据相互不影响的问题。然而这样方式会有副作用,就是数据持久化的过程不再真实,没有了commit的过程。从而会导致:

  • 无法保证 Entity 之间关联关系,唯一索引和主外键关联的准确性
  • 无法保证 Entity 创建时间、更新时间和版本化(乐观锁)的赋值逻辑的准确性
  • 无法保证 Entity 中有 @Transient 注解的属性的赋值逻辑的准确性
  • 测试的数据不是真实场景存在的问题
  • 测试中,单个事务中的准备数据,无法在多线程中共享。

Mock测试

什么是Mock?

在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象。比如:对象B依赖于对象A,但是A代码还没写是一个空类空方法不能用,我们来mock一个假的A来完成测试。

为什么要使用Mock?

在单元测试中,模拟对象可以模拟复杂的、真实的对象的行为, 如果真实的对象无法放入单元测试中,使用模拟对象就很有帮助。

在下面的情形,可能需要使用 “模拟对象行为” 来代替真实对象:

  • 真实对象的行为是不确定的(例如,当前的时间或当前的温度);
  • 真实对象很难搭建起来;
  • 真实对象的行为很难触发(例如,网络错误);
  • 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
  • 真实的对象是用户界面,或包括用户界面在内;
  • 真实的对象使用了回调机制;
  • 真实对象可能还不存在(例如,其他程序员还为完成工作);
  • 真实对象可能包含不能用作测试的信息(高度保密信息等)和方法。

场景实践

  @PostMapping("/selfs")
  public AjaxResponse saveArticle(@RequestBody Self self)

    //因为使用了lombok的Slf4j注解,这里可以直接使用log变量打印日志
    log.info("==========================");
    log.info("saveArticle:" + self);
    return AjaxResponse.success(selfService.save(self));
  

但是因为种种原因,这个接口目前没能实现(只有接口,代码如下)。比如说:另一个程序员暂时没完成工作,或者是机密内容实现,不能被用于测试环境。

但是现在接口调用方找到我了,需要进行接口验证。怎么办?我们就可以使用Mock的方法,先Mock一个假的SelfService,把接口验证完成。

@AutoConfigureMockMvc
@SpringBootTest
//@ExtendWith(SpringExtension.class)
@Slf4j
public class ArticleRestControllerTest 

    //mock对象
    @Resource
    private  MockMvc mockMvc;

 @MockBean
 private SelfService selfService;

    //测试方法
    @Test
    public void saveArticle() throws Exception 



        String self = "\\n" +
                "        \\"name\\": \\"大忽悠\\",\\n" +
                "        \\"age\\": 18,\\n" +
                "        \\"friend\\": \\n" +
                "            \\"name\\": \\"xpy\\",\\n" +
                "            \\"createTime\\": \\"2021-5-21 00:00:00\\"\\n" +
                "        \\n" +
                "    ";
        ObjectMapper objectMapper=new ObjectMapper();
        Self self1 = objectMapper.readValue(self, Self.class);
        //打桩
        when(selfService.save(self1)).thenReturn("ok");

        //测试返回的结果
        MvcResult result =
                //测试执行
                mockMvc.perform(
                        //请求对象的构建
            MockMvcRequestBuilders
                    //请求的方式和请求路径
                .request(HttpMethod.POST, "/rest/selfs")
                    //请求类型
                    .contentType("application/json")
                   //内容
                    .content(self)
        )
         //期望的返回结果---断言
        .andExpect(MockMvcResultMatchers.status().isOk())  //HTTP:status 200
        //可以取出返回结果中的值和期望值进行比较
        .andExpect(MockMvcResultMatchers.jsonPath("$.data").value("ok"))
        //打印结果
        .andDo(MockMvcResultHandlers.print())
        //返回结果
        .andReturn();
        //设置响应的字符编码格式
        result.getResponse().setCharacterEncoding("UTF-8");
       //日志打印响应的数据结果
        log.info("============================================");
        log.info(result.getResponse().getContentAsString());

    

@MockBean

可以用MockBean伪造模拟一个Service ,如上图中的MockBean。

大家注意上文代码中,打了一个桩

when(articleService.saveArticle(articleObj)).thenReturn("ok")

也就是告诉测试用例程序,当你调用articleService.saveArticle(articleObj)方法的时候,不要去真的调用这个方法,直接返回一个结果(“ok”)就好了。

 .andExpect(MockMvcResultMatchers.jsonPath("$.data").value("ok"))

测试用例跑通了,期望结果andExpect:ok与实际结果thenReturn(“ok”)一致。表示程序真正的去执行了MockBean的模拟行为,而不是调用真实对象的方法。


轻量级测试

在ExtendWith的AutoConfigureMockMvc注解的共同作用下,启动了SpringMVC的运行容器,并且把项目中所有的@Bean全部都注入进来。把所有的bean都注入进来是不是很臃肿?这样会拖慢单元测试的效率。如果我只是想测试一下控制层Controller,怎么办?或者说我只想具体到测试一下ArticleRestController,怎么办?要把应用中所有的bean都注入么?有没有轻量级的解决方案?一定是有的

@ExtendWith(SpringExtension.class)
@WebMvcTest(ArticleController.class)
//@SpringBootTest

使用@WebMvcTest替换@SpringBootTest

  • @SpringBootTest注解告诉SpringBoot去寻找一个主配置类(例如带有@SpringBootApplication的配置类),并使用它来启动Spring应用程序上下文。SpringBootTest加载完整的应用程序并注入所有可能的bean,因此速度会很慢
  • @WebMvcTest注解主要用于controller层测试,只覆盖应用程序的controller层,@WebMvcTest(ArticleController.class)只加载ArticleController这一个Bean用作测试。所以WebMvcTest要快得多,因为我们只加载了应用程序的一小部分。

MockMvc更多的用法总结

//模拟GET请求:
mockMvc.perform(MockMvcRequestBuilders.get("/user/id", userId));

//模拟Post请求:
mockMvc.perform(MockMvcRequestBuilders.post("uri", parameters));

//模拟文件上传:
mockMvc.perform(MockMvcRequestBuilders.multipart("uri").file("fileName", "file".getBytes("UTF-8")));


//模拟session和cookie:
mockMvc.perform(MockMvcRequestBuilders.get("uri").sessionAttr("name", "value"));
mockMvc.perform(MockMvcRequestBuilders.get("uri").cookie(new Cookie("name", "value")));

//设置HTTP Header:
mockMvc.perform(MockMvcRequestBuilders
                        .get("uri", parameters)
                        .contentType("application/x-www-form-urlencoded")
                        .accept("application/json")
                        .header("", ""));

以上是关于重学SpringBoot系列之Mockito测试的主要内容,如果未能解决你的问题,请参考以下文章

重学springboot系列之集群多节点应用session共享,redis分布式锁

重学Springboot系列之服务器推送技术

重学Springboot系列之整合数据库开发框架---上

重学SpringBoot系列之整合静态资源与模板引擎

重学SpringBoot系列之嵌入式容器的配置与应用

重学Springboot系列之整合数据库开发框架---下