單元測試框架 Robolectric 原理分析

溫馨提示:閱讀本文前最好簡單使用過 Robolectric。

Robolectric 是基于 Junit 的單元測試框架,實(shí)現(xiàn)了在 JVM 上測試 Android 代碼的功能。在介紹 Robolectric 前有必要先簡單介紹下Junit。

一.Junit介紹

Junit 是 Java 語言的單元測試框架,理論上基于 JVM 的語言都可以使用。本文基于 Junit 4 的源碼進(jìn)行分析,目前最新版本為 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 注解,其余實(shí)現(xiàn)和普通 Java 方法一致。

運(yùn)行方式也很簡單。如果使用的 Android Studio 的話,只需在 evaluatesExpression 方法上點(diǎn)擊右鍵,會(huì)彈出如下彈窗,然后點(diǎn)擊 "Run 'evaluatesExpression'",即可運(yùn)行。

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

下面將分析 evaluatesExpression 方法是如何被調(diào)起的。

大體上分三步:
1.查找并創(chuàng)建執(zhí)行主體(Runner)
2.找到具有 @Test 注解的單測方法
3.運(yùn)行單測方法

1.查找執(zhí)行主體(Runner)

執(zhí)行主體為實(shí)現(xiàn)了 Runner 接口的對象。Runner 接口的核心方法為 run 方法,其中一個(gè)重要的子類為 ParentRunner。

查找 Runner 對象的核心代碼在 AllDefaultPossibilitiesBuilder 類里,下面采用偽代碼描述執(zhí)行流程:

// testClass = CalculatorTest.Class
public Runner runnerForClass(Class<?> testClass) throws Throwable {
    if CalculatorTest 存在 @RunWith 注解
        根據(jù)注解內(nèi)容創(chuàng)建 Runner(本例中即為 BlockJUnit4ClassRunner)
    else
        創(chuàng)建 BlockJUnit4ClassRunner
}

BlockJUnit4ClassRunner 屬于 ParentRunner的子類。

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

第一步創(chuàng)建 Runner 對象時(shí),在構(gòu)造方法里會(huì)傳入 CalculatorTest.Class,然后利用反射,查找標(biāo)記有 @Test 注解的方法,并將這些方法保存起來。

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

3.運(yùn)行單測方法

接下來最后一步,執(zhí)行 Runner 對象的 run 方法。run 方法對 classBlock 方法做了簡單的包裝,核心還是 classBlockmethodBlock 方法。

簡化版 methodBlock

protected Statement methodBlock(FrameworkMethod method) { // FrameworkMethod 是對 Method 類的包裝
    Object test = createTest() // 創(chuàng)建 CalculatorTest的實(shí)例,實(shí)現(xiàn)代碼大概是:CalculatorTest.Class.newInstance()
    Statement statement = methodInvoker(method, test); // 調(diào)用 method,實(shí)現(xiàn)代碼大概是:method.invoke(test, params)
    return statement;
}

上述執(zhí)行流程為了突出核心流程做了大幅簡化,關(guān)心具體實(shí)現(xiàn)細(xì)節(jié)的可以查看源碼。

通過上述分析,我們了解了 Junit 框架的基本執(zhí)行流程。如果我們想以 Junit 為基礎(chǔ)實(shí)現(xiàn)自己的單元測試框架,只需自定義 Runner 類即可。

三.Robolectric介紹

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

Junit 屬于 JVM 平臺(tái)上的單元測試框架,無法提供 Android 運(yùn)行時(shí)環(huán)境。如果在單元測試中涉及到 Android 特性,Junit 則無法實(shí)現(xiàn)。

通常的做法是啟動(dòng) Android 模擬器進(jìn)行測試。但是在模擬器上運(yùn)行測試用例是非常低效的,構(gòu)建、安裝、啟動(dòng),每個(gè)步驟都異常耗時(shí),為了解決這一問題,Robolectric 通過 mock Android 運(yùn)行時(shí)環(huán)境,使得單元測試可以在 JVM 環(huán)境上運(yùn)行。

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源碼分析

本節(jié)的重點(diǎn)是分析 Robolectric 如何 mock Android 運(yùn)行時(shí)環(huán)境的。在此之前,需要先了解下 Java 類加載器 和 ASM或者可以直接跳到 "Robolectric 的實(shí)現(xiàn)" 部分。

1.類加載器

虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的 "通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流" 這個(gè)動(dòng)作放到 Java 虛擬機(jī)外部去實(shí)現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為"類加載器"。

對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在 Java 虛擬機(jī)中的唯一性,每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。

