Mock 测试技术

一、Mock 类框架的使用场景

在实际软件开发中,要进行测试的方法存在外部依赖(如 db,redis,第三方接口调用等),这些外部依赖可能存在各种问题,例如不稳定、缺乏数据、难以模拟等等,所以为了能够专注于对该方法(单元)的 逻辑 进行测试,就希望能虚拟出外部依赖,避免外部依赖成为测试的阻塞项,一般都是测试 service 层即可。

使用 Mock 就可以解决这些问题,Mock 框架他本是是不依赖于其他任何外部依赖的,使用 Mock 对象可以模拟外部依赖的行为和状态,所以相当于将测试方法和外部依赖隔离,能够更好的对单元方法的逻辑测试。

二、Mockito

1、Mockito 简介

Java 中主流的 Mock 框架

Mockito 支持情况:
限制: 老版本对于 final class、final method、staticmethod、private method 均不能被 mockitomock,目前已支持 final class、final method、staticmethod 的 mock
官网文档: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html


spring-boot-starter-test 已经集成了mockito,所以 SpringBoot 项目无需另外引入依赖。

mock 对象与 spy 对象

Mock 对象是一个完全模拟的对象,用于单元测试中替代真实对象。它允许开发者定义方法的预期行为,而不执行任何实际的业务逻辑。这种对象常用于隔离被测类的依赖,以确保测试的独立性和纯粹性。Mock 对象在方法调用时会返回预设的结果,帮助测试者验证方法调用的正确性和交互行为。

Spy 对象是一个部分模拟的对象,用于单元测试中模拟部分行为,同时保留对象的其他真实行为。Spy 对象在未定义行为的方法调用时,会执行真实对象的方法。它允许开发者在某些方法上插桩,而在其他方法上执行真实逻辑。

作用对象方法插桩方法不插桩备注
mock 对象执行插桩逻辑返回 mock 对象的默认值接口或实现类,用于创建完全虚拟的 mock 对象,无需实际实现
spy 对象执行插桩逻辑调用真实方法类,用于创建部分 mock、部分真实的 spy 对象,需要实际实例

插桩: 指定调用某个方法时的行为,一般是指调用某个方法时的返回值。

不插桩: 使用模拟对象的方法默认行为(mock 对象返回默认值,spy 对象调用真实方法)

2、初始化 mock 和 spy 对象的三种方式

  • junit4:@RunWith(MockitoJUnitRunner.class)+@Mock 等注解
  • junit5:@Extendwith(MockitoExtension.class)+@Mock 等注解
@ExtendWith(MockitoExtension.class)
public class InitMockito {
    @Mock
    private IUserService mockUserService;
    @Spy
    private IUserService spyUserService;

    @Test
    public void testMock1() {
        // 判断某对象是不是 mock 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
        // 判断某对象是不是 spy 对象 -- false
        Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
        // 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
        // 判断某对象是不是 spy 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
    }
}
  • Mockito.mock(X.class) 等静态方法
public class InitMockito2 {
    private IUserService mockUserService;

    private IUserService spyUserService;

    @BeforeEach
    public void init() {
        mockUserService = Mockito.mock(IUserService.class);
        spyUserService = Mockito.spy(IUserService.class);
    }

    @Test
    public void testMock1() {
        // 判断某对象是不是 mock 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
        // 判断某对象是不是 spy 对象 -- false
        Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
        // 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
        // 判断某对象是不是 spy 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
    }
}
  • MockitoAnnotations.openMocks(this)+@Mock 等注解
public class InitMockito3 {
    @Mock
    private IUserService mockUserService;
    @Spy
    private IUserService spyUserService;

    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testMock1() {
        // 判断某对象是不是 mock 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(mockUserService).isMock());
        // 判断某对象是不是 spy 对象 -- false
        Assertions.assertFalse(Mockito.mockingDetails(mockUserService).isSpy());
        // 判断某对象是不是 mock 对象 -- true -- spy 对象是另一种不同类型的 mock 对象
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isMock());
        // 判断某对象是不是 spy 对象 -- true
        Assertions.assertTrue(Mockito.mockingDetails(spyUserService).isSpy());
    }
}

3、参数匹配

在 Mockito 中,参数匹配器(ArgumentMatchers)用于指定在模拟对象的方法被调用时匹配某些参数的规则。这样可以灵活地定义方法行为,而不必精确匹配所有参数。Mockito 提供了多种参数匹配器,可以匹配各种类型和条件的参数。

测试参数匹配

