什么是 Mock 测试?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是 Mock 测试?相关的知识,希望对你有一定的参考价值。

参考技术A 作为动词,Mock 是模拟、模仿的意思。

作为名词,Mock 是能够模仿真实对象行为的模拟对象。

那么,在软件测试中,Mock 所模拟的对象是什么呢?

模拟的是 SUT(System Under Test:被测系统) 的依赖,而不是其本身。

比如,我要测试 A,但 A 依赖 B,要模拟的对象就是 B。

为什么要模拟 B 呢?

提高 A 的测试覆盖率: 通过 Mock 模拟 B 返回的正常和异常的结果,使用 A 的测试更充分。

避免 B 的因素对 A 产生影响: 当 B 因各种原因无法正常使用时,导致 A 无法测试。

提高 A 的测试效率: B 的真实行为可能很慢,但模拟可以很快。

Mock 的两大功能:

记录真实的调用信息

生成模拟的返回信息

使用 Mock 的问题是什么?

可能导致问题遗漏: 毕竟是模拟的,是理想可预见的情况,真实的情况可能更复杂。

可能导致维护成本变高: 接口变更 Mock 用例要跟着改,改错和漏改都可能出问题。

常见的 Mock 类型:

方法级别: Mock的对象是一个函数调用,例如:获取系统环境变量。

类级别: Mock 的对象是一个类,例如:一个 HTTP server。

接口级别: Mock 的对象是一个 API 接口。

服务级别: Mock 的对象是整个服务。

使用 Mock 做接口测试时,一般分二步:

1. 打桩: 创建 Mock 桩,指定 API 请求内容及其映射的响应内容。

2 . 调桩: 被测服务来请求 Mock 桩并接收 Mock 响应。

在这二步之间还有一个 Mock 桩的注入 , 啥是 Mock 注入?

Mock 的本质就是用模拟桩来替换真实的依赖。所谓 Mock 桩注入 就是 阻断被测服务与真实服务之间的链路,建立被测服务与 Mock 之间的链路过程。

如下图所示:

常见的方式包括但不限于以下五种:

API 请求构造

客户端 Mock:在被测服务 内部 工作,直接拦截被测服务的 API 请求方法,直接从方法内部返回预定义的 Mock 响应。

服务端 Mock:在被测服务 外部 工作,作为 HTTP 服务器接收被测服务发送的 API 请求,并返回预定义的 Mock 响应。

本地配置:

对于服务端 Mock,打桩之后会生成唯一的 Mock 桩地址,被测服务提供一个依赖服务地址配置项,在需要使用 Mock 时将依赖服务地址修改成 Mock 地址。

配置中心

对于服务端 Mock,为了避免修改依赖服务地址配置项导致被测服务重启,可以采用配置中心存储和管理依赖服务地址配置,或者使用注册中心记录服务与服务地址的映射关系。

反向代理

在微服务架构下,被测服务与依赖服务之间可能不是直连的,而是经过了一层反向代理,例如 API 网关。在这种情况下,被测服务是通过调用 API 网关来间接调用依赖服务的接口。

前向代理

服务端 Mock 除了作为 HTTP 服务器,还可以兼备 HTTP 代理的功能,这种架构又叫做 Mock 代理。

对比:

常用 Mock 工具:

单元测试级别:

easymock、jMock、Mockito、Unitils Mock、PowerMock、JMockit..

接口测试级别

Wiremock、Mockserver、Moco、Mock.js、RAP...

mock详细教程入门这一篇就够了(*)

前言

1、什么是mock测试
Mock 测试就是在测试活动中,对于某些不容易构造或者不容易获取的比较复杂的数据/场景,用一个虚拟的对象(Mock对象)来创建用于测试的测试方法。

2、为什么要进行Mock测试
Mock是为了解决不同的单元之间由于耦合而难于开发、测试的问题。所以,Mock既能出现在单元测试中,也会出现在集成测试、系统测试过程中。

Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

3、Mock适用场景
(1)需要将当前被测单元和其依赖模块独立开来,构造一个独立的测试环境,不关注被测单元的依赖对象,只关注被测单元的功能逻辑。

(2)被测单元依赖的模块尚未开发完成,而被测单元需要依赖模块的返回值进行后续处理。

(3)前后端项目中,后端接口开发完成之前,接口联调

(4) 依赖的上游项目的接口尚未开发完成,需要接口联调测试

(5)被测单元依赖的对象较难模拟或者构造比较复杂

如: 支付业务的异常条件很多,但是模拟这种异常条件很复杂或者无法模拟.

4、代码实例

1、新建测试工程

package com.echo.mockito;
 
public class demo 
 
    //新建一个测试方法
    public int add(int a,  int b)
        return a + b;
    

2、构建mock测试方法
(1)、选中测试类,右键选中generate

点击test

(2)、点击ok后就会在test目录下生成对应的测试方法,和真实的目录是对应的

5、参数方法说明

@BeforeEach 
    用在测试前准备,测试前会构建很多的环境配置或者基础配置,都可以在这里设置。
