溫馨提示:閱讀本文前最好簡單使用過 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)行。

下面將分析 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 方法做了簡單的包裝,核心還是 classBlock和methodBlock 方法。
簡化版 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)系:

應(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)系如下所示:

Robolectric 在
SandboxTestRunner 的 methodBlock 方法中進(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;
}
}
簡單來說,在 SandboxClassLoader 的 findClass方法中,會(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ù)鉆研。