類加載器分為三種:

  • 啟動(dòng)類加載器
    負(fù)責(zé)加載 <JAVA_HOME>/lib 目錄下的文件。

  • 擴(kuò)展類加載器
    負(fù)責(zé)加載 <JAVA_HOME>/lib/ext 目錄下的文件。

  • 應(yīng)用程序類加載器
    也稱為系統(tǒng)類加載器。開發(fā)者可以直接使用這個(gè)類加載器,默認(rèn)情況下,應(yīng)用程序類都是由這個(gè)加載器加載。

如下是類加載器的繼承關(guān)系:

截屏2020-07-05 下午10.15.29.png

應(yīng)用程序類加載器和擴(kuò)展類加載器的具體實(shí)現(xiàn)分別為 AppClassLoader 、ExtClassLoader 。我們在自定義應(yīng)用程序類加載器時(shí),可以直接繼承 UrlClassLoader

2.ASM

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

ASM 是一個(gè)可以分析、操縱 Java 字節(jié)碼的工具,它可以以二進(jìn)制形式修改或創(chuàng)建字節(jié)碼。ASM 的應(yīng)用范圍很廣泛,熱修復(fù)框架 Robust 就有使用其進(jìn)行插樁。

3.Robolectric的實(shí)現(xiàn)

經(jīng)過前面做的大量鋪墊,事情逐漸變得明朗起來。

為了 mock Android 運(yùn)行時(shí)環(huán)境,我們需要使用自定義 ClassLoader 加載如 Activity、Fragment 等類,然后在加載過程中使用 ASM 修改字節(jié)碼,將部分方法的實(shí)現(xiàn)替換。比如將 getTaskId 替換為如下實(shí)現(xiàn):

protected int getTaskId() {
  return 0;
}

這里存在兩種替換方案:
1.靜態(tài)替換-直接替換掉 android.jar
2.動(dòng)態(tài)替換-運(yùn)行時(shí)按需替換
Robolectric 采用的是第二種方案。

實(shí)現(xiàn)過程分為兩步,以 Acivity 為例:

1)替換系統(tǒng)類加載器為自定義類加載器

Robolectric 自定義的類加載器為SandboxClassLoader ,其繼承自 URLClassLoader

在閱讀這部分代碼時(shí)我對如何替換做了兩個(gè)猜想:

  • 直接替換系統(tǒng)類加載器
  • 替換上下文類加載器

事實(shí)證明自己的猜想都是錯(cuò)誤的,一是Java 并沒有提供替換系統(tǒng)類加載器的方法;二是替換上下文類加載器替換完成后,需要顯示使用,否則依然采用的系統(tǒng)類加載器。

那么該如何替換呢?
經(jīng)過查閱資料和驗(yàn)證,從調(diào)用方式上,類加載器分為顯示調(diào)用和隱式調(diào)用兩種。
顯示調(diào)用是在類加載時(shí)直接指明 classLoader,比如下面:

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

沒有指明類加載器時(shí)則為隱式調(diào)用。

隱式調(diào)用有一個(gè)重要特點(diǎn),即類的所有引入類都會(huì)采用同一個(gè)類加載器。在下例中,類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();
    }
}

輸出結(jié)果為:
MyClassLoader@355da254
MyClassLoader@355da254

因此,只需在加載單測類(上例中的 CalculatorTest)時(shí),采用自定義類加載器即可。
接下來再回到 Robolectric。Robolectric 實(shí)現(xiàn)了自定義的 RobolectricTestRunner ,其繼承關(guān)系如下所示:

截屏2020-07-05 下午10.27.25.png

Robolectric 在 SandboxTestRunnermethodBlock 方法中進(jìn)行了類加載器的替換:

// 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 包中預(yù)定義了許多 Shadow 開頭的類,比如 ShadowActivity、ShadowTextView

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

簡單來說,在 SandboxClassLoaderfindClass方法中,會(huì)去尋找相匹配的 Shadow 類,然后利用 ASM 工具,在加載類時(shí)進(jìn)行字節(jié)碼的動(dòng)態(tài)替換。

除了預(yù)定義 Shadow 類,用戶也可以仿照 ShadowActivity 實(shí)現(xiàn)自定義 Shadow 類。

預(yù)定義 Shadow 類和自定義 Shadow 類 的查找方式不同,預(yù)定義 Shadow 類在初始化時(shí),將其存儲(chǔ)在了 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 類進(jìn)行關(guān)聯(lián):

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);
  }
}

總結(jié):
本文只簡單說明了 Robolectric 的核心流程,至于實(shí)現(xiàn)細(xì)節(jié),有興趣的可以通過源碼繼續(xù)鉆研。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容