在上一章中,我们简单分析了Mockito的框架结构以及运行原理,可以发现Mockito虽然为Android测试框架,但是实现方法却基本没有用到Android的相关库,也就是说,我也可以将Mockito直接用到JAVA的单元测试中,实时上也的确可以。
但是正是如此,如果在测试过程需要模拟Android手机启动环境的话,需要我们对Android启动相关的所有类进行模拟,但是这样工作量太大。如果有一个框架已经做好了模拟Android环境的工作,我们可以直接调用框架中的Android库来使用,无疑节省了大量的时间。
Robolectric正是在这种环境下诞生的开源Android单元测试框架。Robolectric自己实现了Android启动的相关库,例如Application、Acticity等,我们可以通过activityController.create()来启动一个activity。除此之外Robolectric也自己实现了TextView等控件,我们可以主动操作控件判断反应来进行测试。
在这里我同样不对Robolectric的使用做详细介绍,而是主要分析Robolectric框架的实现方式。
上一章中我并没有介绍的Mockito的运行方式,主要是因为Mockito的运行是直接沿用Junit的运行框架。而Robolectric的启动是则是通过继承Junit的Runner.java类来实现自己的运行方式。当我们点击Android studio的单元测试按钮时,这时首先运行的不是Robolectric中Runner.java类的run()方法,而是通过Android studio中运行库来启动Junit框架,具体的运行方式,可以通过获取栈元素来打印。
在运行单元测试的地方,可以加上如下语句,就可以打印整个单元测试的调用流程
for(StackTraceElement stackTraceElement:Thread.currentThread().getStackTrace())
{
System.out.println(stackTraceElement.getClassName()+" "+stackTraceElement.getFileName()+" "+stackTraceElement.getMethodName());
}
运行单元测试后显示如下:
java.lang.Thread Thread.java getStackTrace
com.business.RedBizTest RedBizTest.java testRedBiz_initData
//调用测试用例
sun.reflect.NativeMethodAccessorImpl NativeMethodAccessorImpl.java invoke0
sun.reflect.NativeMethodAccessorImpl NativeMethodAccessorImpl.java invoke
sun.reflect.DelegatingMethodAccessorImpl DelegatingMethodAccessorImpl.java invoke
java.lang.reflect.Method Method.java invoke
org.junit.runners.model.FrameworkMethod$1 FrameworkMethod.java runReflectiveCall
org.junit.internal.runners.model.ReflectiveCallable ReflectiveCallable.java run
org.junit.runners.model.FrameworkMethod FrameworkMethod.java invokeExplosively
org.junit.internal.runners.statements.InvokeMethod InvokeMethod.java evaluate
org.robolectric.RobolectricTestRunner$HelperTestRunner$1 RobolectricTestRunner.java evaluate
org.junit.internal.runners.statements.RunBefores RunBefores.java evaluate
org.robolectric.RobolectricTestRunner$2 RobolectricTestRunner.java evaluate
org.robolectric.RobolectricTestRunner RobolectricTestRunner.java runChild
org.robolectric.RobolectricTestRunner RobolectricTestRunner.java runChild
org.junit.runners.ParentRunner$3 ParentRunner.java run
org.junit.runners.ParentRunner$1 ParentRunner.java schedule
org.junit.runners.ParentRunner ParentRunner.java runChildren
org.junit.runners.ParentRunner ParentRunner.java access$000
org.junit.runners.ParentRunner$2 ParentRunner.java evaluate
org.robolectric.RobolectricTestRunner$1 RobolectricTestRunner.java evaluate
//进入Robolectric框架
org.junit.runners.ParentRunner ParentRunner.java run
org.junit.runner.JUnitCore JUnitCore.java run
com.intellij.junit4.JUnit4IdeaTestRunner JUnit4IdeaTestRunner.java startRunnerWithArgs
com.intellij.rt.execution.junit.JUnitStarter JUnitStarter.java prepareStreamsAndStart
com.intellij.rt.execution.junit.JUnitStarter JUnitStarter.java main
sun.reflect.NativeMethodAccessorImpl NativeMethodAccessorImpl.java invoke0
sun.reflect.NativeMethodAccessorImpl NativeMethodAccessorImpl.java invoke
sun.reflect.DelegatingMethodAccessorImpl DelegatingMethodAccessorImpl.java invoke
java.lang.reflect.Method Method.java invoke
com.intellij.rt.execution.application.AppMain AppMain.java main
//调用AndroidStudio运行库,此为入口,log显示从下往上
Androidstudio首先运行的是Android Studio\lib\idea_rt.jar 中的AppMain方法,然后逐渐调用到RobolectricTestRunner.java的evaluate方法,这样就从Junit的框架调用到了Robolectric框架。
具体调用流程如下
ParentRunner.run()
RobolectricTestRunner.classBlock()
RobolectricTestRunner.runChild()
RobolectricTestRunner.methodBlock()
调用顺序从上到下,这四行代码基本表面了Robolectric的主要调用框架。其中调用classBlock的时候还会对TestSuit进行测试方法收集,将TestSuit测试分散到测试用例的测试,获取测试用例的方法是:
/**
* Returns the methods that run tests. Default implementation returns all
* methods annotated with {@code @Test} on this class and superclasses that
* are not overridden.
*/
protected List<FrameworkMethod> computeTestMethods() {
return getTestClass().getAnnotatedMethods(Test.class);//通过“@Test”注解来识别测试用例
}
最后运行测试用例的方法methodBlock()比较长,我们慢慢分析
return new Statement() {
@Override
public void evaluate() throws Throwable {
// Configure shadows *BEFORE* setting the ClassLoader. This is necessary because
// creating the ShadowMap loads all ShadowProviders via ServiceLoader and this is
// not available once we install the Robolectric class loader.
configureShadows(sdkEnvironment, config);
Thread.currentThread().setContextClassLoader(sdkEnvironment.getRobolectricClassLoader());
Class bootstrappedTestClass = sdkEnvironment.bootstrappedClass(getTestClass().getJavaClass());
HelperTestRunner helperTestRunner = getHelperTestRunner(bootstrappedTestClass);
final Method bootstrappedMethod;
try {
//noinspection unchecked
bootstrappedMethod = bootstrappedTestClass.getMethod(method.getName());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
parallelUniverseInterface = getHooksInterface(sdkEnvironment);
try {
try {
// Only invoke @BeforeClass once per class
if (!loadedTestClasses.contains(bootstrappedTestClass)) {
invokeBeforeClass(bootstrappedTestClass);
}
assureTestLifecycle(sdkEnvironment);
parallelUniverseInterface.resetStaticState(config);
parallelUniverseInterface.setSdkConfig(sdkEnvironment.getSdkConfig());
int sdkVersion = pickSdkVersion(config, appManifest);
ReflectionHelpers.setStaticField(sdkEnvironment.bootstrappedClass(Build.VERSION.class),
"SDK_INT", sdkVersion);
SdkConfig sdkConfig = new SdkConfig(sdkVersion);
ReflectionHelpers.setStaticField(sdkEnvironment.bootstrappedClass(Build.VERSION.class),
"RELEASE", sdkConfig.getAndroidVersion());
ResourceLoader systemResourceLoader = sdkEnvironment.getSystemResourceLoader(getJarResolver());
setUpApplicationState(bootstrappedMethod, parallelUniverseInterface, systemResourceLoader, appManifest, config);
testLifecycle.beforeTest(bootstrappedMethod);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
final Statement statement = helperTestRunner.methodBlock(new FrameworkMethod(bootstrappedMethod));
// todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
try {
statement.evaluate();
} finally {
try {
parallelUniverseInterface.tearDownApplication();
} finally {
try {
internalAfterTest(bootstrappedMethod);
} finally {
parallelUniverseInterface.resetStaticState(config); // afterward too, so stuff doesn't hold on to classes?
// todo: is this really needed?
Thread.currentThread().setContextClassLoader(RobolectricTestRunner.class.getClassLoader());
}
}
}
} finally {
parallelUniverseInterface = null;
}
}
};
可以看到一个有意思的对象parallelUniverseInterface,直译叫平行世界接口,其实就是Android环境接口,
在这个对象里面有一个RuntimeEnvironment对象,收集测试App的先关参数,例如传进来的Sdkconfig参数,可以获取AndroidManifest参数。
这些都比较容易理解,但是有一个地方可能有点疑问,模拟的类是如何和非模拟的类一起加载到运行环境中的。如果大家Robolectric用得比较熟的话,也许会发现Robolectric的内部实现了很多Android控件,这些实现类名门都会类似ShadowTextView、ShadowButton等,同样如果我们要模拟一个类,比如我要实现一个Time的模拟类,实现Time在调用getCurrentTime是返回的不是现在的时间,而是其他一个指定的时间,我会这样实现,
@Implements(Time.class)
public class ShadowTime
{
@RealObject
private Time _timeRealObject;
private static Time _time;
@Implementation
public void setToNow()
{
_timeRealObject.set(_time);
}
public static void mockTime(Time time)
{
_time =time;
}
}
这样就可以实现在运行时调用mockTime来指定setToNow()设置的时间。但是这个类就只有一个注解表明了和它和实际类Time.java的关系,具体Time类是如何通过这个类被修改的我们并不知道。
要解决上述问题依然要分析上述四行代码中的runChild() 方法
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
final Config config = getConfig(method.getMethod());
if (shouldIgnore(method, config)) {
eachNotifier.fireTestIgnored();
} else if(shouldRunApiVersion(config)) {
eachNotifier.fireTestStarted();
try {
AndroidManifest appManifest = getAppManifest(config);
InstrumentingClassLoaderFactory instrumentingClassLoaderFactory = new InstrumentingClassLoaderFactory(createClassLoaderConfig(config), getJarResolver());
SdkEnvironment sdkEnvironment = instrumentingClassLoaderFactory.getSdkEnvironment(new SdkConfig(pickSdkVersion(config, appManifest)));
methodBlock(method, config, appManifest, sdkEnvironment).evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
}
在这断代码中可以看到实现SdkEnvironment类传进了一个config对象,找到具体的SdkEnvironment实现代码:
public synchronized SdkEnvironment getSdkEnvironment(SdkConfig sdkConfig) {
Pair<InstrumentationConfiguration, SdkConfig> key = Pair.create(instrumentationConfig, sdkConfig);
SdkEnvironment sdkEnvironment = sdkToEnvironment.get(key);
if (sdkEnvironment == null) {
URL[] urls = dependencyResolver.getLocalArtifactUrls(
sdkConfig.getAndroidSdkDependency(),
sdkConfig.getCoreShadowsDependency());
ClassLoader robolectricClassLoader = new InstrumentingClassLoader(instrumentationConfig, urls);
sdkEnvironment = new SdkEnvironment(sdkConfig, robolectricClassLoader);
sdkToEnvironment.put(key, sdkEnvironment);
}
return sdkEnvironment;
}
}
实现SdkEnvironment的同时实现了一个InstrumentingClassLoader自定义类加载器,找到这个类加载器的实现部分,我们就会豁然开朗,看findclass方法
@Override
protected Class<?> findClass(final String className) throws ClassNotFoundException {
if (config.shouldAcquire(className)) {
final byte[] origClassBytes = getByteCode(className);
ClassNode classNode = new ClassNode(Opcodes.ASM4) {
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
desc = remapParamType(desc);
return super.visitField(access, name, desc, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, remapParams(desc), signature, exceptions);
return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
}
};
final ClassReader classReader = new ClassReader(origClassBytes);
classReader.accept(classNode, 0);
classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));
try {
byte[] bytes;
ClassInfo classInfo = new ClassInfo(className, classNode);
if (config.shouldInstrument(classInfo)) {
bytes = getInstrumentedBytes(classNode, config.containsStubs(classInfo));
} else {
bytes = origClassBytes;
}
ensurePackage(className);
return defineClass(className, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("couldn't load " + className, e);
} catch (OutOfMemoryError e) {
System.err.println("[ERROR] couldn't load " + className + " in " + this);
throw e;
}
} else {
throw new IllegalStateException("how did we get here? " + className);
}
}
config方法保存了哪些类我们需要模拟,哪些不需要关注,这些既包括我们指定的类,也包括框架默认的类。判断这些对象后通过ASM框架将这些需要模拟的类进行动态字节码修改,这样就实现了非模拟类和模拟类同时存在。
最后总结下Robolectric的总体运行步骤
1、在TestSuit中指定SdkConfig,同时在SdkConfig中指定自定义Shadow
2、运行测试用例,调用链调用到RobolectricTestRunner的run()方法
3、run()方法会通过@Test注解分析测试用力的个数,然后通过反射运行每一个测试用例
4、运行每个测试用例的时候,都会通过自定义类加载器加载测试用例所需要的方法
5、类加载器分析SdkConfig中的Shadow类,如果所要加载类为SdkConfig中指定的类,
则通过ASM动态修改字节码,使Shadow类的修改操作应用到实际类中。