如何正確使用Espresso來測試你的Android程式

来源:https://www.cnblogs.com/mengdd/archive/2018/09/30/how-to-use-espresso-on-android-correctly.html
-Advertisement-
Play Games

本文分為三部分, 第一部分簡單介紹如何使用Espresso, 第二部分分析如何處理諸如非同步, 依賴註入, 程式結構對UI測試的影響以及提供解決辦法, 第三部分提供源碼以及一些Reference的地址. ...


UI測試在Android平臺上一直都是一個令人頭痛的事情, 由於大家平時用的很少, 加之很多文檔的缺失, 如果很多東西從頭摸索,勢必踩坑無數.

自Android24正式淘汰掉了InstrumentationTestCase(位於android.test包), 推出Espresso(位於android.support.test包), Google一直致力於降低UI測試的門檻.

瞭解測試金字塔的同學可能知道,UI測試屬於功能測試(Functional Test), 或者按照其他的劃分也屬於集成測試(Integration Test), Google推出了UIAutomatorEspresso來分別處理跨App間的測試(黑盒測試)以及App內的測試(白盒測試).

測試步驟類似,分為:

  • 查找元素
  • 觸發行為
  • 檢測結果

本文分為三部分, 第一部分簡單介紹如何使用Espresso, 第二部分分析如何處理諸如非同步, 依賴註入, 程式結構對UI測試的影響以及提供解決辦法, 第三部分提供源碼以及一些Reference的地址.

Part I

如何配置

1.需要在gradle的dependencies里添加依賴

androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'

2.在gradle的android.defaultConfig里指定TestRunner

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

3.書寫測試文件,通過AndroidJUnit4來跑即可,使用Activity Rule來啟動你的Activity.

@Rule
@JvmField
var activityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>()

4.添加測試.

onView(withText("Hello world!")).check(matches(isDisplayed()));

5.運行

./gradlew connectedAndroidTest

或在IDE中進行運行.

以上步驟寫的比較簡略, 如果第一次使用, 可參考官方文檔.

Part II

貌似已經會了, 打鉤[x]?

對於簡單的UI其實上面的5步已經完全足夠, 這也是Espresso好用的地方, 將UI測試寫的跟普通的Unit Test一樣簡單.

但是隨著你的UI變得複雜, 很多問題接踵而至.

其根本原因在於, Espresso系統在處理內置UI渲染(包括WebView)的非同步操作都沒有問題, 它會等待頁面的渲染與載入, 而你自己如果有非同步邏輯, 可能測試進程不會等待其完成而結束, 導致測試失敗.

而採用Unit Test將無論是RxJava的Scheduler或者是Excutor替換成同一個線程的方法沒法在UI Test中使用. 原因是UI操作只能在創建它的線程使用(UI 線程), 而如果你用了網路或者Room之類的資料庫, 它又無法在UI線程使用, 相互矛盾, 進退兩難.

所以這個時候就需要使用Espresso提供的IdleResource, 來通知系統是否Idle或者Busy.

什麼時候該使用IdleResource

其實IdleResource的官方文檔裡面有指出, 如果你的測試里有使用:

  • Thread.sleep()
  • Retry
  • CountDown ...

來保證你的測試工作正常, 那麼意味著你應該使用IdleResource了.

或許剛剛接觸Espresso的你可能還沒有意識到問題所在, 還沒有使用Work Around的方法來解決問題, 換個角度來說可能更好理解.

如果你所測試程式里有使用:

  • Databinding
  • LiveData
  • 通過非AsyncTask實現的非同步操作
  • Fragment跳轉
  • 等等...

那麼就意味著你需要使用IdleResource來保證你的測試能順利進行, 否則Test Case可能在程式非同步操作未執行時就已經關閉了.

如何使用IdleResource

IdleResource的三個關鍵介面都非常Straigtforward.

1:

fun getName(): String

每一個IdleResource都應該有唯一的Name來註冊到系統里, 不能重覆.