@AfterEach  
    用于测试后设置。
@Mock 
    注解可以理解为对 mock 方法的一个替代,不会走真实的方法,模拟真实方法的行为。使用该注解时,要使用MockitoAnnotations.openMocks(this)  方法,让注解生效。
@Spy
    1、被Spy的对象会走真实的方法,而mock对象不会,
    2、spy方法的参数是对象实例,mock的参数是class@InjectMocks 
       用于将@Mock标记的模拟变量注入到测试类中。
MockitoAnnotations.openMocks(this) 
   开启mock,配合以上两个注解进行测试。一般放在@BeforeEach 中,在测试前开启,这样不用在每个方法中都开启了。
Mockito.when(demo.add(1,2)).thenReturn(3):打桩
    mock核心,可以设置要测试的方法的结果,这样就会忽略真实方法的执行结果,后续的测试都是基于打桩结果执行。
Mockito.when(demo.add(1,2)).thenThrow(new RuntimeException());
    用于模拟异常。
Assertions.assertEquals(3,demo.add(1,2)):断言
    测试的主要方式,结果基于此判断。(期望值,实际值)

6、简单测试

1、打桩测试 设置方法返回4 ,而实际执行为3 ,测试不通过。

2、不打桩测试 因为是spy方式,会走真实的方法 ,测试通过。

3、如果是mock方式,如果不打桩会有默认值,测试会不通过,可以试一下。

7、测试方法说明

详细调用看代码,一般会以下几种:
1、打桩测试
2、异常测试
3、真实方法调用

package com.echo.mockito;
 
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
 
 
class demoTest 
 
     @Mock
     demo demo;
     //测试前开启mock
    @BeforeEach
    void setUp() 
        MockitoAnnotations.openMocks(this);
    
 
    @Test
    void add() 
        //mock 打桩,就是不管真实的方法如何执行,我们可以自行假设该方法执行的结果
        //后续的测试都是基于打桩结果来走
       // Mockito.when(demo.add(1,2)).thenReturn(4);
       // Assertions.assertEquals(3,demo.add(1,2));
        //当测试方法出现异常,测试方法  如果有trycatch 则可以测试异常是否正常
        //Mockito.when(demo.add(1,1)).thenThrow(new RuntimeException());
        //调用真实的方法
        Mockito.when(demo.add(1,1)).thenCallRealMethod();
        Assertions.assertEquals(2,demo.add(1,1));
    
 
    @AfterEach
    void after()
        System.out.println("测试结束");
    

8、Mock静态方法

1、之前的版本中是不允许模拟测试静态方法的,如果需要测试静态方法,需要替换新的mock依赖,注释掉目前有的依赖,会有冲突。
2、静态方法测试要用mock的MockedStatic类构建测试方法。

  <!--   <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.6.1</version>
        </dependency>
-->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.3.1</version>
            <scope>test</scope>
        </dependency>
package com.echo.mockito.Util;
 
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
 
import java.util.Arrays;
 
import static org.junit.jupiter.api.Assertions.*;
 
class StaticUtilsTest 
 
    @BeforeEach
    void setUp() 
    
 
    // 有参静态方法构建
    @Test
    void range() 
        MockedStatic   demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(()->StaticUtils.range(2,6)).thenReturn(Arrays.asList(10,11,12));
        Assertions.assertTrue(StaticUtils.range(2,6).contains(11));
    
    // 无参静态方法构建
    @Test
    void name() 
        MockedStatic   demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(StaticUtils::name).thenReturn("dhmw");
        Assertions.assertEquals("dhmw",StaticUtils.name());
 
    

问题:单个的方法执行是没有问题的,但是我们在类上全部执行的时候,发现报错。
提示static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered
意思就是说,每个方法需要有自己的static mock 对象,不允许公用。一起执行的时候,第一个方法占了对象,第二个方法就没有办法再占了。

解决:每个方法执行完毕后就直接关闭mock对象 demo.close()。相当于是单例的。用完后就的释放,下一个方法才能接着使用。

  @Test
    void range() 
        MockedStatic   demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(()->StaticUtils.range(2,6)).thenReturn(Arrays.asList(10,11,12));
        Assertions.assertTrue(StaticUtils.range(2,6).contains(11));
       //关闭
        demo.close();
    
    // 无参静态方法构建
    @Test
    void name() 
        MockedStatic   demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(StaticUtils::name).thenReturn("dhmw");
        Assertions.assertEquals("dhmw",StaticUtils.name());
        //关闭
        demo.close();
    

9、提升测试覆盖率

案例:数据统计系统,地推人员输入客户的姓名和手机号码,最后构建用户对象存入数据表。
1、业务代码如下:

package com.echo.mockito.service.impl;
 
import com.echo.mockito.dao.UserDao;
import com.echo.mockito.service.RegistrationService;
import com.echo.mockito.vo.User;
import org.springframework.beans.factory.annotation.Autowired;
 
