分析完之前的源碼後,或許我對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