Android單元測試-Robolectric 淺析

介紹

Robolectric 測試框架針對 Android 的組件(包含各種View)進行了統一的 Shadow ,使得我們不再依賴模擬器或真機,直接就單元測試就可方便地測試我們的 UI。

引入

testCompile "org.robolectric:robolectric:3.1.1"

使用

1.通用 Demo 示例

這里先來一個簡單的 Demo, 也是我們經常使用的形式:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class RobolectricTestMainActivity {

@Test
public void test() {
    Activity activity = Robolectric.setupActivity(TestMainActivity.class);

    ShadowActivity shadowActivity = Shadows.shadowOf(activity);

    Button button = (Button) activity.findViewById(R.id.btn_test_main);
    TextView textView = (TextView) activity.findViewById(R.id.tv_test_main);

    button.performClick();
    assertThat(textView.getText().toString(), equalTo("Hello"));

    Intent intent = new Intent(activity, TestToastActivity.class);
    activity.startActivity(intent);
    assertThat(shadowActivity.getNextStartedActivity(), equalTo(intent));
}

}</pre>

在真實的 TestMainActivity 中,存在一個按鈕和一個文本框,當點擊按鈕之后,將文本框的內容修改為 “hello”。當我們通過 Robolectric 的 setupActivity 構造出來一個 Activity 之后,對其進行操作并驗證,完全符合我們的預期結果。

另外,在上面的示例中,針對 Shadow 的使用,我們通過真實的 startActivity 方法啟動下一個 Activity 。若此時,我們需要驗證其是否啟動成功,就可以使用其對應的 ShadowActivity 。在拿到 ShadowActivity 之后,通過獲取其 getNextStartedActivity ,就可驗證其是否啟動成功。

2.Custom Shadow 的使用

初次接觸這個 Shadow 可能有些困惑,我們在 Robolectric 給我們提供的 Shadows 類中,可以發現其已經有很多的 Shadow 實現,其以一個 map 的格式存儲真實類跟 shadow 類對應的關系:

private static final Map<String, String> SHADOW_MAP = new HashMap<>(250);

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.widget.AbsoluteLayout", "org.robolectric.shadows.ShadowAbsoluteLayout"); SHADOW_MAP.put("android.widget.AbsoluteLayout.LayoutParams", "org.robolectric.shadows.ShadowAbsoluteLayout$ShadowLayoutParams"); SHADOW_MAP.put("android.database.AbstractCursor", "org.robolectric.shadows.ShadowAbstractCursor"); ** 省略 }</pre>

這里,大概就可以獲悉其的實現方法,通過 Shadow 類來替換其對應的真實方法的實現,最終達到的目的就會使我們的測試脫離一些底層的具體實現,來達到我們最快測試的目的。

若是大家感興趣的話,可以具體查看相應組件類的 Shadow 實現。當然,這里我們也可以自定義 Shadow ,來滿足定制化的需求,這里來個很簡單的實現:

  • 定義 Shadow 類

@Implements(Toast.class)
public class CustomShadowToast {

private static boolean mIsShown;

public void __constructor__(Context context) {
}

@Implementation
public void show() {
    mIsShown = true;
}

public static boolean isToastShowInvoked() {
    return mIsShown;
}

}</pre>

這里以 Toast 為例,只對其 show 方法做以實現,當調用了 show 方法之后,我們將一靜態變量 mIsShown 標記為 true,通過 isToastShowInvoked 方法來進行判斷其是否調用。

需要注意的三點:@Implements 注解指定需要對哪個類進行 shadow;@Implementation 指定需要對哪個方法進行替換;構造器需要通過 _ constructor_ 來編寫。

  • 測試調用

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows = { CustomShadowToast.class })
public class CustomShadowTest {

@Test
public void testToast() {
    Activity activity = Robolectric.setupActivity(TestToastActivity.class);

    Button button = (Button) activity.findViewById(R.id.btn_test_main);
    button.performClick();

    assertThat(CustomShadowToast.isToastShowInvoked(), is(true));
    assertThat(shadowOf(RuntimeEnvironment.application).getShownToasts().size() == 0, is(true));
}

}</pre>

這里要注意的是在 Config 注解中添加我們的 Shadow 類。在 TestToastActivity 類中,通過 button 的點擊,來隨意顯示一個 Toast ,我們是可以發現自定義 CustomShadowToast 的靜態變量確實是調用了。

不過第二個 assertThat 方法對顯示的 toast 數目做判斷,卻發現個數為零。這 shownToasts 數目的改變,是在 ShadowToast 類中,進行添加的,可看代碼:

@Implementation
public void show() {
 shadowOf(RuntimeEnvironment.application).getShownToasts().add(toast);
}

因為 ShadowToast 類中也對 show 方法做了實現,但是其卻被我們自定義實現給替換掉了。所以我們在自定義 Shadow 實現的時候,需要對這一點謹慎一二。

另外,我們也有在自定義 Shadow 的時候,需要持有真實類的引用,可以直接使用 RealObject 注解,就像 ShadowToast 一樣:

@Implements(Toast.class)
public class ShadowToast {

// 省略

@RealObject Toast toast; }</pre>

淺析

相信大家也是同我一樣會對這里的 Shadow 實現頗感興趣的。問題是 Shadow 類是如何跟真實的類掛上關系的?我們在針對真實類方法的調用,最后卻調用的是 Shadow 類里面的方法。

以第一個 Demo 中的 ShadowActivity 的獲取為例,查看 shadowOf 方法:

public static ShadowActivity shadowOf(Activity actual) {
 return (ShadowActivity) ShadowExtractor.extract(actual);
}

進而再看 ShadowExtractor :

public class ShadowExtractor {
  public static Object extract(Object instance) {
    return ((ShadowedObject) instance).$$robo$getData();
  }
}

而其中的 ShadowedObject 就是一個很簡單的接口:

public interface ShadowedObject {
  Object $$robo$getData();
}

由此可知,我們的 Activity 對象 actual 其實已經實現了 ShadowedObject 接口。這個就比較吊了啊,這里代碼查看到頭,再追溯 Activity 是如何構造的,發現并無什么特別的地方。那最后只剩 @RunWith 注解的參數 RobolectricTestRunner 類了,在 runChild 方法中,發現構造 SdkEnvironment 中 InstrumentingClassLoader 的身影,細看這個類,發現應該就是它完成了我們所需要的功能。

首先,它繼承了 ClassLoader ,它在 loadClass 中進行了重寫,對由需要由自己進行特殊加載的類,執行 findClass 的方法,否則用父類的 loadClass 方法。

在 findClass 中,其使用了 ASM 這個字節碼修改庫,來對我們需要修改的類的字節碼做修改,使其與我們的 shadow 相綁定。最可證明的就是其中的這段代碼:

classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));

通過 ASM 的 ClassNode 對象添加了 ShadowedObject 的接口,與我們之前看到的相吻合。但是類方法是如何替換的,這里的代碼就看的是一頭霧水了。這里先留一個坑,以后理解了 Java 的字節碼,再來填這個坑。若是有小伙伴對這里也有興趣,可加 QQ 群:289926871 一起交流。

參考資料

 

來自:http://www.jianshu.com/p/509d55926033

 

 本文由用戶 zhongxunji 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!