@ExtendWith(MockitoExtension.class)
public class MockitoMatchParamsTest {
    @Mock
    private IUserService mockUserService;
    @Test
    public void test1() {
        // 方法插桩
        Mockito.when(mockUserService.login("lee","123")).thenReturn(new User());
        Mockito.doReturn(new User()).when(mockUserService).login("boy","456");
        // 调用方法
        mockUserService.login("lee","123");
        mockUserService.login("boy","456");
        // 验证 mock 方法是否按预期被调用次数
        Mockito.verify(mockUserService,Mockito.times(1)).login(ArgumentMatchers.eq("lee"),ArgumentMatchers.anyString());
        Mockito.verify(mockUserService,Mockito.times(1)).login(ArgumentMatchers.anyString(),ArgumentMatchers.eq("456"));
        Mockito.verify(mockUserService,Mockito.times(2)).login(ArgumentMatchers.anyString(),ArgumentMatchers.anyString());
    }
}

注意: Mockito 的参数匹配器需要与非匹配器参数分开使用,不能混合。

// 错误:混合使用匹配器和非匹配器参数         
when(myService.performAction(anyString(), "fixed value")).thenReturn("Mocked Result");

正确写法:

// 正确:所有参数都使用匹配器         
when(myService.performAction(anyString(), eq("fixed value"))).thenReturn("Mocked Result");
 // 正确:所有参数都不使用匹配器         
 when(myService.performAction("specific input", "fixed value")).thenReturn("Mocked Result");

4、方法插桩

插桩: 指定调用某个方法时的行为,一般是指调用某个方法时的返回值。

不插桩: 使用模拟对象的方法默认行为(mock 对象返回默认值,spy 对象调用真实方法)。
在 Mockito 中,mock 和 spy 对象在不进行方法插桩的情况下有不同的默认行为。下面是对它们默认值行为的详细说明。

下表,展示了在不插桩的情况下,mock 和 spy 对象的默认值情况:

对象类型Mock 对象默认值Spy 对象默认值
对象类型null调用真实对象的方法返回值
booleanfalse调用真实对象的方法返回值
int0调用真实对象的方法返回值
short0调用真实对象的方法返回值
byte0调用真实对象的方法返回值
long0L调用真实对象的方法返回值
float0.0f调用真实对象的方法返回值
double0.0d调用真实对象的方法返回值
集合类型空集合或数组调用真实对象的方法返回值
void 方法什么都不做调用真实对象的方法(有副作用)

