1. 程式人生 > >Android 自動化測試 Espresso篇:非同步程式碼測試

Android 自動化測試 Espresso篇:非同步程式碼測試

前言

在我的上一篇文章 Android 自動化測試 Espresso篇:簡介&基礎使用 中,簡單闡述了Espresso的基本使用,以及為什麼我們要使用Espresso。

實際上,Espresso進行一個簡單的同步功能測試並不難,比如我們點選了一個Button,點選後改變對應某個TextView的內容,這很簡單。但實際正常開發中,這種簡單的邏輯測試是很少見的,相反,我們需要測試的是各種各樣的非同步測試,比如:

情景一:點選進入Activity,網路請求資料載入,成功後資料展示在介面上。
情景二:點選進入Activity,獲得快取,網路請求資料載入,成功後資料展示在介面上,處理快取。
情景N : ……

假設這樣一個簡單的網路請求測試:

 @Test
    public void testHttp() {
        //假設我們請求網路資料,如果請求網路資料成功,讓TextView顯示"網路請求成功"
        //同時ImageView從不可見變為可見。

        //如果我們直接檢查是不是請求到了資料
        onView(withId(R.id.textView)).check(matches(withText("網路請求成功!")));
        onView(withText(R.id.imageView)).check(matches(isDisplayed()
)
); }

如果我們直接測試,那麼很大概率會報錯,因為在我們要測試資料是否展示在UI上時,網路資料很有可能還沒有獲取到。

顯然這很難處理,因為我們不知道資料到底什麼時候才能獲取到,有同學抖了個機靈,說我們可以這樣:

 @Test
    public void testHttp() {
        // 我們一進來就先讓他等待5秒,等資料載入完畢再檢查UI
        Thread.sleep(5000);

        // 5秒結束,我們檢查是不是請求到了資料
        onView(withId(R.id.textView)).check(matches(withText("網路請求成功!"
)
)
); onView(withText(R.id.imageView)).check(matches(isDisplayed())); }

這樣可以實現嗎,確實很有可能通過,但是這種測試顯然屬於弊大於利,因為網路情況是在不斷變化的,也許0.5s就能獲取網路資料,也有可能數十秒後才能獲取,這樣前者導致我們浪費了4.5s的時間,後者在網路狀態屬於正常的時候測試結果失敗,這都是我們不願看到的結果。

我們更希望在獲取到網路資料之後,立即進行下一步的測試,如果網路請求需要0.5s,我們就等待0.5s之後測試UI,如果需要數十秒,我們就等數十秒。

今天我們結合谷歌官方的MVP模式下的todoApp demo,通過demo,我們學習和了解一下如何進行非同步測試。

筆者的Android單元測試相關係列:

一、依賴及API

我們在Module的build.gradle檔案中新增依賴:

final ESPRESSO_VERSION = '2.2.2'

