单元测试的重要性对于项目来说实在太过重要了,是保障代码可重构和可持续交付的一个重要保证。

主流 Mock 工具对比

主流的Mock工具,主要有三类:

  • 动态代理:Mockito、EasyMock、MockRunner
  • 自定义类加载器:PowerMock
  • 运行时字节码修改:JMockit、TestableMock
工具 原理 最小Mock单元 对被Mock方法的限制 上手难度 IDE支持
Mockito 动态代理 不能Mock私有/静态和构造方法 较容易 很好
PowerMock 自定义类加载器 任何方法皆可 较复杂 较好
JMockit 运行时字节码修改 不能Mock构造方法(new操作符) 较复杂 一般
TestableMock 运行时字节码修改 方法 任何方法皆可 很容易 一般

动态代理只能在被测类的外周做手脚,不改动被测类本身,因此最安全,但功能也最弱。这类Mock工具对被Mock的方法比较挑剔,final类型、静态方法、私有方法全都无法覆盖。

自定义类加载器动态字节码修改都会修改被测类的字节码,前者完全接管测试类的加载过程,后者则是在类加载完成后再对字节码做“二次改造”。从功能而言,两者没有太大差异,都可以实现对几乎任何类型和方法的Mock。两者的主要差异在于机制的启用方式,为了让自定义类加载器生效,需要针对不同的测试框架进行有区分的特殊处理,譬如在JUnit中使用@RunWith注解。这一点体现在PowerMock上就表现为,与不同测试框架配合使用时,它的注解搭配是有明确区别的

TestableMock 简介

我们在写单元测试,定义Mock方法时,我们真正关心的只有一件事:“这个调用,在测试的时候要换成那个假的Mock方法”,特别是与网络交互、资源层上的方法。

然而当下主流的Mock框架在实现Mock功能时,需要我们操心的事情实在太多:Mock框架初始化、与所用的单元测试框架是否兼容、要被Mock的方法是不是私有的、是不是静态的、被Mock对象是new出来的还是注入的、怎样把被测对象送回被测类里等等这些非关键的额外工作极大分散了使用Mock工具应有的乐趣。

于是TestableMock 在 2020 年 12 月开始开源,出自阿里云云效团队,主要想解决在日常单元测试中经常遇到的痛点:

  • 外部依赖Mock繁琐
  • 私有方法难测试
  • 无返回值方法难测试
  • 复杂参数难构造

Java没有难测的方法。

使用 TestableMock

使用方式确实简单很多,文档也简洁,赞。

TestableMock 原理

TestableMock是基于动态字节码,为了与测试框架完全解耦,TestableMock通过直接扫描测试类中是否存在@MockMethod(或者@MockConstructor)修饰的方法,来自动判断是否要进行相应的初始化准备工作,实现了只需一个注解就能完成Mock初始化、定义和置换的极致体验。加之以可复用的方法(而非整个类型)作为粒度执行Mock替换,整个过程对测试的代码编写毫无侵入。

目前碰到一些问题

使用起来感觉还是很不错的,官方也一直在完善。

mockMethod 无效

这两种方式替换试试

@MockMethod(targetClass=JdbcTemplate.class)
public Object queryForObject(String sql, RowMapper<Bucket> rowMapper, Object... args)

@MockMethod
public Object queryForObject(JdbcTemplate self, String sql, RowMapper<Bucket> rowMapper, Object... args)

泛型问题

Mock方法需要和原方法声明一致,或者把泛型替换成 Object

//原方法
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)

@MockMethod
public Object queryForObject(JdbcTemplate self, String sql, RowMapper<Bucket> rowMapper, Object... args)

mock 接口

0.5.0版本,我在mock接口时候,出现失败。后面不得不使用Mockito方式了。

对应 issue:mock 接口失败

总结

优点是写单元测试会比较方便,代码量会少很多,毕竟只 mock 你想要 mock 的方法,不用把整个类都"初始化(笼统说法)"。

但缺点也很明显,单元测试不直观,并且在某些场景如(一个方法多次调用返回不同值,需借助TestableTool.MOCK_CONTEXT),比较有侵入性。