Android單元測試框架源碼分析(二)淺析Robolectric

    在上一章中,我們簡單分析了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類的修改操作應用到實際類中。

   

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