Android单元测试框架源码分析(三)构建自己的单元测试框架

    分析完之前的源码后,或许我对Android单元测试有了一定的了解,但是如果要深入Android单元测试,就必须尝试自己编写Android单元测试框架。现在流行的Android单元测试框架期初都并不完美,我们开始编写框架时不要考虑太多细节,首先构建测试框架的骨架,所以我们先构建Java单元测试框架,然后在此基础上修改。

    简单描述下框架运行流程就是:1. 收集测试信息 2. 运行自定义类加载器 3. 自定义类加载器加载测试用例 4. 原类被替换为模拟类 5. 返回测试结果

    为了方便测试,我们假设现在有个天气预报站,要从数据中心收集数据进行预报,我们要测试预报的准确性,测试思路无非是模拟数据中心的数据,判断预报的数据和测试中心的数据是否一致。

public class Station
{
    public Object getWeatherData()
    {
        return "WeatherData";
    }
}

public class Broadcast
{
    public String broadcastWeather(Object object)
    {
        System.out.println("broadcastWeather:"+object);
        return "broadcastWeather:"+object;
    }
}

    根据以上信息,我们来设计自己的单元测试框架:



    首先看第一步,收集测试信息

   

public class CustomUnitTestRunner
{
    public static void main(String[] args)
    {
        try
        {
            //通过类加载器加载测试用例
            Class testSuteClazz=UnitTestClassLoader.getInstance().loadClass("customunittestarchitecture.TestSute");
            Object testSute=testSuteClazz.newInstance();

            //运行测试用例
            Method method=testSuteClazz.getMethod("testWeatherForcast");
            method.invoke(testSute);

        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}

     为了方便,我并没有使用@Test来获取测试用例,而是直接通过反射调用测试用例


    第二步,运行类加载器

public class UnitTestClassLoader extends URLClassLoader
{
    //使用URLClassLoader必须要制定jar包目录
    private static String defaultWorkhomePath="E:\\workhome\\IDEA\\ExampleForUnitTese\\out\\production\\ExampleForUnitTese\\";
    private static String defaultJarPath1="E:\\workhome\\IDEA\\ExampleForUnitTese\\lib\\cglib-3.2.4.jar";
    private static String defaultJarPath2="E:\\workhome\\IDEA\\ExampleForUnitTese\\lib\\asm-5.2.jar";

    private byte[] _bytes;
    private Class clazz;

    public UnitTestClassLoader(URL[] urls, ClassLoader parent)
    {
        super(urls, parent);
    }

    public UnitTestClassLoader(URL[] urls)
    {
        super(urls);
    }

    public UnitTestClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
    {
        super(urls, parent, factory);
    }

    private static UnitTestClassLoader _instance;

    public static UnitTestClassLoader getInstance()
    {
        if(_instance==null)
        {
            try
            {
                //指定jar包目录
                _instance=new UnitTestClassLoader(new URL[]{
                        new File(defaultJarPath1).toURI().toURL(),
                        new File(defaultJarPath2).toURI().toURL(),
                        new File(defaultWorkhomePath).toURI().toURL()},null);
            }
            catch(Exception e)
            {
                _instance=new UnitTestClassLoader(new URL[]{},null);
                e.printStackTrace();
            }
        }
        return _instance;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        //对比模拟类,如果需要模拟,就利用ASM修改字节码,动态生成模拟类
        if(name.equals(Station.class.getName()))
        {
            byte[] bytes=AsmCore.getClassByteArray(Station.class.getName());
            return defineClass(name,bytes,0,bytes.length);
        }
        return super.findClass(name);
    }
}

    此处使用自定义类加载器是继承URLClassLoader,使用此URLClassLoader时可以通过直接指定Jar包目录来指定依赖库地址。

    使用ClassLoader的关键之处在于重写findClass类,当匹配到的类名为指定的类名时,则交给ASM来修改字节码。



    第三步,其实就是上述第一步代码中的反射调用。

 

   第四部,原始类被替换为模拟类

public class AsmCore
{
    public static byte[] getClassByteArray(String className)
    {
        try
        {
            //根据传进来的类名作为ClassReader的参数
            ClassReader classReader=new ClassReader(className);
            //ASM通过职责链的设计模式来访问类的字节码
            ClassWriter classWriter=new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS);
            //通过添加职责链来控制输出的字节码
            ClassVisitor classVisitor=new ChangeClassClassVisitor(Opcodes.ASM5,classWriter);
            classReader.accept(classVisitor,Opcodes.ASM5);

            byte[] bytes=classWriter.toByteArray();
            File file = new File("E:\\workhome\\TestForAsm\\Station$$$.class");
            FileOutputStream fout = new FileOutputStream(file);
            //这里通过输出字节码到文件方便测试
            fout.write(bytes);
            fout.close();

            return bytes;
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return new byte[]{};
    }

    static class ChangeClassClassVisitor extends ClassVisitor
    {

        public ChangeClassClassVisitor(int i)
        {
            super(i);
        }

        public ChangeClassClassVisitor(int i, ClassVisitor classVisitor)
        {
            super(i, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions)
        {
            //ClassVisit里面捕捉方法信息
            if(name.equals("getWeatherData"))
            {
                MethodVisitor visitor= super.visitMethod(access, name, desc, signature, exceptions);
                return new ChangeMethodMethdVisiter(Opcodes.ASM5,visitor);
            }
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }

    static  class ChangeMethodMethdVisiter extends MethodVisitor
    {

        public ChangeMethodMethdVisiter(int i)
        {
            super(i);
        }

        public ChangeMethodMethdVisiter(int i, MethodVisitor methodVisitor)
        {
            super(i, methodVisitor);
        }


        @Override
        public void visitInsn(int i)
        {
            //MethodVisit里面捕捉返回信息
            if(i==ARETURN)
            {
                mv.visitLdcInsn("MyCustomData");
            }
            super.visitInsn(i);
        }
    }
}

    这里简单的使用了ASM,使用ASM找到了getWeatherData方法,并且在返回常量值得时候返回指定的常量,这样就实现了方法返回值的替换。


    第五步,返回测试信息

public class TestSute
{
    public void testWeatherForcast()
    {
        Station station=new Station();
        Broadcast broadcast=new Broadcast();
        String log=broadcast.broadcastWeather(station.getWeatherData());
        if(log.equals("broadcastWeather:MyCustomData"))
        {
            System.out.println("-----------test success-----------");
        }
        else
        {
            System.out.println("-----------test failure-----------");
        }
    }
}

    这里直接在测试用例里面输出,根据测试输出判断测试结果。


    代码地址:http://download.csdn.net/detail/xiaoshixiu/9765668


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章