1. 程式人生 > >Mockito實現原理探析 -- Mockito.when(...).thenReturn(...)的一個簡化實現

Mockito實現原理探析 -- Mockito.when(...).thenReturn(...)的一個簡化實現

        Mockito是一套非常強大的測試框架,被廣泛的應用於Java程式的unit test中,而在其中被使用頻率最高的恐怕要數"Mockito.when(...).thenReturn(...)"了。那這個用起來非常便捷的"Mockito.when(...).thenReturn(...)",其背後的實現原理究竟為何呢?為了一探究竟,筆者實現了一個簡單的MyMockito,也提供了類似的"MyMockito.when(...).thenReturn(...)"支援。當然這個實現只是一個簡單的原型,完備性和健壯性上都肯定遠遠不及mockito(比如沒有annotation支援和thread safe),但應該足以幫助大家理解"Mockito.when(...).thenReturn(...)"的核心實現原理。

       在閱讀以下程式碼之前,希望讀者能簡單的瞭解一下Java動態代理和cglib,沒接觸過的至少應該寫兩個簡單的小程式感受一下(

這篇博文中就提供了一些簡短的示例)。cglib是一個強大的高效能的程式碼生成包,被許多AOP的框架所使用,Mockito也使用了這個庫,我們的MyMockito自然也不例外。

P.S.:

示例程式碼中還使用了lombok,省去了建構函式的編寫。


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import lombok.Data;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

@Data
class MethodInfo {
    private final MyCGLibInterceptor interceptor;
    private final Method method;
    private final Object[] args;
    
    @Override
    public String toString() {
        return "{interceptor: " + interceptor + ", Method: " + method + ", args: " + Arrays.toString(args) + "}";
    }
    
    @Override
    public boolean equals(final Object other) {
        if (other instanceof MethodInfo) {
            final MethodInfo otherMethodInfo = (MethodInfo)other;
            return interceptor.equals(otherMethodInfo.interceptor) && method.equals(otherMethodInfo.method) && Arrays.equals(args, otherMethodInfo.args);
        }
        
        return false;
    }
    
    @Override
    public int hashCode() {
        return interceptor.hashCode() + method.hashCode() + Arrays.hashCode(args);
    }
}

@Data
class MockInjector {
    private final MethodInfo methodInfo;
    public void thenReturn(final Object mockResult) {
        MyMockito.MOCKED_METHODS.put(methodInfo, mockResult);
    }
}

class MyCGLibInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {        
        final MethodInfo key = new MethodInfo(this, method, args);
        final boolean hasMocked = MyMockito.MOCKED_METHODS.containsKey(key);
        if (!hasMocked) {
            // When called for the first time (by MyMockito.when(...)),
            // return a MethodInfo object used as key,
            // so that the later MethodInfo.thenReturn(...) will use this key
            // to insert mock result into the MyMockito.MOCKED_METHODS.
            System.out.println("Initializing the mock for " + key.toString());
            return key;
        } else {
            // Now that MyMockito.MOCKED_METHODS already contains the mock result
            // for this method call, just return the mock result.
            System.out.println("Returns the mock result:");
            return MyMockito.MOCKED_METHODS.get(key);
        }
    }
    
    public Object getInstance(final Class<?> t) {
        final Enhancer enhancer = new Enhancer();  
        enhancer.setSuperclass(t);  
        enhancer.setCallback(this);  
        return enhancer.create();
    }
}

public class MyMockito {
    public static final Map<MethodInfo, Object> MOCKED_METHODS = new HashMap<MethodInfo, Object>();
    
    public static MockInjector when(Object methodCall) {
        return new MockInjector((MethodInfo)methodCall);
    }
    
    private static MyCGLibInterceptor getInterceptor() {
        return new MyCGLibInterceptor();
    }

    public static void main(String[] args) {
        final List<String> myMockList1 = 
                (List<String>)getInterceptor().getInstance(List.class); 
        final List<String> myMockList2 = 
                (List<String>)getInterceptor().getInstance(List.class); 
        final Map<Integer, String> myMockMap = 
                (Map<Integer, String>)getInterceptor().getInstance(Map.class); 
        
        MyMockito.when(myMockList1.get(0)).thenReturn("Hello, I am James");
        MyMockito.when(myMockList1.get(2)).thenReturn("Hello, I am Billy");
        MyMockito.when(myMockList2.get(0)).thenReturn("Hello, I am Tom");
        MyMockito.when(myMockMap.get(10)).thenReturn("Hello, I am Bob");
        
        System.out.println("myMockList1.get(0) = " + myMockList1.get(0));
        System.out.println("myMockList1.get(2) = " + myMockList1.get(2));
        System.out.println("myMockList2.get(0) = " + myMockList2.get(0));
        System.out.println("myMockMap.get(10) = " + myMockMap.get(10));
    }
}