溫馨提示:閱讀本文前最好簡單使用過 Robolectric浸踩。
Robolectric 是基于 Junit 的單元測試框架辕漂,實現(xiàn)了在 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
注解瓤球,其余實現(xiàn)和普通 Java 方法一致融欧。
運行方式也很簡單。如果使用的 Android Studio 的話卦羡,只需在 evaluatesExpression
方法上點擊右鍵噪馏,會彈出如下彈窗权她,然后點擊 "Run 'evaluatesExpression'",即可運行逝薪。
下面將分析 evaluatesExpression
方法是如何被調(diào)起的隅要。
大體上分三步:
1.查找并創(chuàng)建執(zhí)行主體(Runner)
2.找到具有 @Test
注解的單測方法
3.運行單測方法
1.查找執(zhí)行主體(Runner)
執(zhí)行主體為實現(xiàn)了 Runner
接口的對象。Runner
接口的核心方法為 run
方法董济,其中一個重要的子類為 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
對象時廓啊,在構(gòu)造方法里會傳入 CalculatorTest.Class
,然后利用反射封豪,查找標記有 @Test
注解的方法谴轮,并將這些方法保存起來。
protected void scanAnnotatedMembers() {
for (Class<?> eachClass : getSuperClasses(clazz)) {
for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {
addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);
}
}
}
3.運行單測方法
接下來最后一步吹埠,執(zhí)行 Runner
對象的 run
方法第步。run
方法對 classBlock
方法做了簡單的包裝,核心還是 classBlock
和methodBlock
方法缘琅。
簡化版 methodBlock
:
protected Statement methodBlock(FrameworkMethod method) { // FrameworkMethod 是對 Method 類的包裝
Object test = createTest() // 創(chuàng)建 CalculatorTest的實例粘都,實現(xiàn)代碼大概是:CalculatorTest.Class.newInstance()
Statement statement = methodInvoker(method, test); // 調(diào)用 method,實現(xiàn)代碼大概是:method.invoke(test, params)
return statement;
}
上述執(zhí)行流程為了突出核心流程做了大幅簡化刷袍,關(guān)心具體實現(xiàn)細節(jié)的可以查看源碼翩隧。
通過上述分析,我們了解了 Junit 框架的基本執(zhí)行流程呻纹。如果我們想以 Junit 為基礎(chǔ)實現(xiàn)自己的單元測試框架堆生,只需自定義 Runner
類即可。
三.Robolectric介紹
官方文檔:http://robolectric.org
github地址:https://github.com/robolectric/robolectric
Junit 屬于 JVM 平臺上的單元測試框架雷酪,無法提供 Android 運行時環(huán)境淑仆。如果在單元測試中涉及到 Android 特性,Junit 則無法實現(xiàn)太闺。
通常的做法是啟動 Android 模擬器進行測試糯景。但是在模擬器上運行測試用例是非常低效的,構(gòu)建省骂、安裝蟀淮、啟動,每個步驟都異常耗時钞澳,為了解決這一問題怠惶,Robolectric 通過 mock Android 運行時環(huán)境,使得單元測試可以在 JVM 環(huá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é)的重點是分析 Robolectric 如何 mock Android 運行時環(huán)境的。在此之前通惫,需要先了解下 Java 類加載器 和 ASM或者可以直接跳到 "Robolectric 的實現(xiàn)" 部分茂翔。
1.類加載器
虛擬機設(shè)計團隊把類加載階段中的 "通過一個類的全限定名來獲取描述此類的二進制字節(jié)流" 這個動作放到 Java 虛擬機外部去實現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類履腋。實現(xiàn)這個動作的代碼模塊稱為"類加載器"珊燎。
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性遵湖,每一個類加載器悔政,都擁有一個獨立的類名稱空間。
類加載器分為三種:
啟動類加載器
負責加載 <JAVA_HOME>/lib 目錄下的文件延旧。擴展類加載器
負責加載 <JAVA_HOME>/lib/ext 目錄下的文件谋国。應(yīng)用程序類加載器
也稱為系統(tǒng)類加載器。開發(fā)者可以直接使用這個類加載器迁沫,默認情況下芦瘾,應(yīng)用程序類都是由這個加載器加載。
如下是類加載器的繼承關(guān)系:
應(yīng)用程序類加載器和擴展類加載器的具體實現(xiàn)分別為
AppClassLoader
弯洗、ExtClassLoader
旅急。我們在自定義應(yīng)用程序類加載器時逢勾,可以直接繼承 UrlClassLoader
牡整。
2.ASM
官方文檔:https://asm.ow2.io/
ASM 是一個可以分析、操縱 Java 字節(jié)碼的工具溺拱,它可以以二進制形式修改或創(chuàng)建字節(jié)碼逃贝。ASM 的應(yīng)用范圍很廣泛,熱修復(fù)框架 Robust 就有使用其進行插樁迫摔。
3.Robolectric的實現(xiàn)
經(jīng)過前面做的大量鋪墊沐扳,事情逐漸變得明朗起來。
為了 mock Android 運行時環(huán)境句占,我們需要使用自定義 ClassLoader 加載如 Activity沪摄、Fragment 等類,然后在加載過程中使用 ASM 修改字節(jié)碼纱烘,將部分方法的實現(xiàn)替換杨拐。比如將 getTaskId
替換為如下實現(xiàn):
protected int getTaskId() {
return 0;
}
這里存在兩種替換方案:
1.靜態(tài)替換-直接替換掉 android.jar
2.動態(tài)替換-運行時按需替換
Robolectric 采用的是第二種方案。
實現(xiàn)過程分為兩步擂啥,以 Acivity 為例:
1)替換系統(tǒng)類加載器為自定義類加載器
Robolectric 自定義的類加載器為SandboxClassLoader
哄陶,其繼承自 URLClassLoader
。
在閱讀這部分代碼時我對如何替換做了兩個猜想:
- 直接替換系統(tǒng)類加載器
- 替換上下文類加載器
事實證明自己的猜想都是錯誤的哺壶,一是Java 并沒有提供替換系統(tǒng)類加載器的方法屋吨;二是替換上下文類加載器替換完成后蜒谤,需要顯示使用,否則依然采用的系統(tǒng)類加載器至扰。
那么該如何替換呢鳍徽?
經(jīng)過查閱資料和驗證,從調(diào)用方式上敢课,類加載器分為顯示調(diào)用和隱式調(diào)用兩種旬盯。
顯示調(diào)用是在類加載時直接指明 classLoader,比如下面:
Class.forName("Activity", true, MyClassLoader())
沒有指明類加載器時則為隱式調(diào)用翎猛。
隱式調(diào)用有一個重要特點胖翰,即類的所有引入類都會采用同一個類加載器。在下例中切厘,類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
)時疫稿,采用自定義類加載器即可培他。
接下來再回到 Robolectric。Robolectric 實現(xiàn)了自定義的 RobolectricTestRunner
遗座,其繼承關(guān)系如下所示:
Robolectric 在
SandboxTestRunner
的 methodBlock
方法中進行了類加載器的替換:
// 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
方法中猛遍,會去尋找相匹配的 Shadow 類,然后利用 ASM 工具号坡,在加載類時進行字節(jié)碼的動態(tài)替換懊烤。
除了預(yù)定義 Shadow 類,用戶也可以仿照 ShadowActivity
實現(xiàn)自定義 Shadow 類宽堆。
預(yù)定義 Shadow 類和自定義 Shadow 類 的查找方式不同腌紧,預(yù)定義 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 類進行關(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 的核心流程,至于實現(xiàn)細節(jié)籽慢,有興趣的可以通過源碼繼續(xù)鉆研浸遗。