iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 25
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 25

[Day 25] Test:Part 7 Activity

  • 分享至 

  • xImage
  •  

Activity 的測試基本上寫法與 Fragment 相似,但要完成 Activity 的測試會馬上面臨一些問題:

  • Espresso 對 Dagger 的支援還不完整,所以要建立一個假的 Activity
  • 如果你的 Activity 有 Animation ,執行 Espresso 時有可能會 Time out ,因此必須要到開發人員選項關閉動畫
  • Espresso 在線程方面只有對 AsyncTask 支援的比較完善,所以在自定義的 Background thread 執行的程式 (Executor、RxJava、Coroutines) ,必須要想辦法把程式切換成同步執行
  • 有些像是 DataBinding 的 library 有一套自己的更新資料的機制(Choreographer) ,也要額外處理。

如果不處理這些問題,很大機率會碰上 NoMatchingViewException ,其原因就是上面幾點造成 Espresso 來不及更新 UI 導致。

因此 Espresso 提供 Idling Resources通知 Espresso 當前 App 是否處於忙碌中

關於 Idling Resources 不會在今天介紹太多,可能會在後面還有多餘的時間時才會深入探討。由於 Activity 的測試需要與多個頁面連動,在還沒有把程式寫完的情況下我今天會以寫範例的性質寫測試。

建立測試

基本上這個階段與建立 FragmentTest 時雷同,不過還要再加入 Idling Resources。

先創建一個基本的 Sample IdlingResource :


class SimpleIdlingResource(private val resourceName: String) : IdlingResource {

    private val isIdle = AtomicBoolean(true)

    @Volatile private var resourceCallback: IdlingResource.ResourceCallback? = null

    override fun getName(): String = resourceName

    override fun isIdleNow(): Boolean = isIdle.get()

    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
        this.resourceCallback = resourceCallback
    }

    fun setIdle(isIdleNow: Boolean) {
        if (isIdleNow == isIdle.get()) return
        isIdle.set(isIdleNow)
        if (isIdleNow) {
            resourceCallback?.onTransitionToIdle()
        }
    }
}

object EspressoIdlingResource {

    private const val RESOURCE = "GLOBAL"

    @JvmField
    val idlingResource = SimpleIdlingResource(RESOURCE)

    fun busy() {
        idlingResource.setIdle(true)
    }

    fun idle() {
        if (!idlingResource.isIdleNow) {
            idlingResource.setIdle(false)
        }
    }
}

Idling Resources 需要再測試的 @Before 及 @After 進行 register & unregister :

@RunWith(AndroidJUnit4::class)
@LargeTest
class TasksActivityTest {

    private lateinit var repository: ITasksRepository

    private val dataBindingIdlingResource = DataBindingIdlingResource()

    @get:Rule
    val rule = DaggerTestApplicationRule()

    @Before
    fun setupDaggerComponent() {
        repository = rule.component.tasksRepository
        repository.deleteAllTasksBlocking()
    }

    @Before
    fun registerIdlingResource() {
        IdlingRegistry.getInstance()
            .register(EspressoIdlingResource.idlingResource)
    }

    @After
    fun unregisterIdlingResource() {
        IdlingRegistry.getInstance()
            .unregister(EspressoIdlingResource.idlingResource)
    }
}

在實際的程式碼裡加入 Idling Resource 告知當前忙碌與閒置:

fun View.showSnackbar(snackbarText: String, timeLength: Int) {
    Snackbar.make(this, snackbarText, timeLength).run {
        addCallback(object: Snackbar.Callback() {
            override fun onShown(sb: Snackbar?) {
                EspressoIdlingResource.busy()
            }

            override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
                EspressoIdlingResource.idle()
            }
        })
        show()
    }
}

然後就可以寫一個測試檢查看看 Snackbar 了,有人有興趣的話可以看看拿掉 Idling Resource 後測試的結果如何。

討論

這裏我拿 Snackbar 當例子,絕對不是因為 codelab 也有寫,是因為如果要測 Snackbar 上的文字及顯示與否其實不太好測試。

首先, Snackbar 本身有顯示/隱藏的動畫,然後如果顯示時間設定為 LENGTH_SHORT,則 Snackbar 會這樣執行:

顯示 -> 出現 2 秒 -> 隱藏

這裡有個問題, Espresso 預設最多 delay 15 毫秒,如果沒有 Idling Resource 告知 Espresso 當前 App 忙碌中,則 Espresso 會馬上跳過 Snackbar 進入下一步測試,理所當然所期望的下個階段的 UI 還沒出現,程式就報錯了。

那現在有了 Idling Resource , Espresso 會在 Snackbar 出現時等待,直到他消失後恢復狀態才進入下個階段的測試。

雖然今天還沒辦法把 Activity 完全做完,不過因為 UI 已經拉好了,我可以完成 Navigatioin Drawer 的測試,在最後附上。


上一篇
[Day 24] Test:Part 6 Fragment
下一篇
[Day 26] Test:Part 8 Navigation Test
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言