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 对象默认的属性值与方法默认返回值。

该类的完整代码如下:

  1 package cc.unmi;
  2  
  3 import java.util.Collection;
  4 import java.util.Map;
  5 import java.util.Optional;
  6  
  7 public class MyClass {
  8     public int integer;
  9     public Double aDouble;
 10     public String string;
 11     public long[] array;
 12     public Optional<String> optional;
 13     public Collection<String> collection;
 14     public Map<String, String> map;
 15  
 16     public int getInteger() {
 17         return 99;
 18     }
 19  
 20     public long[] getArray() {
 21         return new long[]{0};
 22     }
 23  
 24     public Double getDouble() {
 25         return 9.9;
 26     }
 27  
 28     public String getString() {
 29         return "hello";
 30     }
 31  
 32     public Optional<String> getOptional() {
 33         return null;
 34     }
 35  
 36     public Collection<String> getCollection() {
 37         return null;
 38     }
 39  
 40     public Map<String, String> getMap() {
 41         return null;
 42     }
 43 }
 44 为了认识到调用 Mock 对象时默认情况下不会调用实际方法实现,我们故意让上面的方法返回一些乱七八糟的值。
 45 
 46 测试类 MyClassTest 的代码如下
 47 
 48 1
 49 2
 50 3
 51 4
 52 5
 53 6
 54 7
 55 8
 56 9
 57 10
 58 11
 59 12
 60 13
 61 14
 62 15
 63 16
 64 17
 65 18
 66 19
 67 20
 68 21
 69 22
 70 23
 71 24
 72 25
 73 26
 74 27
 75 28
 76 29
 77 30
 78 31
 79 32
 80 33
 81 34
 82 35
 83 36
 84 package cc.unmi;
 85  
 86 import org.junit.Test;
 87 import org.junit.runner.RunWith;
 88 import org.mockito.Mockito;
 89 import org.mockito.junit.MockitoJUnitRunner;
 90  
 91 @RunWith(MockitoJUnitRunner.class)
 92 public class MyClassTest {
 93  
 94     @Test
 95     public void watchMockedClass() {
 96         MyClass myClass = Mockito.mock(MyClass.class);
 97         printDefaults(myClass);
 98     }
 99  
100     private void printDefaults(MyClass myClass) {
101         System.out.println("fields ---- ");
102         System.out.println("integer: " + myClass.integer);
103         System.out.println("array: " + myClass.array);
104         System.out.println("double: " + myClass.aDouble);
105         System.out.println("string: " + myClass.string);
106         System.out.println("optional: " + myClass.optional);
107         System.out.println("collection: " + myClass.collection);
108         System.out.println("map: " + myClass.map);
109  
110         System.out.println("\nmethods ---- ");
111         System.out.println("integer: " + myClass.getInteger());
112         System.out.println("array: " + myClass.getArray());
113         System.out.println("double: " + myClass.getDouble());
114         System.out.println("string: " + myClass.getString());
115         System.out.println("optional: " + myClass.getOptional());
116         System.out.println("collection: " + myClass.getCollection() + ", " + myClass.getCollection().getClass());
117         System.out.println("map: " + myClass.getMap() + ", " + myClass.getMap().getClass());
118     }
119 }

执行上面的代码输出如下:

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 的如下方法:

1 public static <T> T mock(Class<T> classToMock) {
2     return mock(classToMock, withSetting());
3 }

再看 withSetting() 方法:

1 public static MockSetting withSetting() {
2     return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
3 }

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

Mockito.mock(MyClass.class, Answers.RETURNS_DEFAULTS);

