天天看點

Android單元測試架構源碼分析(一)淺析Mockito

    Android單元測試架構有很多,我們首先分析Mockito.

    Mockito作為Android常用單元測試架構之一,為大家熟悉的作用就是打樁來模拟環境,但是随着需求的變化往往需要對Mockito流程做出稍許修改,我們常用的是對Mockito的類進行繼承,甚至如果Mockito不能實作我們的需求,還需要自定義單元測試架構。

    一、Mockito的mock過程

Mockito.mock(Object.class);           

    這是Mockito模拟對象最基本的用法,模拟對象後就可以對對象進行打樁。所謂打樁,就是方法模拟,比如設定某個方法的傳回值為自定義的值,在打樁之後就可以保證環境的證券,之後才能通過斷言來進行單元測試。很明顯,模拟後的對象不是原對象。通過輸出模拟類的類資訊,可以發現這個模拟的對象是原對象的子類。為什麼我明明沒有看到有 XXX entends Object{}這樣的類卻能夠生成模拟對象的子類呢?在這裡Mockito是通過動态生成位元組碼來生成模拟對象的子類。

    Mockito引用庫結構:

Android單元測試架構源碼分析(一)淺析Mockito

            可以看出,Mockito引用了CGLIB架構,而CGLIB又引用了ASM架構,有TX可能會問,這兩個架構是幹什麼用的呢?其實了解了這兩個架構基本就能了解Mockito的運作原理。

        先說說各自的作用,CGLIB的作用是建立動态代理,根據《Think in java》中的說法,動态代理的目的是為了添加額外的操作。而在單元測試模拟對象的過程中需要添加哪些操作呢?我們可以自己思考下,如果自己來實作模拟對象要做哪些事呢?

        假如需要模拟的類如下:

public class MockTest
{
    private void test(Object... args)
    {
        someOperate();
    }

    private void someOperate()
    {

    }
}           

        為了實作控制方法的傳回值,我們需要在每個方法的開頭添加類似如下代碼

