iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
1
Mobile Development

從0開始,全方面自動化測試Android App系列 第 21

[Day 21] Android Espresso 處理非同步呼叫

大家在測試Application的時候一定都有這個經驗,如果我們今天是選擇直接連Testing Server的話,如果有非同步的task測試很容易就失敗了,其它實際例子還有Image Processing,Database Initialization等等,用[Day 20]範例來說Espresso測試程序已經跑到onView(withId(id)).check(matches(some condition))的時候結果非同步的callback還沒回來Espresso就會回傳無法matches指定情況的錯誤訊息,這時候通常大家都會用大絕招。Thread.sleep(millisecs)的方式來強迫Intrumentation Test Thread停下來等UI Thread,直到UI Thread做完callback再執行測試程式判斷的程式碼。這個做法可以暫時解決問題但是並不好,設定一個常數時間會有兩種問題

  • sleep time設太長,測試多花很多無謂的時間才完成,例如本來10分鐘完成變成做20分鐘
  • sleep time設太短,一樣失敗沒效果還浪費設定的等待時間

https://ithelp.ithome.com.tw/upload/images/20191006/20120975Vtu49kDfTO.png

例如我們一樣用[Day 20]裡的的範例繼續延伸,一樣測試MainActivity裡的TextView,這裡我們預期會收到false的字串(這裡不用MockWebServer的假response,直接測試server連線部份)。測試程式因為不確定Server連線的callback何時會好所以sleep 10秒鐘來等待。

    @Test
   fun testAsynchronous() {
        Thread.sleep(100000)
        onView(withId(R.id.textView)).check(matches(withText("false")))
    }

當然我們要選擇聰明一點的方法不用選擇上述的笨方法,Espresso有提供一個非常方便的類別叫做IdlingResource,實作的細節類似鎖的概念,在執行時把Intrumentation Test Thread鎖住,到指定的時機點再把它釋放,其中有針對不同狀況提供不同實作的IdlingResource,但我們這裡舉例最常用的CountingIdlingResource,先加入build.gradle的dependency。

    implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'

我們先用一個object的類別來實作一個CountingIdlingResource的實體做為全域變數,我們一個測試期間只用一個IdlingResource,在Java中可用static class取代。其中傳入的參數是用來trace log用,可以選擇任意字串。

object Idling {
    val idlingResource = CountingIdlingResource("test")
}

CountingIdlingResource是利用計數器的方式來做lock跟unlock的動作,計數器大於1的時候,Instrumentation Test Thread被鎖住,計數器等於0的時候Test Thread被釋放執行。

  • increment() 用來把鎖計數器 + 1
  • decrement() 用來把鎖計數器 - 1

IdlingResource要被實作在production code裡,因為我們要鎖的是production code。
然後在Test時才會跟Instrumentation Test Thread註冊這個idlingResource,所以不會影響正常production code執行。

因此在ServerHelper的類別裡,我們在發出http reqeust前把Thread鎖住,等到callback回來的時候再把Thread釋放。

class ServerHelper() {
   
   val sampleUrl = "http://dummy.restapiexample.com/api/v1/employee/1"
   
   fun request(callback: (response: String) -> Unit) {
        //開始http request前計數器+1鎖住testing thread
        Idling.idlingResource.increment()
        HandlerThread("demo").apply {
            start()
            Handler(looper).post {
                val client = OkHttpClient()
                val request = Request.Builder()
                    .url(sampleUrl)
                    .build()
                try {
                    val response = client.newCall(request).execute()
                    val responseString = response.body?.string()
                    responseString?.let {
                        Handler(Looper.getMainLooper()).post {
                            callback(it)
                            //callback執行完成,計數器-1變成0後釋放testing thread
                            Idling.idlingResource.decrement()
                        }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
    }
}

在我們Production code設置好後我們來實作Testing code,我們命名一個testAsynchronous的function,在測試非同步前我們要先透過Espresso的IdlingRegistry註冊我們剛剛在prodcution code寫的IdlingResource,記得測試完後要release這個註冊實體。

當我們launch這個測試的時候,Espresso就會暫時停止測試等到callback回來才繼續下一步判斷我們matches裡的Assertion。

    @Test
    fun testAsynchronous() {
        //註冊IdlingResource   
        IdlingRegistry.getInstance().register(Idling.idlingResource)
        onView(withId(R.id.textView)).check(matches(withText("false")))
        //釋放IdlingResource
        IdlingRegistry.getInstance().unregister(Idling.idlingResource)
    }

上一篇
[Day 20] Mock Server's Response
下一篇
[Day 22] Integration Automation之前的注意事項
系列文
從0開始,全方面自動化測試Android App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言