背景
上一篇讲了 mock
的基本原理,这一篇简单谈谈 spy
。
可以这么理解这两者之间的区别:
mock
完全就是为了欺骗编译器。你mock
的对象完全就是一个空壳,返回的结果要么就是类型默认值(比如boolean
类型结果就返回false
,int
类型就返回0
),要么就是你自己指定。总之,这个 mock 对象不会真正执行它的方法,也没有相应的内部状态;spy
则获取一个“真实”的对象,也就是说,完全可以拿来当一个正常的对象来用,有内部状态,也返回正确结果。唯一的不同是,你也可以指定让它返回你想要的结果;
spy
的一个典型用法就是,你想测试某个对象 obj
的某个方法 A,但 A 会调用它的另一个方法 B,而你想把 B mock 掉。这时候就可以基于 obj
创建一个 spiedObj
,然后把它的 B 方法 mock 掉就可以了。
(注:本文基于 Mockito 4.6.1 源码)
spy 和 mock 在实现上的不同
不同点 1:默认返回值
可以看到,在创建 spy
对象时,Mockito 会指定 CALLS_REAL_METHODS
作为 defaultAnswer
:
// java/org/mockito/Mockito.java
// 第 2063 行
public static <T> T spy(T object) {
return MOCKITO_CORE.mock(
(Class<T>) object.getClass(),
// 注意这里
withSettings().spiedInstance(object).defaultAnswer(CALLS_REAL_METHODS));
}
这里的 CALLS_REAL_METHODS
并非一个字符串,而是某个对象。总之就是,根据这个配置,在对 mock 对象进行方法调用时,默认会执行真正的方法,而不是返回 mock 结果。
对比一下,创建 mock
对象时,会设置 RETURNS_DEFAULTS
作为 defaultAnswer
:
// java/org/mockito/Mockito.java
// 第 1895 行
public static <T> T mock(Class<T> classToMock) {
return mock(classToMock, withSettings());
}
// ...
// 第 3251 行
public static MockSettings withSettings() {
// 注意这里
return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
}
这里的 RETURNS_DEFAULTS
的实现,基本可以理解为返回类型默认值(比如 boolean
类型结果就返回 false
,int
类型就返回 0
)。
不同点 2:内部状态
当然,只执行真实方法还不够,还需要有内部状态才行。
比如,下面的代码:
List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
List spiedList = Mockito.spy(list);
既然 list
对象中添加了 "123"
和 "456"
这两个元素,那么 spiedList
中也得有这两个元素才行。
Mockito 中相关实现代码如下,说白了就是会把原对象的字段都复制过来:
// java/org/mockito/internal/util/MockUtil.java
// 第 41 行
T mock;
if (spiedInstance != null) {
// spy 会走这里
mock =
mockMaker
.createSpy(settings, mockHandler, (T) spiedInstance)
.orElseGet(
() -> {
// 首先创建一个 mock
// 可以看到,和下面创建普通 mock 的方法是一模一样的
T instance = mockMaker.createMock(settings, mockHandler);
// 注意这一步,会把原对象中的字段都复制过来
new LenientCopyTool().copyToMock(spiedInstance, instance);
return instance;
});
} else {
// 普通的 mock 会走这里
mock = mockMaker.createMock(settings, mockHandler);
}
小结
可以看到,在实现上,spy
和 mock
是非常相近的。
如果你想创建 spy
,实际上也是先创建了一个 mock
(只不过 settings
不同,包括 settings
里设置的默认返回结果方式不同),然后把原对象的字段复制过去。