2:

fun isIdleNow(): Boolean

Espresso會從UI線程調用, 通過這個方法來獲得是否進入Idle狀態.

3:

fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback)

當該IdleResource被使用時, Espresso會註冊該callback, 當background job執行完畢後, 需要調用callback.onTransitionToIdle()通知(如果已經是Idle狀態, 調用也不影響, 所以很多簡單的實現都是將這個調用放在isIdleNow中, 判斷已經idle就調用, 雖然google的best practice里說不要這樣), 該調用會通知UI線程, 並可以在任何線程調用.

在使用IdleResource的時候, 通常是通過註冊Rule來驅動的, 這個就需要繼承TestWatcher.

覆寫它的starting與finished方法, 通過IdlingRegistry.getInstance().registerIdlingRegistry.getInstance().unregister來註冊/反註冊IdleReource, 當然可能需要在finished的時候drain掉所有在運行的Task.

給一個簡單的例子把.

class SampleIdleResourceRule : TestWatcher() {
    private val idlingResource: IdlingResource = xxx
    
    override fun starting(description: Description?) {
        IdlingRegistry.getInstance().register(idlingResource)
        super.starting(description)
    }
    
    override fun finished(description: Description?) {
        //drain all the pending task here if needed.
        IdlingRegistry.getInstance().unregister(idlingResource)
        super.finished(description)
    }
}

舉個IdleResource的例子吧.

1.使用LiveData等Archtecture Component組件

我們知道LiveData是一個訂閱系統, 是必涉及後臺線程, 比較方便的是它自己內部已經調用了IdleResource來增加/減少後臺job, 所以直接使用系統提供的CountingTaskExecutorRule.

由於Resource name不能重覆, 所以為了繞過這個檢測, 需要繼承CountingTaskExecutorRule來複寫getName.

具體可以參考google的TaskExecutorWithIdlingResourceRule.

Google還提供了Databinding的Rule, 可以參考.

2.等待彈框結束

一般情況下我們使用DialogFragment來彈框, 如果我們去check一些text被dialog遮擋, 就必須等待其消失後在進行檢查.

這時我們可以通過findFragmentByTag來檢測該彈框是否dismiss.

class DialogIdlingResource(
        private val manager: FragmentManager, 
        private val tag: String) : IdlingResource {
    private var resourceCallback: IdlingResource.ResourceCallback? = null

    override fun getName(): String = "xxx"

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

    override fun isIdleNow(): Boolean {
        val idle = manager.findFragmentByTag(tag) == null
        if (idle) {
            resourceCallback?.onTransitionToIdle()
        }
        return idle
    }
}

3.Delegate Executors/Scheduler

如果有非同步處理邏輯, 大多都位於Repository/ViewModel層, 這部分會被Mock, 但也有一些UI邏輯可能會用到Excecutor. 如RecyclerView的DiffUtil, 需要傳入一個Executor來做非同步Diff, 這時我們就需要一個Excecutor的IdlingResource, 並把它裡面的Delegate賦值給UI.

這部分可以參考Google GithubBrowser Sample的CountingAppExecutorsRule.

應該怎麼測試, 需要測試什麼?

雖然Espresso測試是集成測試, 但是由於涉及到非同步邏輯導致Test Case無法按照預期進行的問題時而存在, 且有時候無法通過IdlingResource來解決.

比如涉及到多個Fragment的跳轉, 就會發生在Fragment未打開時Test Case就掛掉的情況.

再比如使用RxJava, 在Espresso3.x + RxJava2.x的情況下, 即便將Scheduler代理給IdlingResource也無法保證整個業務流程完整走下來, 非同步操作仍無法完整運行, 具體問題可參考Jake大神RxIdler的Issue.

