Mockito 中被 Mocked 的对象属性及方法的默认值

在 Java 测试中使用 Mockito 有段时日了,以前只是想当然的认为 Mock 的对象属性值和方法返回值都是依据同样的规则。基本类型是 0, 0.0, 或 false, 对象类型都是 null, Mock 对象的默认返回值也应该是一样的。直到最近有一天,有一个返回 Optional<String> 类型的方法,由于忘记对该方法打桩,意外的发现它返回的不是 null, 而 Optional.empty(), 因此才意识到此处定有蹊跷。着实有必要用代码验证一下 Mockito 是怎么决定属性及方法的各种返回类型的默认值的。

此次测试所用的 Mockito 版本是 mockito-core-2.12.0.

于是创建了下面一个类 MyClass 用于生成 Mock 对象,选取了一些典型的数据类型, 包括 int, Double, String, long[], Optional<String>, Collection<String>, Map<String, String>, 同时测试 Mock 对象默认的属性值与方法默认返回值。

该类的完整代码如下:

为了认识到调用 Mock 对象时默认情况下不会调用实际方法实现,我们故意让上面的方法返回一些乱七八糟的值。

测试类 MyClassTest 的代码如下

执行上面的代码输出如下

fields ----
integer: 0
array: null
double: null
string: null
optional: null
collection: null
map: null

methods ----
integer: 0
array: null
double: 0.0
string: null
optional: Optional.empty
collection: [], class java.util.LinkedList
map: {}, class java.util.HashMap

Mockito mock 的对象属性的默认值没什么异议,与 Java 初始化对象的规则一致,基本类型的默认值是 0, 0.0, 或 false。但是对于方法默认返回值就不一样了,从上面我们看到

  1. int 类型方法默认返回 0
  2. long[] 类型方法默认返回 null
  3. Double 类型方法默认返回 0.0
  4. string 类型方法默认返回 null
  5. Optional<String> 类型方法默认返回 Optional.empty
  6. Collection<String> 类型方法默认返回 new LinkedList<String>(0)
  7. Map<String, String> 类型方法默认返回 new HashMap<String, String>(0)

关于 Mock 对象属性的默认值可以搁一边,那么 Mockito 是如何定义 Mock 对象方法的默认返回值的呢?

通常的,我们创建一个 Mock 对象都是简单的调用 Mockito 的如下方法

再看 withSetting() 方法

绕了一圈,基实我们默认采用的 Mock 对象的方式其实就是如下

org.mockito.Answers 中定义了如下设定方法默认返回值的选项

  1. RETURN_DEFAULTS(new GloballyConfiguredAnswer())  -- 基本对应到 ReturnsEmptyValues 实现
  2. RETURNS_SMART_NULLS(new ReturnsSmartNulls())  -- 最后对应到 ReturnsMoreEmptyValues 实现
  3. RETURN_MOCKS(new ReturnsMocks())
  4. RETURNS_DEEP_STUBS(new ReturnsDeepStubs())
  5. CALL_REAL_METHODS(new CallsRealMethods())
  6. RETURNS_SELF(new TriesToReturnSelf())

所以默认情况下的 RETURNS_DEFAULTS, Mock 对象方法返回值就是由 ReturnsEmptyValues 类决定的,看这个类的注释

Default answer of every Mockito mock.

  • Returns appropriate primitive for primitive-returning methods
  • Returns consistent values for primitive wrapper classes (e.g. int-returning method retuns 0 and Integer-returning method returns 0, too)
  • Returns empty collection for collection-returning methods (works for most commonly used collection types)
  • Returns description of mock for toString() method
  • Returns zero if references are equals otherwise non-zero for Comparable#compareTo(T other) method (see issue 184)
  • Returns null for everything else

至此,最能说明问题仍然是源代码,很想节约些篇幅,但实在是不行; 欣赏一下 ReturnsEmptyValues 的源代码吧

从上可以看到所有列出的方法默认返回值的映射情况,未涉及到的就是 null.

我们还可以关注一下另一个 Answer: RETURN_SMART_NULL, 同样是看相应实现类 ReturnsMoreEmptyValues  的注解 

It's likely this implementation will be used by default by every Mockito 3.0.0 mock.
Currently used only by Mockito.RETURNS_SMART_NULLS
Current version of Mockito mocks by default use ReturnsEmptyValues

  • Returns appropriate primitive for primitive-returning methods
  • Returns consistent values for primitive wrapper classes (e.g. int-returning method returns 0 and Integer-returning method returns 0, too)
  • Returns empty collection for collection-returning methods (works for most commonly used collection types)
  • Returns empty array for array-returning methods
  • Returns "" for String-returning method
  • Returns description of mock for toString() method
  • Returns non-zero for Comparable#compareTo(T other) method (see issue 184)
  • Returns null for everything else

这还是一个面向未来(Mockito 3.0.9) 的默认的 Answer, 它与 RETURNS_DEFAULTS 有所不同的是数组,字符串不再为 null, 而是空数组和空字符串。

我们可以作一个测试,前面的 MyClassTest 代码,把构造 MyClass Mock  对象那一行从

改成

我们同时开启了调用 Mock 方法时的详细输出,重新运行后,控制台输出

fields ----
integer: 0
array: null
double: null
string: null
optional: null
collection: null
map: null

methods ----
############ Logging method invocation #1 on mock/spy ########
myClass.getInteger();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:31)
has returned: "0" (java.lang.Integer)

integer: 0
############ Logging method invocation #2 on mock/spy ########
myClass.getArray();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:32)
has returned: "[J@4009e306" ([J)

array: [J@4009e306
############ Logging method invocation #3 on mock/spy ########
myClass.getDouble();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:33)
has returned: "0.0" (java.lang.Double)

double: 0.0
############ Logging method invocation #4 on mock/spy ########
myClass.getString();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:34)
has returned: "" (java.lang.String)

string:
############ Logging method invocation #5 on mock/spy ########
myClass.getOptional();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:35)
has returned: "Optional.empty" (java.util.Optional)

optional: Optional.empty
############ Logging method invocation #6 on mock/spy ########
myClass.getCollection();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:36)
has returned: "[]" (java.util.LinkedList)

############ Logging method invocation #7 on mock/spy ########
myClass.getCollection();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:36)
has returned: "[]" (java.util.LinkedList)

collection: [], class java.util.LinkedList
############ Logging method invocation #8 on mock/spy ########
myClass.getMap();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:37)
has returned: "{}" (java.util.HashMap)

############ Logging method invocation #9 on mock/spy ########
myClass.getMap();
invoked: -> at cc.unmi.MyClassTest.printDefaults(MyClassTest.java:37)
has returned: "{}" (java.util.HashMap)

map: {}, class java.util.HashMap

有所不同的也就是数组默认为空,字符串默认为空字符串,都不再是 null 了。

另外,剩下的几个 Answer,除了 CALL_REAL_METHODS 很容易理解(就是不 Mock 方法了)。其余三个

  • RETURN_MOCKS(new ReturnsMocks())
  • RETURNS_DEEP_STUBS(new ReturnsDeepStubs())
  • RETURNS_SELF(new TriesToReturnSelf())

的具体用意待到有需求时再去扒它们吧。

类比于 Mockito, 我也大致测试了一下 JMockit,也有类似的行为,不在此罗列了。

本文链接 https://yanbin.blog/mockito-mocked-default-fields-method-returns/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments