Exploring
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
  • Java

    • 基础

    • 并发与多线程

    • 日志系统

    • 单元测试

      • Mockito 原理
        • 介绍
          • 单元测试步骤
        • 原理介绍
          • 代理
          • 打桩
          • 源码演示
          • 静态方法 mock
        • 动手实现
          • 实现 stub
          • 测试
    • JVM

    • Spring

    • SpringBoot

    • 一些工具

  • 语言
  • Java
  • 单元测试
unclezs
2022-07-29
0
目录

Mockito 原理

# 介绍

Mockito 也是 Java 单元测试的老大哥了,在 2.x 也支持了 mockStatic,之前都是配合 PowerMock 进行 mockstatic 使用的。我们开发中也离不开单元测试,所以理解单元测试的原理更加便于我们写好 case 。

# 单元测试步骤

  • 设定目标
  • 设置消费条件
  • 预期返回结果
  • 消费并检验返回结果

# 原理介绍

# 代理

我们写 Java 应该都是知道代理模式的,有 JDK 动态代理与 Cglib 代理等,其实 Mock 也是基于代理实现的,在我们调用 mock(xxxx) 的时候就是对这个对象创建了一个代理,拦截每个方法,保存方法的入参,返回值等信息。在我们真正调用时,就是通过代理对象进行操作,方法都被代理了那方法想返回什么东西也是可以随意指定了。

# 打桩

打桩就是标识在哪个位置进行代理,比如我们经常写的

when(xxxMock.xxMethod()).thenReturn(xxx)
1

这其实就是打桩,在调用这个方法的时候,其实就是调用了我们的代理方法,所以我们知道入参信息,所以就可以把方法名、方法入参记录下来,生成一个匹配器(桩),如果下次调用的时候匹配了,就满足这个桩点,执行桩点设置的返回值(或者其他 answer)。

# 源码演示

# 调用 obj.get("a") 的时候返回 "b"
when(obj.get(eq("a"))).thenReturn("b")
1
2

当我们调用 when 时,内部会调用

# 记录开始打桩的位置
mockingProgress.stubbingStarted();
1
2

当我们调用 eq 等匹配逻辑的时候会记录参数

argumentMatcherStorage.reportMatcher(matcher)
1

然后当我们 then 的时候内部会调用

# 保存对应匹配模式的对应的应答方式然后标记完成
mockingProgress.stubbingCompleted(invocation);
1
2

然后当我们真正去调用方法的时候

obj.get("a")
1

就会去通过当前调用的方法,是否打桩过,并且存在匹配器能够匹配上,最终得到匹配器的应答器,得到返回值,直接返回。

# org.mockito.internal.handler.MockHandlerImpl#handle

InvocationMatcher invocationMatcher = matchersBinder.bindMatchers(mockingProgress.getArgumentMatcherStorage(),invocation);
invocationContainerImpl.findAnswerFor(invocation)
1
2
3
4

# 静态方法 mock

这个在 mockito 1.x 时是不支持的,还需要利用 powermock 实现,在 mockito 2.x 时,就支持了 mockStatic 了。那么静态方法我们改怎么 mock 呢。

PowerMock 的实现原理是通过 @PrepareForTest 注解指定出哪些是需要 mockStatic 的,然后在启动时通过一个 classLoader 加载这些类,并且通过动态修改字节码的方式进行 mock。

Mockito 实现原理也是字节码增强。

# 动手实现

Mock的实现关键是,实现动态代理,被 mock 的对象只是“假装”调用了该方法,然后返回假的值。

可以使用cglib来进行动态代理。通过class对象创建该对象的动态代理对象,然后设置该对象的父类与回调即可。并在回调函数中定义拦截器,实现自定义逻辑。

public class Mockito {

    /**
     * 根据class对象创建该对象的代理对象 
     * 1、设置父类;2、设置回调 
     * 本质:动态创建了一个class对象的子类 
     *
     * @return
     */
    public static <T> T mock(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MockInterceptor());
        return (T)enhancer.create();
    }

    private static class MockInterceptor implements MethodInterceptor {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            return null;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 实现 stub

首先定义一个类,来表示对函数的调用,重写equals()方法,通过函数名 + 参数列表来判断调用是否相同。

public class Invocation {
    private final Object mock;
    private final Method method;
    private final Object[] arguments;
    private final MethodProxy proxy;

    public Invocation(Object mock, Method method, Object[] args, MethodProxy proxy) {
        this.mock = mock;
        this.method = method;
        this.arguments = copyArgs(args);
        this.proxy = proxy;
    }

    private Object[] copyArgs(Object[] args) {
        Object[] newArgs = new Object[args.length];
        System.arraycopy(args, 0, newArgs, 0, args.length);
        return newArgs;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !obj.getClass().equals(this.getClass())) { return false; }
        Invocation other = (Invocation)obj;
        return this.method.equals(other.method) && this.proxy.equals((other).proxy)
            && Arrays.deepEquals(arguments, other.arguments);
    }

    @Override
    public int hashCode() {
        return 1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

接下来,在 MockInterceptor 类中,需要做两个操作。

  1. 为了设置方法的返回值,需要存放对方法的引用(Invocation)
  2. 调用方法时,检查是否已经设置了该方法的返回值(results)。如果设置了,则返回该值。
public class Mockito {

    private static Map<Invocation, Object> results = new HashMap<Invocation, Object>();
    private static Invocation lastInvocation;

    public static <T> T mock(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new MockInterceptor());
        return (T)enhancer.create();
    }

    private static class MockInterceptor implements MethodInterceptor {
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            Invocation invocation = new Invocation(proxy, method, args, proxy);
            lastInvocation = invocation;
            if (results.containsKey(invocation)) {
                return results.get(invocation);
            }
            return null;
        }
    }

    public static <T> When<T> when(T o) {
        return new When<T>();
    }

    public static class When<T> {
        public void thenReturn(T retObj) {
            results.put(lastInvocation, retObj);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 测试

@Test
public void test() {
    Calculate calculate = mock(Calculate.class);
    when(calculate.add(1, 1)).thenReturn(1);
    Assert.assertEquals(1, calculate.add(1, 1));
}
1
2
3
4
5
6
在 GitHub 编辑此页 (opens new window)
上次更新: 2024/02/25, 12:11:11
好看的彩色日志输出
Class类字节码详解

← 好看的彩色日志输出 Class类字节码详解→

Theme by Vdoing | Copyright © 2018-2024 unclezs
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式