if(mocked)
        {
            try
            {
                mockMethod.invoke(receiver,args);
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
        }           

         新生成的類類似這樣

public class MockTest
{
    private boolean mocked;
    private Method mockMethod;
    private Object receiver;

    private void test(Object... args)
    {
        if(mocked)
        {
            try
            {
                mockMethod.invoke(receiver,args);
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
        }
        someOperate();
    }

    private void someOperate()
    {

    }
}           

       為了能夠驗證哪個方法是否已經執行或者執行多少次,需要記錄方法調用,需要添加類似如下代碼

if(recorded)
        {
            if(!methodsRecord.keySet().contains("test"))
            {
                methodsRecord.put("test",0);
            }
            int count=methodsRecord.get("test");
            methodsRecord.put("test",++count);
        }           

        之後新生成的類類似這樣

public class MockTest
{
    private boolean recorded;
    private Map<String,Integer> methodsRecord=new HashMap<>();

    private void test(Object... args)
    {
        if(recorded)
        {
            if(!methodsRecord.keySet().contains("test"))
            {
                methodsRecord.put("test",0);
            }
            int count=methodsRecord.get("test");
            methodsRecord.put("test",++count);
        }
        someOperate();
    }

    private void someOperate()
    {

    }
}           

        除了這些,還有驗證方法參數等,需要一個容器儲存參數值等,這些方法都需要我們插入到模拟方法中。這些操作就是動态代理中的“額外操作”。關于動态代理可以看看這篇博文Java動态代理機制詳解(JDK 和CGLIB,Javassist,ASM),裡面也講了ASM的一些原理。

       二、從源代碼方面看Mockito通過動态代理實作對象模拟

        通過上述第一行代碼,我們可以查到模拟對象實作地方:org.mockito.internal    MockitoCore.java

public <T> T mock(Class<T> typeToMock, MockSettings settings) {
        if (!MockSettingsImpl.class.isInstance(settings)) {
            throw new IllegalArgumentException(
                    "Unexpected implementation of '" + settings.getClass().getCanonicalName() + "'\n"
                    + "At the moment, you cannot provide your own implementations that class.");
        }
        MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
        MockCreationSettings<T> creationSettings = impl.confirm(typeToMock);
        T mock = mockUtil.createMock(creationSettings);
        mockingProgress.mockingStarted(mock, typeToMock);
        return mock;
    }           

        繼續查找createMock()方法:這裡通過org.mockito.internal.util 的MockUtil.java動态調用了org.mockito.internal.creation.cglib    CglibMockMaker.java類

public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
        InternalMockHandler mockitoHandler = cast(handler);
        new AcrossJVMSerializationFeature().enableSerializationAcrossJVM(settings);
        return new ClassImposterizer(new InstantiatorProvider().getInstantiator(settings)).imposterise(
                new MethodInterceptorFilter(mockitoHandler, settings), settings.getTypeToMock(), settings.getExtraInterfaces());
}           

       可以看到主要生産<T>泛型的方法為imposterise()

public <T> T imposterise(final MethodInterceptor interceptor, Class<T> mockedType, Class<?>... ancillaryTypes) {
        Class<Factory> proxyClass = null;
        Object proxyInstance = null;
        try {
            setConstructorsAccessible(mockedType, true);
            proxyClass = createProxyClass(mockedType, ancillaryTypes);
            proxyInstance = createProxy(proxyClass, interceptor);
            return mockedType.cast(proxyInstance);
        } catch (ClassCastException cce) {
            throw new MockitoException(join(
                "ClassCastException occurred while creating the mockito proxy :",
                "  class to mock : " + describeClass(mockedType),
                "  created class : " + describeClass(proxyClass),
                "  proxy instance class : " + describeClass(proxyInstance),
                "  instance creation by : " + instantiator.getClass().getSimpleName(),
                "",
                "You might experience classloading issues, disabling the Objenesis cache *might* help (see MockitoConfiguration)"
            ), cce);
        } finally {
            setConstructorsAccessible(mockedType, false);
        }
    }           

        查找createProxy()方法

private Object createProxy(Class<Factory> proxyClass, final MethodInterceptor interceptor) {
        Factory proxy;
        try {
            proxy = instantiator.newInstance(proxyClass);
        } catch (InstantationException e) {
            throw new MockitoException("Unable to create mock instance of type '" + proxyClass.getSuperclass().getSimpleName() + "'", e);
        }
        proxy.setCallbacks(new Callback[] {interceptor, SerializableNoOp.SERIALIZABLE_INSTANCE });
        return proxy;
    }           

        繼續查找Initantiator的方法,發現creaye僅僅是将參數中的class對象進行執行個體化,是以應該查找上一塊代碼的createProxyClass()方法,好了接下來就是真正核心的部分

public Class<Factory> createProxyClass(Class<?> mockedType, Class<?>... interfaces) {
        if (mockedType == Object.class) {
            mockedType = ClassWithSuperclassToWorkAroundCglibBug.class;
        }
        
        Enhancer enhancer = new Enhancer() {
            @Override
            @SuppressWarnings("unchecked")
            protected void filterConstructors(Class sc, List constructors) {
                // Don't filter
            }
        };
        Class<?>[] allMockedTypes = prepend(mockedType, interfaces);
		enhancer.setClassLoader(SearchingClassLoader.combineLoadersOf(allMockedTypes));
        enhancer.setUseFactory(true);
        if (mockedType.isInterface()) {
            enhancer.setSuperclass(Object.class);
            enhancer.setInterfaces(allMockedTypes);
        } else {
            enhancer.setSuperclass(mockedType);
            enhancer.setInterfaces(interfaces);
        }
        enhancer.setCallbackTypes(new Class[]{MethodInterceptor.class, NoOp.class});
        enhancer.setCallbackFilter(IGNORE_BRIDGE_METHODS);
        if (mockedType.getSigners() != null) {
            enhancer.setNamingPolicy(NAMING_POLICY_THAT_ALLOWS_IMPOSTERISATION_OF_CLASSES_IN_SIGNED_PACKAGES);
        } else {
            enhancer.setNamingPolicy(MockitoNamingPolicy.INSTANCE);
        }

        enhancer.setSerialVersionUID(42L);
        
        try {
            return enhancer.createClass(); 
        } catch (CodeGenerationException e) {
            if (Modifier.isPrivate(mockedType.getModifiers())) {
                throw new MockitoException("\n"
                        + "Mockito cannot mock this class: " + mockedType 
                        + ".\n"
                        + "Most likely it is a private class that is not visible by Mockito");
            }
            throw new MockitoException("\n"
                    + "Mockito cannot mock this class: " + mockedType 
                    + "\n" 
                    + "Mockito can only mock visible & non-final classes."
                    + "\n" 
                    + "If you're not sure why you're getting this error, please report to the mailing list.", e);
        }
    }           

        這個地方可以看到在做很多設定,包括設定父類設定攔截函數等,最後實作Class對象的部分是Enhance.createClass()

protected Object create(Object key) {
        try {
        	Class gen = null;
        	
            synchronized (source) {
                ClassLoader loader = getClassLoader();
                Map cache2 = null;
                cache2 = (Map)source.cache.get(loader);
                if (cache2 == null) {
                    cache2 = new HashMap();
                    cache2.put(NAME_KEY, new HashSet());
                    source.cache.put(loader, cache2);
                } else if (useCache) {
                    Reference ref = (Reference)cache2.get(key);
                    gen = (Class) (( ref == null ) ? null : ref.get()); 
                }
                if (gen == null) {
                    Object save = CURRENT.get();
                    CURRENT.set(this);
                    try {
                        this.key = key;
                        
                        if (attemptLoad) {
                            try {
                                gen = loader.loadClass(getClassName());
                            } catch (ClassNotFoundException e) {
                                // ignore
                            }
                        }
                        if (gen == null) {
                            byte[] b = strategy.generate(this);
                            String className = ClassNameReader.getClassName(new ClassReader(b));
                            getClassNameCache(loader).add(className);
                            gen = ReflectUtils.defineClass(className, b, loader);
                        }
                       
                        if (useCache) {
                            cache2.put(key, new WeakReference(gen));
                        }
                        return firstInstance(gen);
                    } finally {
                        CURRENT.set(save);
                    }
                }
            }
            return firstInstance(gen);
        } catch (RuntimeException e) {
            throw e;
        } catch (Error e) {
            throw e;
        } catch (Exception e) {
            throw new CodeGenerationException(e);
        }
    }           

    主要部分在這一塊

if (gen == null)

{

    byte[] b = strategy.generate(this);//動态生成位元組碼的位元組數組

    String className = ClassNameReader.getClassName(new ClassReader(b));

    getClassNameCache(loader).add(className);

    gen = ReflectUtils.defineClass(className, b, loader);//通過位元組數組生成class對象

}

    其中strategy.generate(this)屬于ASM部分,有興趣的了解下,最後的ReflectUtils.defineClass()用來通過位元組數組來生成模拟類。

    好了,最後總結下,根據以上代碼的分析,我們知道Mockito首先儲存設定的類資訊,例如模拟類的父類,對象方法攔截操作等,然後根據這些類的資訊來動态生成class對象,這個對象是模拟對象的子類,所有方法都插入了攔截事件,可以動态的設定方法傳回值,可以動态的擷取某些方法的調用次數,可以動态擷取某方法傳入參數。最後将這些資訊提供給開發者,用于開發者利用這些資訊來進行對比,進而判斷測試的正确還是錯誤。

參考:

1.Android最佳Mock單元測試方案:Junit + Mockito + Powermock

2.Java動态代理機制詳解(JDK 和CGLIB,Javassist,ASM)