所以測試起來就有一些原則需要遵守, 才能保證整個流程的可測性.

  • 最好對每一個Fragment進行單獨測試, Mock所依賴的部分, 如網路, 數據模塊, 如果涉及Fragment跳轉邏輯, 通過繼承來複寫進行測試.
  • 如果使用了RxJava, 需要將其封裝在Repository或者Presenter/ViewModel中進行整體的Mock.
  • 如果使用了Dagger2.android進行自動註入, 最好對測試部分自定義TestRunner提供一個空的Application來Disable註入, 對所測試Fragment註入對象進行手動賦值.
  • 如果Activity有註入邏輯, 最好將其解耦到Fragment, 因為Espresso的Activity是通過ActivityRule來啟動, 無法進行直接手動註入.
  • 如果無法Move到Fragment, 或者不想... 那就需要在測試里構建自己的Dagger Component, 對於使用Dagger2.android自動註入的, 還需要手動創建Fake的DispatchingAndroidInjector完成手動註入.
  • 如果未使用Dagger2.android, 通過AndroidInjector來註入的, 可以忽略與註入相關的item.

能再講的仔細一些嗎?

1.單獨測試Fragment的好處是可以解耦Fragment之間的跳轉, 往往Fragment都是UI流程中的一個環節, 當邏輯完成時會跳向下一Fragment. 可以創建一個空Activity來專門用於顯示該Fragment, 並且在測試的setUp里commit該Fragment.

class TestActivity {
    fun showFragment()
}

@RunWith(AndroidJUnit4::class)
class XXXFragmentTest {
    @Rule
    @JvmField
    val activityRule = ActivityTestRule(XXXFragment::class.java)
    
    @Before
    fun init() {
        //1. init fragment
        //2. assign mock data
        activityRule.showFragment(xxx)  
    }
    
    @Test
    fun testXXX() {
        xxx
    }
}

2.由於常常會需要繼承需要測試的Fragment來複寫一些類, 對於使用Dagger.android自動註入的, 該子Fragment又未通過@ContributesAndroidInjector進行註冊, 往往需要自定義TestRunner, 然後手動註入Fragment.

class CustomTestRunner : AndroidJUnitRunner() {
    override fun newApplication(...) {
        return ...TestApp:class...
    }
}

android {
    defaultConfig {
        testInstrumentationRunner "xxx.CustomTestRunner"
    }
}

class TestApp : Application() {}

@RunWith(AndroidJUnit4::class)
class XXXFragmentTest {
    //activity rule
    ...
    val testFragment = TestFragment()
    
    @Before
    fun init() {
        testFragment.xxx = mockXXX
        ...
        activityRule.activity.showFragment(testFragment)
    }
    
    @Test
    fun testXXX() {
        onView...check(...)
        assertTrue(testFragment.isXXXShow)
    }
    
    class TestFragment : XXXFragment() {
        var isXXXShow = false
        override fun showXXX() {
            isXXXShow = true
        }
    }
}

3.如果Activity有註入邏輯與業務邏輯, 並且不想抽到Fragment中去, 則需要創建Fake的Injector保證可以完成註入,

fun createFakeInjector(block: T.() -> Unit): DispatchingAndroidInjector<Activity> {
    ...
}

@RunWith(AndroidJUnit4::class)
class XXXActivityTest {
    @Rule
    @JvmField
    var activityRule = object : ActivityTestRule<XXX>(XXX::class.java) {
       val app = ...get application
       app.dispatchingAndroidInjector = createFakeInjector<XXX>() {
           //手動註入
           xxx =  mockXXX
           `when`(xxx).thenReturn(xxx)
       }
    }
}

4.為了支持需要通過繼承Fragment來完成測試的Case, 還需要對測試模塊創建自己的Component來註冊從而進行Fake Injector的創建 (類似3, 只是Application/Activity可能為Test版本).

Grale

dependencies {
    kaptAndroidTest 'com.google.dagger:dagger-android-processor:2.X'
}


