在 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 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 34 35 36 37 38 39 40 41 42 43 |
package cc.unmi; import java.util.Collection; import java.util.Map; import java.util.Optional; public class MyClass { public int integer; public Double aDouble; public String string; public long[] array; public Optional<String> optional; public Collection<String> collection; public Map<String, String> map; public int getInteger() { return 99; } public long[] getArray() { return new long[]{0}; } public Double getDouble() { return 9.9; } public String getString() { return "hello"; } public Optional<String> getOptional() { return null; } public Collection<String> getCollection() { return null; } public Map<String, String> getMap() { return null; } } |
为了认识到调用 Mock 对象时默认情况下不会调用实际方法实现,我们故意让上面的方法返回一些乱七八糟的值。
测试类 MyClassTest
的代码如下
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 34 35 36 |
package cc.unmi; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class MyClassTest { @Test public void watchMockedClass() { MyClass myClass = Mockito.mock(MyClass.class); printDefaults(myClass); } private void printDefaults(MyClass myClass) { System.out.println("fields ---- "); System.out.println("integer: " + myClass.integer); System.out.println("array: " + myClass.array); System.out.println("double: " + myClass.aDouble); System.out.println("string: " + myClass.string); System.out.println("optional: " + myClass.optional); System.out.println("collection: " + myClass.collection); System.out.println("map: " + myClass.map); System.out.println("\nmethods ---- "); System.out.println("integer: " + myClass.getInteger()); System.out.println("array: " + myClass.getArray()); System.out.println("double: " + myClass.getDouble()); System.out.println("string: " + myClass.getString()); System.out.println("optional: " + myClass.getOptional()); System.out.println("collection: " + myClass.getCollection() + ", " + myClass.getCollection().getClass()); System.out.println("map: " + myClass.getMap() + ", " + myClass.getMap().getClass()); } } |
执行上面的代码输出如下
fields ----
integer: 0
array: null
double: null
string: null
optional: null
collection: null
map: nullmethods ----
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。但是对于方法默认返回值就不一样了,从上面我们看到
- int 类型方法默认返回 0
- long[] 类型方法默认返回 null
- Double 类型方法默认返回 0.0
- string 类型方法默认返回 null
- Optional<String> 类型方法默认返回 Optional.empty
- Collection<String> 类型方法默认返回 new LinkedList<String>(0)
- Map<String, String> 类型方法默认返回 new HashMap<String, String>(0)
关于 Mock 对象属性的默认值可以搁一边,那么 Mockito 是如何定义 Mock 对象方法的默认返回值的呢?
通常的,我们创建一个 Mock 对象都是简单的调用 Mockito 的如下方法
1 2 3 |
public static <T> T mock(Class<T> classToMock) { return mock(classToMock, withSetting()); } |
再看 withSetting()
方法
1 2 3 |
public static MockSetting withSetting() { return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS); } |
绕了一圈,基实我们默认采用的 Mock 对象的方式其实就是如下
1 |
Mockito.mock(MyClass.class, Answers.RETURNS_DEFAULTS); |
在 org.mockito.Answers
中定义了如下设定方法默认返回值的选项
- RETURN_DEFAULTS(new GloballyConfiguredAnswer()) -- 基本对应到 ReturnsEmptyValues 实现
- RETURNS_SMART_NULLS(new ReturnsSmartNulls()) -- 最后对应到 ReturnsMoreEmptyValues 实现
- RETURN_MOCKS(new ReturnsMocks())
- RETURNS_DEEP_STUBS(new ReturnsDeepStubs())
- CALL_REAL_METHODS(new CallsRealMethods())
- 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 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
public class ReturnsEmptyValues implements Answer<Object>, Serializable { private static final long serialVersionUID = 1998191268711234347L; /* (non-Javadoc) * @see org.mockito.stubbing.Answer#answer(org.mockito.invocation.InvocationOnMock) */ public Object answer(InvocationOnMock invocation) { if (isToStringMethod(invocation.getMethod())) { Object mock = invocation.getMock(); MockName name = MockUtil.getMockName(mock); if (name.isDefault()) { return "Mock for " + MockUtil.getMockSettings(mock).getTypeToMock().getSimpleName() + ", hashCode: " + mock.hashCode(); } else { return name.toString(); } } else if (isCompareToMethod(invocation.getMethod())) { //see issue 184. //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). //Only for compareTo() method by the Comparable interface return invocation.getMock() == invocation.getArgument(0) ? 0 : 1; } Class<?> returnType = invocation.getMethod().getReturnType(); return returnValueFor(returnType); } Object returnValueFor(Class<?> type) { if (Primitives.isPrimitiveOrWrapper(type)) { return Primitives.defaultValue(type); //new instances are used instead of Collections.emptyList(), etc. //to avoid UnsupportedOperationException if code under test modifies returned collection } else if (type == Iterable.class) { return new ArrayList<Object>(0); } else if (type == Collection.class) { return new LinkedList<Object>(); } else if (type == Set.class) { return new HashSet<Object>(); } else if (type == HashSet.class) { return new HashSet<Object>(); } else if (type == SortedSet.class) { return new TreeSet<Object>(); } else if (type == TreeSet.class) { return new TreeSet<Object>(); } else if (type == LinkedHashSet.class) { return new LinkedHashSet<Object>(); } else if (type == List.class) { return new LinkedList<Object>(); } else if (type == LinkedList.class) { return new LinkedList<Object>(); } else if (type == ArrayList.class) { return new ArrayList<Object>(); } else if (type == Map.class) { return new HashMap<Object, Object>(); } else if (type == HashMap.class) { return new HashMap<Object, Object>(); } else if (type == SortedMap.class) { return new TreeMap<Object, Object>(); } else if (type == TreeMap.class) { return new TreeMap<Object, Object>(); } else if (type == LinkedHashMap.class) { return new LinkedHashMap<Object, Object>(); } else if ("java.util.Optional".equals(type.getName())) { return JavaEightUtil.emptyOptional(); } else if ("java.util.OptionalDouble".equals(type.getName())) { return JavaEightUtil.emptyOptionalDouble(); } else if ("java.util.OptionalInt".equals(type.getName())) { return JavaEightUtil.emptyOptionalInt(); } else if ("java.util.OptionalLong".equals(type.getName())) { return JavaEightUtil.emptyOptionalLong(); } else if ("java.util.stream.Stream".equals(type.getName())) { return JavaEightUtil.emptyStream(); } else if ("java.util.stream.DoubleStream".equals(type.getName())) { return JavaEightUtil.emptyDoubleStream(); } else if ("java.util.stream.IntStream".equals(type.getName())) { return JavaEightUtil.emptyIntStream(); } else if ("java.util.stream.LongStream".equals(type.getName())) { return JavaEightUtil.emptyLongStream(); } //Let's not care about the rest of collections. return null; } } |
从上可以看到所有列出的方法默认返回值的映射情况,未涉及到的就是 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 2 |
MyClass myClass = Mockito.mock(MyClass.class, Mockito.withSettings() .defaultAnswer(Answers.RETURNS_SMART_NULLS).verboseLogging()); |
我们同时开启了调用 Mock 方法时的详细输出,重新运行后,控制台输出
fields ----
integer: 0
array: null
double: null
string: null
optional: null
collection: null
map: nullmethods ----
############ 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,也有类似的行为,不在此罗列了。