天天看点

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)