美文网首页
单元测试框架 Robolectric 原理分析

单元测试框架 Robolectric 原理分析

作者: 请输入妮称 | 来源:发表于2020-07-07 01:11 被阅读0次

    温馨提示:阅读本文前最好简单使用过 Robolectric。

    Robolectric 是基于 Junit 的单元测试框架,实现了在 JVM 上测试 Android 代码的功能。在介绍 Robolectric 前有必要先简单介绍下Junit。

    一.Junit介绍

    Junit 是 Java 语言的单元测试框架,理论上基于 JVM 的语言都可以使用。本文基于 Junit 4 的源码进行分析,目前最新版本为 Junit 5。

    二.Junit源码分析

    单元测试的用法很简单。下面以 Calculator 类为例,为其中的 evaluate 方法编写单元测试:

    import static org.junit.Assert.assertEquals;
    import org.junit.Test;
    
    @RunWith(BlockJUnit4ClassRunner.class)
    public class CalculatorTest {
      @Test
      public void evaluatesExpression() {
        Calculator calculator = new Calculator();
        int sum = calculator.evaluate("1+2+3");
        assertEquals(6, sum);
      }
    }
    

    可以看到除了 @RunWith(BlockJUnit4ClassRunner.class)@Test 注解,其余实现和普通 Java 方法一致。

    运行方式也很简单。如果使用的 Android Studio 的话,只需在 evaluatesExpression 方法上点击右键,会弹出如下弹窗,然后点击 "Run 'evaluatesExpression'",即可运行。

    截屏2020-07-04 下午11.52.17.png

    下面将分析 evaluatesExpression 方法是如何被调起的。

    大体上分三步:
    1.查找并创建执行主体(Runner)
    2.找到具有 @Test 注解的单测方法
    3.运行单测方法

    1.查找执行主体(Runner)

    执行主体为实现了 Runner 接口的对象。Runner 接口的核心方法为 run 方法,其中一个重要的子类为 ParentRunner

    查找 Runner 对象的核心代码在 AllDefaultPossibilitiesBuilder 类里,下面采用伪代码描述执行流程:

    // testClass = CalculatorTest.Class
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        if CalculatorTest 存在 @RunWith 注解
            根据注解内容创建 Runner(本例中即为 BlockJUnit4ClassRunner)
        else
            创建 BlockJUnit4ClassRunner
    }
    

    BlockJUnit4ClassRunner 属于 ParentRunner的子类。

    2.找到具有 @Test 注解的方法

    第一步创建 Runner 对象时,在构造方法里会传入 CalculatorTest.Class,然后利用反射,查找标记有 @Test 注解的方法,并将这些方法保存起来。

    protected void scanAnnotatedMembers() {
        for (Class<?> eachClass : getSuperClasses(clazz)) {
            for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {
                addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);
            }
        }
    }
    

    3.运行单测方法

    接下来最后一步,执行 Runner 对象的 run 方法。run 方法对 classBlock 方法做了简单的包装,核心还是 classBlockmethodBlock 方法。

    简化版 methodBlock

    protected Statement methodBlock(FrameworkMethod method) { // FrameworkMethod 是对 Method 类的包装
        Object test = createTest() // 创建 CalculatorTest的实例,实现代码大概是:CalculatorTest.Class.newInstance()
        Statement statement = methodInvoker(method, test); // 调用 method,实现代码大概是:method.invoke(test, params)
        return statement;
    }
    

    上述执行流程为了突出核心流程做了大幅简化,关心具体实现细节的可以查看源码。

    通过上述分析,我们了解了 Junit 框架的基本执行流程。如果我们想以 Junit 为基础实现自己的单元测试框架,只需自定义 Runner 类即可。

    三.Robolectric介绍

    官方文档:http://robolectric.org
    github地址:https://github.com/robolectric/robolectric

    Junit 属于 JVM 平台上的单元测试框架,无法提供 Android 运行时环境。如果在单元测试中涉及到 Android 特性,Junit 则无法实现。

    通常的做法是启动 Android 模拟器进行测试。但是在模拟器上运行测试用例是非常低效的,构建、安装、启动,每个步骤都异常耗时,为了解决这一问题,Robolectric 通过 mock Android 运行时环境,使得单元测试可以在 JVM 环境上运行。

    Robolectric 的使用方式如下:

    import static org.junit.Assert.assertEquals;
    import org.junit.Test;
    
    @RunWith(RobolectricTestRunner.class)
    public class CalculatorTest {
      @Test
      public void evaluatesExpression() {
        Calculator calculator = new Calculator();
        int sum = calculator.evaluate("1+2+3");
        assertEquals(6, sum);
      }
    }
    

    依然以 CalculatorTest 为例,只是将注解替换为了 @RunWith(RobolectricTestRunner.class)

    四.Robolectric源码分析

    本节的重点是分析 Robolectric 如何 mock Android 运行时环境的。在此之前,需要先了解下 Java 类加载器 和 ASM或者可以直接跳到 "Robolectric 的实现" 部分。

    1.类加载器

    虚拟机设计团队把类加载阶段中的 "通过一个类的全限定名来获取描述此类的二进制字节流" 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为"类加载器"。

    对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

    类加载器分为三种:

    • 启动类加载器
      负责加载 <JAVA_HOME>/lib 目录下的文件。

    • 扩展类加载器
      负责加载 <JAVA_HOME>/lib/ext 目录下的文件。

    • 应用程序类加载器
      也称为系统类加载器。开发者可以直接使用这个类加载器,默认情况下,应用程序类都是由这个加载器加载。

    如下是类加载器的继承关系:

    截屏2020-07-05 下午10.15.29.png
    应用程序类加载器和扩展类加载器的具体实现分别为 AppClassLoaderExtClassLoader 。我们在自定义应用程序类加载器时,可以直接继承 UrlClassLoader

    2.ASM

    官方文档:https://asm.ow2.io/

    ASM 是一个可以分析、操纵 Java 字节码的工具,它可以以二进制形式修改或创建字节码。ASM 的应用范围很广泛,热修复框架 Robust 就有使用其进行插桩。

    3.Robolectric的实现

    经过前面做的大量铺垫,事情逐渐变得明朗起来。

    为了 mock Android 运行时环境,我们需要使用自定义 ClassLoader 加载如 Activity、Fragment 等类,然后在加载过程中使用 ASM 修改字节码,将部分方法的实现替换。比如将 getTaskId 替换为如下实现:

    protected int getTaskId() {
      return 0;
    }
    

    这里存在两种替换方案:
    1.静态替换-直接替换掉 android.jar
    2.动态替换-运行时按需替换
    Robolectric 采用的是第二种方案。

    实现过程分为两步,以 Acivity 为例:

    1)替换系统类加载器为自定义类加载器

    Robolectric 自定义的类加载器为SandboxClassLoader ,其继承自 URLClassLoader

    在阅读这部分代码时我对如何替换做了两个猜想:

    • 直接替换系统类加载器
    • 替换上下文类加载器

    事实证明自己的猜想都是错误的,一是Java 并没有提供替换系统类加载器的方法;二是替换上下文类加载器替换完成后,需要显示使用,否则依然采用的系统类加载器。

    那么该如何替换呢?
    经过查阅资料和验证,从调用方式上,类加载器分为显示调用和隐式调用两种。
    显示调用是在类加载时直接指明 classLoader,比如下面:

    Class.forName("Activity", true, MyClassLoader())
    

    没有指明类加载器时则为隐式调用。

    隐式调用有一个重要特点,即类的所有引入类都会采用同一个类加载器。在下例中,类A 采用 MyClassLoader 加载,那么类 B 使用的也是 MyClassLoader

    public class A {
        public A() {
            System.out.println(getClass().getClassLoader());
            System.out.println(B.class.getClassLoader());
        }
    }
    
    public class Main {
        public static void main(String[] args) throws Exception{
            Class.forName("A", true, new MyClassLoader()).newInstance();
        }
    }
    

    输出结果为:
    MyClassLoader@355da254
    MyClassLoader@355da254

    因此,只需在加载单测类(上例中的 CalculatorTest)时,采用自定义类加载器即可。
    接下来再回到 Robolectric。Robolectric 实现了自定义的 RobolectricTestRunner ,其继承关系如下所示:

    截屏2020-07-05 下午10.27.25.png
    Robolectric 在 SandboxTestRunnermethodBlock 方法中进行了类加载器的替换:
    // getTestClass().getJavaClass() 作用是获取 CalculatorTest 的 Class 对象
    Class bootstrappedTestClass = bootstrappedClass(getTestClass().getJavaClass());
    
    public <T> Class<T> bootstrappedClass(Class<?> clazz) {
        try {
        return (Class<T>) sandboxClassLoader.loadClass(clazz.getName());
        } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
        }
    }
    

    2)查找 Acivity 类的替换类

    Robolectric 在 org.robolectric.shadows 包中预定义了许多 Shadow 开头的类,比如 ShadowActivityShadowTextView

    @Implements(Activity.class)
    public class ShadowActivity extends ShadowContextThemeWrapper {
      // 省略了其他大部分内容
        @Implementation
      protected int getTaskId() {
        return 0;
      }
    }
    

    简单来说,在 SandboxClassLoaderfindClass方法中,会去寻找相匹配的 Shadow 类,然后利用 ASM 工具,在加载类时进行字节码的动态替换。

    除了预定义 Shadow 类,用户也可以仿照 ShadowActivity 实现自定义 Shadow 类。

    预定义 Shadow 类和自定义 Shadow 类 的查找方式不同,预定义 Shadow 类在初始化时,将其存储在了 Map 中:

    public class Shadows implements ShadowProvider {
      private static final Map<String, String> SHADOW_MAP = new HashMap<>(391);
    
      static {
        SHADOW_MAP.put("android.widget.AbsListView", "org.robolectric.shadows.ShadowAbsListView");
        SHADOW_MAP.put("android.widget.AbsSeekBar", "org.robolectric.shadows.ShadowAbsSeekBar");
        SHADOW_MAP.put("android.widget.AbsSpinner", "org.robolectric.shadows.ShadowAbsSpinner");
        SHADOW_MAP.put("android.database.AbstractCursor", "org.robolectric.shadows.ShadowAbstractCursor");
        SHADOW_MAP.put("android.accessibilityservice.AccessibilityButtonController", "org.robolectric.shadows.ShadowAccessibilityButtonController");
        SHADOW_MAP.put("android.view.accessibility.AccessibilityManager", "org.robolectric.shadows.ShadowAccessibilityManager");
        SHADOW_MAP.put("android.view.accessibility.AccessibilityNodeInfo", "org.robolectric.shadows.ShadowAccessibilityNodeInfo");
        SHADOW_MAP.put("android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction", "org.robolectric.shadows.ShadowAccessibilityNodeInfo$ShadowAccessibilityAction");
        SHADOW_MAP.put("android.view.accessibility.AccessibilityRecord", "org.robolectric.shadows.ShadowAccessibilityRecord");
        SHADOW_MAP.put("android.accessibilityservice.AccessibilityService", "org.robolectric.shadows.ShadowAccessibilityService");
        SHADOW_MAP.put("android.view.accessibility.AccessibilityWindowInfo", "org.robolectric.shadows.ShadowAccessibilityWindowInfo");
        ......
    

    自定义 Shadow 类需要在 @Config 注解中显示声明,这样可以通过读取注解中的 shadows 值 ,将原类和 Shadow 类进行关联:

    import static org.junit.Assert.assertEquals;
    import org.junit.Test;
    
    @Config(shadows = {MyShadowTextView.class})
    @RunWith(RobolectricTestRunner.class)
    public class CalculatorTest {
      @Test
      public void evaluatesExpression() {
        Calculator calculator = new Calculator();
        int sum = calculator.evaluate("1+2+3");
        assertEquals(6, sum);
      }
    }
    

    总结:
    本文只简单说明了 Robolectric 的核心流程,至于实现细节,有兴趣的可以通过源码继续钻研。

    相关文章

      网友评论

          本文标题:单元测试框架 Robolectric 原理分析

          本文链接:https://www.haomeiwen.com/subject/byznqktx.html