import javax.xml.bind.ValidationException;
import java.sql.SQLException;
 
public class RegistrationServiceImpl implements RegistrationService 
     @Autowired
     UserDao userDao;
    @Override
    public User register(String name, String phone) throws Exception 
        if (name == null || name.length() == 0)
            throw new ValidationException("name 不能为空");
        
        if (phone == null || phone.length() ==0 )
            throw new ValidationException("phone 不能为空");
        
        User user;
        try 
              user = userDao.save(name,phone);
        catch (Exception e)
            throw  new Exception("SqlException thrown" + e.getMessage());
        
        return user;
    

package com.echo.mockito.dao;
 
import com.echo.mockito.vo.User;
 
public class UserDao 
 
    public User save(String name,String phnoe)
        User user = new User();
        user.setName(name);
        return user;
    

2、生成相应的测试代码,此时有几个问题需要思考。
1、测试的类是RegistrationServiceImpl但是里面的userDao如何注入到测试类中?

2、因为需要测试覆盖度,需要考虑该测试类中总共有几种情况需要测试。
(1):两个if 抛出了两个异常,总共是2种情况要测试。
(2):保存数据库分为正常保存和异常保存,总共2种情况测试。
综上所述,必须完成这四种情况的测试,才能覆盖所有代码。

3、相同的方法,我们生成测试用例.代码中有详细的说明,每一种情况都有测试用例。

package com.echo.mockito.service.impl;
 
import com.echo.mockito.dao.UserDao;
import com.echo.mockito.vo.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
 
import javax.xml.bind.ValidationException;
 
import java.sql.SQLException;
 
import static org.junit.jupiter.api.Assertions.*;
 
class RegistrationServiceImplTest 
 
  @InjectMocks     //RegistrationServiceImpl 实例中注入@Mock标记的类,此处是注入userDao
    @Spy
    private RegistrationServiceImpl registrationService;
    @Mock
    private  UserDao userDao;
    @BeforeEach
    void setUp() 
        MockitoAnnotations.openMocks(this);
    
 
    @Test
    void register() throws Exception 
        // ------------------  第一种 name  异常情况   测试   start ------------------------
        String name = null;
        String phone = "1234";
        try 
          registrationService.register(name,phone);
        catch (Exception e)
        Assertions.assertTrue(e instanceof ValidationException);
        
 
        // ------------------  name  异常情况   测试   end  ------------------------
 
        // ------------------  第二种 phone  异常情况   测试   start  ------------------------
        name = "111";
        phone = null;
      try 
        registrationService.register(name,phone);
      catch (Exception e)
        Assertions.assertTrue(e instanceof ValidationException);
      
        // ------------------  phone  异常情况   测试   start  ------------------------
 
 
        // ------------------  第三种 userDao.save   正常情况   测试   start  ------------------------
      name = "111";
      phone = "111";
        //正常保存测试  打桩  走真实的方法
        Mockito.when(userDao.save(name,phone)).thenCallRealMethod();
        User user =  registrationService.register(name,phone);
        Assertions.assertEquals("111",user.getName());
        // ------------------  userDao.save   正常情况   测试   end  ------------------------
 
        // ------------------   第四种 userDao.save   异常情况   测试   start  ------------------------
      //异常保存测试  打桩   通过thenThrow 抛出异常  测试异常是否被捕获
      Mockito.when(userDao.save(name,phone)).thenThrow(new RuntimeException());
      try 
         registrationService.register(name,phone);
      catch (Exception e)
        Assertions.assertTrue(e instanceof Exception);
      
// ------------------  userDao.save   异常情况   测试   end  ------------------------
    

如上所示:上面提到的第一个问题,如何注入测试类中的成员变量,是通过@InjectMocks 注解即可完成。

测试覆盖率方法如下:

测试结果如下:
1、右边部分会显示测试覆盖率。
2、真实代码绿色代表已覆盖测试,红色代表未覆盖测试。

如果所有的测试情况都100%覆盖,结果如下:

综上所述就是覆盖测试的方法,总结如下:
1、根据业务代码,分析出所有需要测试的情况。
2、根据不同的测试情况,编写具体的测试代码。
3、针对每一种情况,可以编写具体的测试代码,然后通过打桩,断言等方式,穷尽所有的清册情况即可。

问题:
1、如果真实代码 方法级别有 throws Exception 那么同样的,测试方法也必须方法级别要抛出异常,不然测试会报错。

  @Test
    void register() throws Exception 

2、保存数据库分为正常和异常,那么先测正常分支,然后在测试异常分支,如果顺序反了,测试先抛出异常,正常的分支就不会执行,这样会导致测试覆盖不全。

以上是关于什么是 Mock 测试?的主要内容,如果未能解决你的问题,请参考以下文章

简述 Mock 接口测试

mock详细教程入门这一篇就够了(*)

mock详细教程入门这一篇就够了(*)

mock详细教程入门这一篇就够了(*)

Mockito-部分mock测试使用

Mock工具介绍,为什么使用Mock?