(1)Mockito 提供了两种插桩方式

  1. when(obj.someMethod().thenXxx()–其中 obj 可以是 mock 对象,不适用无返回值的方法
  2. doXxx().when(obj).someMethod()–其中 obj 可以是 mock/spy 对象,也适用于没有返回值的方法 注意:spy 对象写在 when 中会先执行一次原方法,即使插桩也起不到 mock 的目的
@ExtendWith(MockitoExtension.class)
public class MockitoStubbingTest {
    @Mock
    private IUserService mockUserService;
    @Spy
    private UserServiceImpl spyUserService; // spy 作用于实现类

    @Test
    public void testStubbing() {
        // 1. mock 和 spy 对象都使用 when-then 进行插桩
        when(mockUserService.realMethod()).thenReturn("test-mockMethod!");
        when(spyUserService.realMethod()).thenReturn("test-spyMethod!");
        // 结果:spy 对象写在 when 中会先执行一次原方法,即使插桩也起不到 mock 的目的
        System.out.println(mockUserService.realMethod());
        System.out.println(spyUserService.realMethod());
    }
}

正确写法:

@Test
public  void testStubbing2() {
    // 1. mock 和 spy 对象都使用 when-then 进行插桩
    when(mockUserService.realMethod()).thenReturn("test-mockMethod!");
    doReturn("test-spyMethod!").when(spyUserService).realMethod();
    // 结果:spy 对象写在 when 中会先执行一次原方法,及时插桩也起不到 mock 的目的
    System.out.println(mockUserService.realMethod());
    System.out.println(spyUserService.realMethod());
}


(2)void 返回值方法插桩

使用 doNothing().when().yourMethod():

@Test
public void testStubbing3() {
    doNothing().when(mockUserService).modefyPwd(anyString(),anyString(),any(User.class));
    mockUserService.modefyPwd("123","456",new User());
}

(3)抛异常

两种方式:

  1. doThrow - when
  2. when - thenThrow
@Test
public void testThrowException() {
    // 方法一:使用 doThrow-when
    doThrow(RuntimeException.class).when(mockUserService).realMethod();
    try {
        mockUserService.realMethod();
        // 如果未抛出异常,这 mock 失败
        Assertions.fail();
    } catch (Exception e) {
        // 断言异常类型
        Assertions.assertTrue(e instanceof RuntimeException);
    }

    // 方法二:使用 when-throw
    when(mockUserService.login("boy","123")).thenThrow(RuntimeException.class);
    try {
        mockUserService.login("boy","123");
        // 如果未抛出异常,这 mock 失败
        Assertions.fail();
    } catch (Exception e) {
        // 断言异常类型
        Assertions.assertTrue(e instanceof RuntimeException);
    }
}

(4)多次插桩

    @Test
    public void multiStubbing() {
        // 第一次调用返回 1,第二次调用返回 2,第三次调用返回 3,第四次调用返回 3 ...
//        doReturn(1l).doReturn(2l).doReturn(3l).when(mockUserService).count();
//        doReturn(1l,2l,3l).when(mockUserService).count();
        
//        when(mockUserService.count()).thenReturn(1l).thenReturn(2l).thenReturn(3l);
        when(mockUserService.count()).thenReturn(1l,2l,3l);
        Assertions.assertEquals(1,mockUserService.count());
        Assertions.assertEquals(2,mockUserService.count());
        Assertions.assertEquals(3,mockUserService.count());
        Assertions.assertEquals(3,mockUserService.count());
    }

(5)thenAnswer 指定插桩逻辑

  1. 调用 thenAnswer 方法,重写 Answer
  2. 调用 doAnswer 方法,重写 Answer 使用 thenAnswer 时

注意: 泛型表示[插桩方法的返回值]

@Test
public void thenAnswerStubbing() {
    when(mockUserService.login(anyString(),anyString())).thenAnswer(new Answer<User>() {
        /**
         * 泛型表示[插桩方法的返回值]
         */
        @Override
        public User answer(InvocationOnMock invocationOnMock) throws Throwable {
            // getArgument 表示获取插桩方法的第几个参数值
            String name = invocationOnMock.getArgument(0, String.class);
            String pasw = invocationOnMock.getArgument(1, String.class);
            // 重组数据
            User user = new User();
            user.setUsername("用户名:"+name);
            user.setPassword("密码:"+pasw);
            return user;
        }
    });
    // 或者使用 doAnswer()
    
    // 调用方法
    User user = mockUserService.login("lee", "123");
    // 断言
    Assertions.assertEquals("用户名:lee",user.getUsername());
    Assertions.assertEquals("密码:123",user.getPassword());
}

(6)执行真正的原始方法

  1. 调用 thenCallRealMethod 方法
  2. 调用 doCallRealMethod 方法

注意: 要求此时 @Mock 注解标记的是 实现类。

    @Test
    public void testMockRealMethod() {
        when(mockUserService.realMethod()).thenCallRealMethod();
//        doCallRealMethod().when(mockUserService).realMethod();
        // 调用
        String res = mockUserService.realMethod();
        // 断言
        Assertions.assertEquals("service-realMethod!",res);
    }

(7)verify 的使用

verify 方法主要用于验证 mock 对象的方法是否按预期被调用。也可以验证方法的调用次数、顺序和参数。

@Test
public void testMockVerify() {
    // 默认返回 null (没有插桩)
    User user = mockUserService.login("lee", "123");
    Assertions.assertNull(user);

    // 验证调用过 1 次 login 方法,且参数是 "lee","123"
    verify(mockUserService).login("lee","123");
    // 等价于上面 vetify
    verify(mockUserService,times(1)).login("lee","123");

    // 校验没有调用的两种方式
    verify(mockUserService,times(0)).count();
    verify(mockUserService,never()).count();

    // 校验最少或最多调用了多少次
    verify(mockUserService,atLeast(1)).login("lee","123");
    verify(mockUserService,atMost(1)).login("lee","123");
}

5、@InjectMocks 注解使用

@InjectMocks 注解的主要作用是将使用 @Mock 或 @Spy 注解创建的 mock 对象或 spy 对象注入到被测试对象中,以便在测试中使用这些对象。它可以自动识别和匹配需要注入的字段,无论是通过构造函数、字段注入还是方法注入。

注意: 被 @InjectMocks 标注的属性必须是实现类,因为 mockito 会创建对应的实例对象,默认创建的对象就是未经过 mockito 处理的普通对象,因此常配合 @Spy 注解使其变为默认调用真实方法的 mock 对象。一般被测试的类,都需要标注这两个注解。

// 被测试的服务类
class MyService {
    private final MyDependency myDependency;

    // 构造函数注入依赖
    public MyService(MyDependency myDependency) {
        this.myDependency = myDependency;
    }

    public String performAction() {
        return myDependency.someMethod();
    }
}

// 依赖类
class MyDependency {
    public String someMethod() {
        return "Real Response";
    }
}

public class MyServiceTest {

    @Mock
    private MyDependency myDependency; // 创建 mock 对象

    @InjectMocks
    @Spy
    private MyService myService; // 自动注入 mock 对象

    @Before
    public void setUp() {
        // 初始化注解标注的对象
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testPerformAction() {
        // 设置 mock 对象的行为
        when(myDependency.someMethod()).thenReturn("Mocked Response");

        // 调用被测试对象的方法
        String result = myService.performAction();

        // 验证结果
        assertEquals("Mocked Response", result);
    }
}

三、实际编写单元测试注意事项

  1. 在实际测试中,mock 不全,可能会出现空指针情况,这个时候不要慌,先看报错,再根据报错将为空的对象 mock 到测试类中。
  2. 实际代码中,每个 service 接口方法都可能有多个 return,此时要想保证代码覆盖率,就需要为每个方法构造合适插桩,多写几个 Test 测试,确保覆盖到每个 return。

四、Mockito 在 SpringBoot 环境使用(不推荐,慢)

  1. @MockBean:类似 @Mock 用于通过类型或名字替换 spring 容器中已经存在的 bean, 从而达到对这些 bean 进行 mock 的目的。
  2. @SpyBean:作用类似 @Spy 用于通过类型或名字包装 spring 容器中已经存在的 bean,当需要 mock 被测试类的某些方法时可以使用。
  3. Spring 中生成的对象受 spring 管理,上述 Bean 被 mock 或 spy 后,依然受 Spring 容器管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/787236.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++中的多重继承和虚继承:横向继承、纵向继承和联合继承;虚继承

多重继承 A.横向多重继承&#xff1a; B.纵向多重继承&#xff1a; C.联合多重继承&#xff1a; 因为 single 和 waiter 都继承了一个 worker 组件&#xff0c;因此 SingingWaiter 将包含两个 worker 组件&#xff0c;那么将派生类对象的地址赋给基类指针将出现二义性 那么如何…

使用F1C200S从零制作掌机之debian文件系统完善NES

一、模拟器源码 源码&#xff1a;https://files.cnblogs.com/files/twzy/arm-NES-linux-master.zip 二、文件系统 文件系统&#xff1a;debian bullseye 使用builtroot2018构建的文件系统&#xff0c;使用InfoNES模拟器存在bug&#xff0c;搞不定&#xff0c;所以放弃&…

Git 快速上手

这个文档适用于需要快速上手 Git 的用户&#xff0c;本文尽可能的做到简单易懂 ❤️❤️❤️ git 的详细讲解请看这篇博客 Git 详解&#xff08;原理、使用&#xff09; 1. 什么是 Git Git 是目前最主流的一个版本控制器&#xff0c;并且是分布式版本控制系统&#xff0c;可…

k8s中port,targetPort,nodePort,containerPort的区别

一、说明 在 Kubernetes 中&#xff0c;port、targetPort、nodePort 和 containerPort 是用于定义服务&#xff08;Service&#xff09;和容器之间网络通信的不同参数。 它们各自的作用和含义如下&#xff1a; 1. port 定义&#xff1a;这是服务对外暴露的端口号。作用&#x…

树莓派_Pytorch学习笔记20:初步认识深度学习框架

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: ​ Python 版本3.7.3&#xff1a; ​ 本文很水&#xff0c;就介绍一下我以后的学习使用P…

【JavaEE】 简单认识CPU

&#x1f435;本篇文章将对cpu的相关知识进行讲解 一、认识CPU 下图是简略的冯诺依曼体系结构图 上图中&#xff0c;存储器用来存储数据&#xff0c;注意在存储器中都是以二进制的形式存储数据的&#xff0c;CPU就是中央处理器&#xff0c;其功能主要是进行各种算术运算和各种…

C++·模板进阶

1. 非类型模板参数 之前我们写的模板参数都设定class类型的&#xff0c;这个模板参数用来给下面的代码中的某些元素定义类型&#xff0c;我们管这种模板参数叫类型形参。非类型模板参数就是用一个常量作为模板的一个参数&#xff0c;在模板中可将该参数当作常量来使用&#xff…

tk Text文本框赋值,清空

import tkinter as tk# 创建主窗口 root tk.Tk() root.title("文本框内容赋值示例")# 创建一个Text小部件 text_area tk.Text(root, height10, width50) text_area.pack()# 将内容赋值给Text小部件 text_area.insert(tk.END, "这是文本框中的内容。\n")#…

STL--栈(stack)

stack 栈是一种只在一端(栈顶)进行数据插入(入栈)和删除(出栈)的数据结构,它满足后进先出(LIFO)的特性。 使用push(入栈)将数据放入stack,使用pop(出栈)将元素从容器中移除。 使用stack,必须包含头文件: #include<stack>在头文件中,class stack定义如下: namespace std…

前端面试题33(实时消息传输)

前端实时传输协议主要用于实现实时数据交换&#xff0c;特别是在Web应用中&#xff0c;它们让开发者能够构建具有实时功能的应用&#xff0c;如聊天、在线协作、游戏等。以下是几种常见的前端实时传输协议的讲解&#xff1a; 1. Short Polling (短轮询) 原理&#xff1a;客户…

k8s record 20240705

k8s 安全管理 request 是1g&#xff0c;你得不到要求&#xff0c;我就不创建了&#xff0c;这就是准入控制二次校验 SA就是serviceAccount。 内部是SA和 token, 外部用户进来就是 .kube/config文件 namespace下的是role&#xff0c;整个集群是 ClusterRole. 动作就是Binding li…

一文带你彻底搞懂什么是责任链模式!!

文章目录 什么是责任链模式&#xff1f;详细示例SpingMVC 中的责任链模式使用总结 什么是责任链模式&#xff1f; 在我们日常生活中&#xff0c;经常会出现一种场景&#xff1a;一个请求需要经过多个对象的处理才能得到最终的结果。比如&#xff0c;一个请假申请&#xff0c;需…

集训 Day 2 模拟赛总结

复盘 7&#xff1a;30 开题 想到几天前被普及组难度模拟赛支配的恐惧&#xff0c;下意识觉得题目很难 先看 T1&#xff0c;好像不是很难&#xff0c;魔改 Kruskal 应该就行 看 T2 &#xff0c;感觉很神奇&#xff0c;看到多串匹配想到 AC 自动机&#xff0c;又想了想 NOIP …

【开源】基于RMBG的一键抠图与证件照制作系统【含一键启动包】

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

优秀策划人必逛的地方,你不会还不知道吧?

道叔今天依然记得当初刚入行的时候&#xff0c;每天为完成策划任务&#xff0c;焦虑的整晚睡不着觉的痛苦。 但其实……很多时候&#xff0c;选择比努力更重要 优秀的策划和文案&#xff0c;也从来不是天生&#xff0c;你要走的路&#xff0c;前人都已经走过,你要做的仅仅是整…

【计算几何】凸包问题 (Convex Hull)

【计算几何】凸包问题 (Convex Hull) 引言 凸多边形 凸多边形是指所有内角大小都在 [ 0 , π ] [0,π] [0,π]范围内的简单多边形 凸包 在平面上能包含所有给定点的最小凸多边形叫做凸包。 其定义为&#xff1a;对于给定集合 X&#xff0c;所有包含 X 的凸集的交集 S 被称…

QT文件生成可执行的exe程序

将qt项目生成可执行的exe程序可按照以下步骤进行&#xff1a; 1、在qt中构建运行生成.exe文件&#xff1b; 2、从自定义的路径中取出exe文件放在一个单独的空文件夹中&#xff08;exe文件在该文件夹中的release文件夹中&#xff09;&#xff1b; 3、从开始程序中搜索qt&#xf…

Python入门 2024/7/8

目录 数据容器 dict(字典&#xff0c;映射) 语法 定义字典字面量 定义字典变量 定义空字典 从字典中基于key获取value 字典的嵌套 字典的常用操作 新增元素 更新元素 删除元素 清空字典 获取全部的key 遍历字典 统计字典内的元素数量 练习 数据容器的通用操作…

运维锅总详解设计模式

本首先简介23种设计模式&#xff0c;然后用Go语言实现这23种设计模式进行举例分析。希望对您理解这些设计模式有所帮助&#xff01; 一、设计模式简介 设计模式是软件设计中用于解决常见设计问题的一套最佳实践。它们不是代码片段&#xff0c;而是解决特定问题的通用方案。设…

(图文详解)小程序AppID申请以及在Hbuilderx中运行

今天小编给大家带来了如何去申请APPID&#xff0c;如果你是小程序的开发者&#xff0c;就必须要这个id。 申请步骤 到小程序注册页面&#xff0c;注册一个小程序账号 微信公众平台 填完信息后提交注册 会在邮箱收到 链接激活账号 确认。邮箱打开链接后&#xff0c;会输入实…