Mockito實現原理探析 -- Mockito.when(...).thenReturn(...)的一個簡化實現
阿新 • • 發佈:2019-02-01
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)); } }