dependencies {
    ...
    ...
    //Espresso依賴,AndroidStudio 2.2+之後自動依賴
    androidTestCompile("com.android.support.test.espresso:espresso-core:${ESPRESSO_VERSION}", {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    //Espresso的IdlingResource非同步介面依賴:
    compile("com.android.support.test.espresso:espresso-idling-resource:${ESPRESSO_VERSION}") {
        exclude module: 'support-annotations'
    }
    androidTestCompile("com.android.support.test.espresso:espresso-idling-resource:${ESPRESSO_VERSION}") {
        exclude module: 'support-annotations'
    }
    ...
    ...
}

我們想要對非同步程式碼進行單元測試,首先要了解IdlingResource這個介面,介面需要我們實現的方法如下:

public interface IdlingResource {
  /**
   * 用來標識 IdlingResource 名稱
   */
  public String getName();

  /**
   * 當前 IdlingResource 是否空閒 .
   */
  public boolean isIdleNow();

  /**
   註冊一個空閒狀態變換的ResourceCallback回撥
   */
  public void registerIdleTransitionCallback(ResourceCallback callback);

  /**
   * 通知Espresso當前IdlingResource狀態變換為空閒的回撥介面
   */
  public interface ResourceCallback {
    /**
     * 當前狀態轉變為空閒時,呼叫該方法告訴Espresso
     */
    public void onTransitionToIdle();
  }

}

對於這個介面,當初在網上看了不少的用法,也嘗試用了不少,感覺都不太好用,感覺特別麻煩!原因很簡單,我們一定要每次都定義這樣一個介面嗎?

後來花了點時間認真翻了翻Google的todoapp關於單元測試的處理,豁然開朗,畢竟是谷歌,程式碼思路非常清晰。

我們來看一看Google的工程師們是如何進行測試的:

二、Google程式碼:

1、首先自定義一個SimpleCountingIdlingResource

這個IdlingResource實現了IdlingResource介面:

public final class SimpleCountingIdlingResource implements IdlingResource {

    private final String mResourceName;

    //這個counter值就像一個標記,預設為0
    private final AtomicInteger counter = new AtomicInteger(0);

    private volatile ResourceCallback resourceCallback;

    public SimpleCountingIdlingResource(String resourceName) {
        mResourceName = resourceName;
    }

    @Override
    public String getName() {
        return mResourceName;
    }

    @Override
    public boolean isIdleNow() {
        return counter.get() == 0;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    //每當我們開始非同步請求,把counter值+1
    public void increment() {
        counter.getAndIncrement();
    }

    //當我們獲取到網路資料後,counter值-1;
    public void decrement() {
        int counterVal = counter.decrementAndGet();
        //如果這時counter == 0,說明非同步結束,執行回撥。
        if (counterVal == 0) {
            // 
            if (null != resourceCallback) {
                resourceCallback.onTransitionToIdle();
            }
        }

        if (counterVal < 0) {
            //如果小於0,丟擲異常
            throw new IllegalArgumentException("Counter has been corrupted!");
        }
    }
}

2、定義一個該IdlingResource的管理類EspressoIdlingResource:

我們實際上並不直接處理SimpleCountingIdlingResource,而是將其業務處理交給管理類去處理。

public class EspressoIdlingResource {

    private static final String RESOURCE = "GLOBAL";

    private static SimpleCountingIdlingResource mCountingIdlingResource =
            new SimpleCountingIdlingResource(RESOURCE);

    public static void increment() {
        mCountingIdlingResource.increment();
    }

    public static void decrement() {
        mCountingIdlingResource.decrement();
    }

    public static IdlingResource getIdlingResource() {
        return mCountingIdlingResource;
    }
}

3、Activity中如何使用:

在Activity中我們先新增這樣一個方法,這個方法是給測試時提供獲取IdlingResource的介面:

   @VisibleForTesting
   public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
   }

當我們開始非同步請求前,將IdlingResource的標記加一,這就意味著已經開始非同步了:

//在開始非同步請求前新增這行程式碼,意味著開始了非同步
EspressoIdlingResource.increment();

當請求結束,我們再新增這行程式碼,說明網路請求結束:

//圖片載入成功,結束非同步
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
 EspressoIdlingResource.decrement();
}

好的,我們已經在我們的程式碼中將需要做到的都做好了,接下來就是去AndroidTest中寫我們的測試程式碼了!

4、測試程式碼:

我們直接回到文章開始時的那個小案例:

public class A06AsyncActivity2Test {

    @Rule
    public ActivityTestRule<A06AsyncActivity2> activityRule = new ActivityTestRule<>(A06AsyncActivity2.class);

    private IdlingResource idlingresource;


    @Before
    public void setUp() throws Exception {
        //呼叫Activity中我們已經設定好的getIdlingresource()方法,獲取Idlingresource物件
        idlingresource = activityRule.getActivity().getIdlingresource();

        //去掉下行註釋,只有非同步結束後,才進行接下來的測試程式碼(tests passed)
        //註冊非同步監聽,當該idlingresource中的counter標記值為0時才進行接下來的測試程式碼
        Espresso.registerIdlingResources(idlingresource);
    }

    @Test
    public void onLoadingFinished() throws Exception {
        //  不再需要這樣的程式碼
        //  Thread.sleep(5000);

        // 未註冊idlingResource時,立即進行test,此時非同步並未結束,報錯(tests failed)
        onView(withId(R.id.text))
                .check(matches(withText("success!")));
    }

    @After
    public void release() throws Exception {
        //我們在測試結束後取消註冊,釋放資源
        Espresso.unregisterIdlingResources(idlingresource);
    }
}

Espresso.registerIdlingResources(idlingresource);

我們將上面這行程式碼分別註釋/開啟,進行測試,發現在沒有註冊IdlingResources時,進行測試會報錯,當註冊後,程式碼執行邏輯應該是:

1.Activity中開始非同步請求(IdResource.counter從0 -> 1),
2.非同步請求中…(這時監聽到的counter一直為1)
3.非同步請求結束(這時監聽到的counter 1 -> 0)
4.執行IdlingResources中執行 resourceCallback.onTransitionToIdle();
5.測試程式碼繼續進行測試

也就是說

Espresso.registerIdlingResources(idlingresource)

這行程式碼和之前的

Thread.sleep(5000);

這兩行程式碼都是一樣的效果,阻塞住當前的測試,只不過前者更優越的是能夠在非同步結束之後馬上執行接下來的測試程式碼,從效果上來說不知好了多少。

三、OkHttp非同步請求測試:

其實之前的SimpleCountingIdlingResource介面已經能夠滿足我們日常開發中的測試需求了,但是如果您所使用的網路請求庫剛好是OkHttp,那麼這裡有一個更簡單的實現方式,先看程式碼:

依賴:

    androidTestCompile('com.jakewharton.espresso:okhttp3-idling-resource:1.0.0') {
        exclude module: 'support-annotations'
        exclude module: 'okhttp'
    }

測試程式碼中使用:

@RunWith(AndroidJUnit4.class)
public class A07OkhttpActivityTest {

    @Rule
    public ActivityTestRule<A07OkhttpActivity> rule = new ActivityTestRule<>(A07OkhttpActivity.class);

    @Test
    public void requestHttpTest() throws Exception {
        //初始化
        OkHttp3IdlingResource idlingResource = OkHttp3IdlingResource.create("okhttp", OkHttpProvider.getOkHttpInstance());
        //註冊
        Espresso.registerIdlingResources(idlingResource);

        onView(withId(R.id.tv_name))
                .check(matches(withText("qingmei2")));

        //解除註冊
        Espresso.unregisterIdlingResources(idlingResource);
    }
}

OkHttpProvider是提供OkHttpClient物件的類:

public abstract class OkHttpProvider {

    private static OkHttpClient instance = null;

    public static OkHttpClient getOkHttpInstance() {
        if (instance == null) {
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY);

            instance = new OkHttpClient()
                    .newBuilder()
                    .addInterceptor(interceptor)
                    .build();
        }
        return instance;
    }
}

