• 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("
    methods ---- ");
    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,也有类似的行为,不在此罗列了。

  • 相关阅读:
    MySQL根据出生日期计算年龄的五种方法比较
    用于测试API并生文档的开发人员工具
    【实例】使用Eolinker工具进行接口测试时传递集合参数的方法
    如何克服API测试中的挑战
    关于API网关(一)性能
    为什么需要监控API
    比Swagger2更好用的自动生成文档工具?对比流程说话!
    如何通过3个步骤执行基本的API测试
    【学习】API接口测试用例编写规则
    微信小程序之蓝牙 BLE 踩坑记录
  • 原文地址:https://www.cnblogs.com/fnlingnzb-learner/p/10635250.html
Copyright © 2020-2023  润新知