在 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 的源代码吧:

 1 public class ReturnsEmptyValues implements Answer<Object>, Serializable {
 2  
 3     private static final long serialVersionUID = 1998191268711234347L;
 4  
 5  
 6     /* (non-Javadoc)
 7      * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock)
 8      */
 9     public Object answer(InvocationOnMock invocation) {
10         if (isToStringMethod(invocation.getMethod())) {
11             Object mock = invocation.getMock();
12             MockName name = MockUtil.getMockName(mock);
13             if (name.isDefault()) {
14                 return "Mock for " + MockUtil.getMockSettings(mock).getTypeToMock().getSimpleName() + ", hashCode: " + mock.hashCode();
15             } else {
16                 return name.toString();
17             }
18         } else if (isCompareToMethod(invocation.getMethod())) {
19             //see issue 184.
20             //mocks by default should return 0 if references are the same, otherwise some other value because they are not the same. Hence we return 1 (anything but 0 is good).
21             //Only for compareTo() method by the Comparable interface
22             return invocation.getMock() == invocation.getArgument(0) ? 0 : 1;
23         }
24  
25         Class<?> returnType = invocation.getMethod().getReturnType();
26         return returnValueFor(returnType);
27     }
28  
29     Object returnValueFor(Class<?> type) {
30         if (Primitives.isPrimitiveOrWrapper(type)) {
31             return Primitives.defaultValue(type);
32             //new instances are used instead of Collections.emptyList(), etc.
33             //to avoid UnsupportedOperationException if code under test modifies returned collection
34         } else if (type == Iterable.class) {
35             return new ArrayList<Object>(0);
36         } else if (type == Collection.class) {
37             return new LinkedList<Object>();
38         } else if (type == Set.class) {
39             return new HashSet<Object>();
40         } else if (type == HashSet.class) {
41             return new HashSet<Object>();
42         } else if (type == SortedSet.class) {
43             return new TreeSet<Object>();
44         } else if (type == TreeSet.class) {
45             return new TreeSet<Object>();
46         } else if (type == LinkedHashSet.class) {
47             return new LinkedHashSet<Object>();
48         } else if (type == List.class) {
49             return new LinkedList<Object>();
50         } else if (type == LinkedList.class) {
51             return new LinkedList<Object>();
52         } else if (type == ArrayList.class) {
53             return new ArrayList<Object>();
54         } else if (type == Map.class) {
55             return new HashMap<Object, Object>();
56         } else if (type == HashMap.class) {
57             return new HashMap<Object, Object>();
58         } else if (type == SortedMap.class) {
59             return new TreeMap<Object, Object>();
60         } else if (type == TreeMap.class) {
61             return new TreeMap<Object, Object>();
62         } else if (type == LinkedHashMap.class) {
63             return new LinkedHashMap<Object, Object>();
64         } else if ("java.util.Optional".equals(type.getName())) {
65             return JavaEightUtil.emptyOptional();
66         } else if ("java.util.OptionalDouble".equals(type.getName())) {
67             return JavaEightUtil.emptyOptionalDouble();
68         } else if ("java.util.OptionalInt".equals(type.getName())) {
69             return JavaEightUtil.emptyOptionalInt();
70         } else if ("java.util.OptionalLong".equals(type.getName())) {
71             return JavaEightUtil.emptyOptionalLong();
72         } else if ("java.util.stream.Stream".equals(type.getName())) {
73             return JavaEightUtil.emptyStream();
74         } else if ("java.util.stream.DoubleStream".equals(type.getName())) {
75             return JavaEightUtil.emptyDoubleStream();
76         } else if ("java.util.stream.IntStream".equals(type.getName())) {
77             return JavaEightUtil.emptyIntStream();
78         } else if ("java.util.stream.LongStream".equals(type.getName())) {
79             return JavaEightUtil.emptyLongStream();
80         }
81  
82         //Let's not care about the rest of collections.
83         return null;
84     }
85 }

从上可以看到所有列出的方法默认返回值的映射情况,未涉及到的就是 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  对象那一行从

1 MyClass myClass = Mockito.mock(MyClass.class);
1 MyClass myClass = Mockito.mock(MyClass.class, Mockito.withSettings()
2     .defaultAnswer(Answers.RETURNS_SMART_NULLS).verboseLogging());

我们同时开启了调用 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,也有类似的行为,不在此罗列了。

posted @ 2019-04-01 11:11  Boblim  阅读(6564)  评论(0编辑  收藏  举报