也就是說,我們並不需要去自己實現IdlingResource介面,已經有人幫我們封裝好了,我們直接拿去測試就好了。

有同學也許會說,這是誰封裝的,靠譜不啊。

作者就是GitHub上著名的開源狂魔 JakeWharton大神,關於他「開源」以及「參與開源貢獻大量程式碼」的專案有:

所以說放心去用,絕對靠譜。

不過經筆者嘗試,發現這個庫並不能直接配合Rx+Retrofit使用,略遺憾。

總結

學會了非同步測試,接下來就可以嘗試去寫測試程式碼了。

推薦一些好的文章,特別感謝,給我單元測試的學習提供了很大的幫助,再次真摯感謝!

本系列的所有程式碼都已託管GitHub

2018/8/3追加更新

關於UI自動化測試的如何實踐,我將自己寫的一個圖片選擇器 RxImagePicker 進行了自動化測試的全面覆蓋,希望能夠給嘗試Espresso的開發者一些經驗參考,其效果:

UI自動化測試效果

我需要做的就是一遍愜意喝茶,一遍等待自動化測試的結果,很快,我得到了下面的結果:

關於 RxImagePicker UI自動化測試的實踐歷程,請參考我的這篇文章,希望對一些想要學習Espresso的朋友有一定的幫助: