如何正確使用Espresso來測試你的Android程式
UI測試在Android平臺上一直都是一個令人頭痛的事情, 由於大家平時用的很少, 加之很多文件的缺失, 如果很多東西從頭摸索,勢必踩坑無數.
自Android24正式淘汰掉了InstrumentationTestCase(位於android.test包), 推出Espresso(位於android.support.test包), Google一直致力於降低UI測試的門檻.
瞭解測試金字塔的同學可能知道,UI測試屬於功能測試(Functional Test), 或者按照其他的劃分也屬於整合測試(Integration Test), Google推出了UIAutomator與Espresso來分別處理跨App間的測試(
測試步驟類似,分為:
- 查詢元素
- 觸發行為
- 檢測結果
本文分為三部分, 第一部分簡單介紹如何使用Espresso, 第二部分分析如何處理諸如非同步, 依賴注入, 程式結構對UI測試的影響以及提供解決辦法, 第三部分提供原始碼以及一些Reference的地址.
Part I
如何配置
- 需要在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'
- 在gradle的android.defaultConfig裡指定TestRunner
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- 書寫測試檔案,通過
AndroidJUnit4
來跑即可,使用Activity Rule來啟動你的Activity.
@Rule
@JvmField
var activityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>()
- 新增測試.
onView(withText("Hello world!")).check(matches(isDisplayed()));
- 執行
./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.
fun getName(): String
每一個IdleResource都應該有唯一的Name來註冊到系統裡, 不能重複.
fun isIdleNow(): Boolean
Espresso會從UI執行緒呼叫, 通過這個方法來獲得是否進入Idle狀態.
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().register
與IdlingRegistry.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還提供了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.
應該怎麼測試, 需要測試什麼?
雖然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
如果還不是很明白可以檢視程式碼
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