@Component(modules = [
    AndroidInjectionModule::class,
    AndroidSupportInjectionModule::class,
    ...主App所註冊的所有Module,
    TestActivityModule::class])
interface TestCompnent {
    fun inject(xxx: XXX)
    ...
}

@Module
abstract class TestActivityModule {
  //通過`ContributesAndroidInjector`註冊你的TestActivity, 以及TestFragment  
}

class TestApp : Application(), HasActivityInjector {
    @Inject
    lateinit var injector: DispatchingAndroidInjector<Activity>
}

class TestActivity : Activity(), HasSupportFragmentInjector {
    @Inject
    lateinit var injector: DispatchingAndroidInjector<Fragment>
}

Part III

如果還不是很明白可以查看代碼

Disable註入的在這裡:
Google的Demo GithubBrowser

跟註入相關的在這裡:
自己的Demo

Reference

  • https://proandroiddev.com/activity-espresso-test-with-daggers-android-injector-82f3ee564aa4
  • https://github.com/SabagRonen/dagger-activity-test-sample
  • https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample
  • https://developer.android.com/training/testing/espresso/idling-resource
  • http://blog.sqisland.com/2015/07/espresso-wait-for-dialog-to-dismiss.html

歡迎關註微信公眾號: 聖騎士Wind
微信公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • NOT IN查詢示例(示例背景描述:根據條件查詢Questions表得到的數據基本在PostedData表中不存在,為完全保證查詢結果在PostedData表中不存在,使用NOT IN): 查詢耗時如圖: 換用NOT EXISTS 查詢示例: 查詢耗時如圖: 根據查詢耗時明顯可以看出,使用 NOT ...
  • 一. 安裝說明 安裝XtraBackup 2.4 版本有三種方式: (1) 存儲庫安裝Percona XtraBackup(推薦) (2 )下載的rpm或apt包安裝Percona XtraBackup。 (3) 源代碼編譯和安裝。 Percona為yum (Red Hat、CentOS和Amazo ...
  • 按照ASCII碼, SELECT char(64) 例如64 對應 @,則 則結果為 abckkqq.com 依此類推, 去掉其他特殊符號,參考ASCII碼對照表, 去掉tab符號為 select REPLACE(‘要替換的字元或列名’,char(9),’替換的目標字元’) 去掉空格符號為 sele ...
  • 現在的大數據是很紅火的,薪資比通常的軟體行業要高,所以很多年輕人想進入這個行業。但並不是每個大數據相關的工作都是高薪的,主要還是根據自己的專長進行選擇發展。大數據涉及的知識很廣泛,如果要當全能選手,是非常艱難,一個人的精力也是有限的。進行細分選擇,然後專攻才是正道。要瞭解學習大數據,如果是程式員,其 ...
  • 在MSSQL Server中通過查看SQL語句執行所用的時間,來衡量SQL語句的性能。 通過設置STATISTICS我們可以查看執行SQL時的系統情況。選項有PROFILE,IO ,TIME。介紹如下: SET STATISTICS PROFILE ON:顯示分析、編譯和執行查詢所需的時間(以毫秒為 ...
  • 使用SSMS資料庫管理工具刪除索引 使用表設計器刪除索引 表設計器可以刪除任何類型的索引,本示例演示刪除XML輔助索引,刪除其他索引步驟相同。 1、連接資料庫,選擇資料庫,展開資料庫-》選擇數據表,右鍵點擊-》選擇設計。 2、在表設計器視窗-》選擇要刪除索引的數據列-》右鍵點擊-》選擇要刪除的索引類 ...
  • mysql安裝(rpm) 1.卸載系統自帶的 mariadb lib [root@centos linux ~] rpm qa|grep mariadb mariadb libs 5.5.44 2.el7.centos.x86_64 [root@centos linux ~] rpm e maria ...
  • 在將項目集成到 Jenkins 後,經常會出現不穩定的構建,Jenkins 控制台輸出的錯誤信息為: Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed) 。 經過調查,問 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...