
使用假數(shù)據(jù)和Espresso來創(chuàng)建UI測(cè)試
這是Android測(cè)試系列的最后一部分。 如果你錯(cuò)過了前2個(gè)部分,不用擔(dān)心,即使你沒有閱讀過,也可以理解這一點(diǎn)。 如果你真的想看看,你可以從下面的鏈接找到它們。
Complete example of testing MVP architecture with Kotlin and RxJava?—?Part 1
Complete example of testing MVP architecture with Kotlin and RxJava?—?Part 2
在這部分中,您將學(xué)習(xí)如何使用假數(shù)據(jù)在Espresso中創(chuàng)建UI測(cè)試,如何模擬Mockito-Kotlin的依賴關(guān)系,以及如何模擬Android測(cè)試中的final 類。
用假數(shù)據(jù)編寫Espresso測(cè)試
如果我們想編寫始終產(chǎn)生相同的結(jié)果的UI測(cè)試,我們最需要做的事情就是使我們的測(cè)試獨(dú)立于來自網(wǎng)絡(luò)或本地?cái)?shù)據(jù)庫的任何數(shù)據(jù)。
在其他層面,我們可以通過模擬測(cè)試類的依賴來輕松實(shí)現(xiàn)這一點(diǎn)(正如你在前兩部分中看到的)。 這在UI測(cè)試中有所不同。 在前面的例子中,我們的類是從構(gòu)造函數(shù)中得到了它們的依賴,所以我們可以很容易地將模擬對(duì)象傳遞給構(gòu)造函數(shù)。 而Android組件是由系統(tǒng)實(shí)例化的,通常是通過字段注入獲得它們的依賴。
使用假數(shù)據(jù)創(chuàng)建UI測(cè)試有多種方法。 首先讓我們看看如何在我們的測(cè)試中用FakeUserRepository替換UserRepository。
實(shí)現(xiàn)FakeUserRepository
FakeUserRepository是一個(gè)簡(jiǎn)單的類,它為我們提供了假數(shù)據(jù)。 它實(shí)現(xiàn)了UserRepository接口。 DefaultUserRepository也實(shí)現(xiàn)了它,但它為我們提供應(yīng)用程序中的真實(shí)數(shù)據(jù)。
class FakeUserRepository : UserRepository {
override fun getUsers(page: Int, forced: Boolean): Single<UserListModel> {
val users = (1..10L).map {
val number = (page - 1) * 10 + it
User(it, "User $number", number * 100, "")
}
return Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
val userListModel = UserListModel(users)
emitter.onSuccess(userListModel)
}
}
}
我認(rèn)為這個(gè)代碼不需要太多的解釋。 我們創(chuàng)建了一個(gè)Single來發(fā)送一串假的users數(shù)據(jù)。 雖然值得一提的是這部分代碼:
val users = (1..10L).map
我們可以使用map函數(shù)從一個(gè)范圍里創(chuàng)建列表。 這在這種情況下可能非常有用。
將FakeUserRepository注入我們的測(cè)試
現(xiàn)在我們有了假的UserRepository實(shí)現(xiàn),但我們?nèi)绾卧谖覀兊臏y(cè)試中使用它呢? 當(dāng)使用Dagger時(shí),我們通常有一個(gè)ApplicationComponent和一個(gè)ApplicationModule來提供應(yīng)用程序級(jí)的依賴關(guān)系。 我們?cè)谧远xApplication類中初始化component。
class CustomApplication : Application() {
lateinit var component: ApplicationComponent
override fun onCreate() {
super.onCreate()
initAppComponent()
Stetho.initializeWithDefaults(this);
component.inject(this)
}
private fun initAppComponent() {
component = DaggerApplicationComponent
.builder()
.applicationModule(ApplicationModule(this))
.build()
}
}
現(xiàn)在我們將創(chuàng)建一個(gè)FakeApplicationModule和一個(gè)FakeApplicationComponent,這將為我們提供FakeUserRepository。 在我們的UI測(cè)試中,我們將component字段設(shè)置為FakeApplicationComponent。
來看一下這個(gè)例子:
@Singleton
@Component(modules = arrayOf(FakeApplicationModule::class))
interface FakeApplicationComponent : ApplicationComponent
由于該component繼承自ApplicationComponent,所以我們可以使用它來替代。
@Module
class FakeApplicationModule {
@Provides
@Singleton
fun provideUserRepository() : UserRepository {
return FakeUserRepository()
}
@Provides
@Singleton
fun provideSchedulerProvider() : SchedulerProvider = AppSchedulerProvider()
}
我們不需要在這里提供任何其他東西,因?yàn)榇蠖鄶?shù)提供的依賴關(guān)系用于真正的UserRepository實(shí)現(xiàn)。
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField
var activityRule = ActivityTestRule(MainActivity::class.java, true, false)
@Before
fun setUp() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as CustomApplication
val testComponent = DaggerFakeApplicationComponent.builder()
.fakeApplicationModule(FakeApplicationModule())
.build()
app.component = testComponent
activityRule.launchActivity(Intent())
}
@Test
fun testRecyclerViewShowingCorrectItems() {
// TODO
}
}
view raw
前兩個(gè)片段已經(jīng)在上面解釋過了。 這里有趣的部分是MainActivityTest類。來看看這里發(fā)生了什么。
在setUp方法中,我們得到了一個(gè)CustomApplication類的實(shí)例,創(chuàng)建了我們的FakeApplicationComponent,接著啟動(dòng)了MainActivity。
在設(shè)置component后,啟動(dòng)Activity很重要。 可以通過將另一個(gè)構(gòu)造函數(shù)參數(shù)傳遞給ActivityTestRule的構(gòu)造函數(shù)來實(shí)現(xiàn)。 第三個(gè)參數(shù)是一個(gè)布爾值,它決定了測(cè)試運(yùn)行程序是否應(yīng)立即啟動(dòng)該Activity。
Espresso示例
現(xiàn)在我們可以開始寫一些測(cè)試。 我不想過多描述如何用Espresso來編寫測(cè)試用例的細(xì)節(jié),已經(jīng)有了很多教程,但是我們先來看一個(gè)簡(jiǎn)單的例子。
首先我們需要添加依賴關(guān)系到build.gradle。 如果我們使用了RecyclerView,在普通espresso-core之外,我們還需要添加espresso-contrib依賴。
androidTestImplementation ('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation('com.android.support.test.espresso:espresso-contrib:2.2') {
// Necessary to avoid version conflicts
exclude group: 'com.android.support', module: 'appcompat'
exclude group: 'com.android.support', module: 'support-v4'
exclude group: 'com.android.support', module: 'support-annotations'
exclude module: 'recyclerview-v7'
}
現(xiàn)在我們的測(cè)試看起來是這樣:
@Test
fun testOpenDetailsOnItemClick() {
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, ViewActions.click()))
val expectedText = "User 1: 100 pts"
Espresso.onView(Matchers.allOf(ViewMatchers.withId(android.support.design.R.id.snackbar_text), ViewMatchers.withText(expectedText)))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
}
發(fā)生了什么?
首先,我們找到RecyclerView然后在RecyclerViewActions的幫助下,點(diǎn)擊它的第一個(gè)(0索引)項(xiàng)。
在我們作出斷言之后,一個(gè)Snackbar顯示出了User 1: 100 pts的文本。
這是一個(gè)非常簡(jiǎn)單的測(cè)試用例。 您可以在Github倉庫中找到更多測(cè)試用例的示例。 該部分的代碼更改可以在此提交中找到:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8152c2065af2e0871ba1175cadecb92b3fa8417f
在UI測(cè)試中模擬UserRepository
如果我們想測(cè)試以下情景,該怎么辦?
- 加載第一頁數(shù)據(jù)成功
- 加載第二頁錯(cuò)誤
- 驗(yàn)證當(dāng)我們嘗試加載第二頁時(shí)是否在屏幕上顯示了Toast
我們不能在這里使用我們的假實(shí)現(xiàn),因?yàn)樗偸浅晒Ψ祷匾粋€(gè)user list。 我們可以修改實(shí)現(xiàn),對(duì)于第二個(gè)頁面,讓它返回一個(gè)會(huì)發(fā)送錯(cuò)誤的Single,但這并不好。 如果我們要添加另一個(gè)測(cè)試用例,我們需要一次又一次地進(jìn)行修改。
這種情況我們可以模擬getUsers方法的行為。 為此,我們需要對(duì)FakeApplicationModule進(jìn)行一些修改。
@Module
class FakeApplicationModule(val userRepository: UserRepository) {
@Provides
@Singleton
fun provideUserRepository() : UserRepository {
return userRepository
}
...
}
現(xiàn)在我們?cè)跇?gòu)造函數(shù)中傳遞UserRepository,所以在測(cè)試中,我們可以創(chuàng)建一個(gè)mock對(duì)象,并使用它來構(gòu)建我們的component。
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
...
private lateinit var mockUserRepository: UserRepository
@Before
fun setUp() {
mockUserRepository = mock()
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as CustomApplication
val testComponent = DaggerFakeApplicationComponent.builder()
.fakeApplicationModule(FakeApplicationModule(mockUserRepository))
.build()
app.component = testComponent
}
...
}
這是我們修改后的測(cè)試類。 使用了我在第一部分中提到過的用來模擬UserRepository的mockito-kotlin庫。 我們需要添加以下依賴關(guān)系到build.gradle,然后使用它。
androidTestImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0"
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.2.0'
現(xiàn)在我們可以修改模擬的行為了。 我為此創(chuàng)建了兩個(gè)私有的工具方法,可以在測(cè)試用例中重用它們。
private fun mockRepoUsers(page: Int) {
val users = (1..20L).map {
val number = (page - 1) * 20 + it
User(it, "User $number", number * 100, "")
}
val mockSingle = Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
val userListModel = UserListModel(users)
emitter.onSuccess(userListModel)
}
whenever(mockUserRepository.getUsers(page, false)).thenReturn(mockSingle)
}
private fun mockRepoError(page: Int) {
val mockSingle = Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->
emitter.onError(Throwable("Error"))
}
whenever(mockUserRepository.getUsers(page, false)).thenReturn(mockSingle)
}
我們需要做的另一個(gè)改變是在建立模擬對(duì)象之后,在測(cè)試用例中啟動(dòng)Activity,而不是在setUp方法中去啟動(dòng)。
有了這個(gè)變化,我們前面的測(cè)試用例如下所示:
@Test
fun testOpenDetailsOnItemClick() {
mockRepoUsers(1)
activityRule.launchActivity(Intent())
Espresso.onView(ViewMatchers.withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, ViewActions.click()))
val expectedText = "User 1: 100 pts"
Espresso.onView(Matchers.allOf(ViewMatchers.withId(android.support.design.R.id.snackbar_text), ViewMatchers.withText(expectedText)))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()));
}
GitHub倉庫中還有一些其它的測(cè)試用例,包括錯(cuò)誤時(shí)的情況。 此部分中的更改可以在此提交中看到:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/3889286528ad5a88035358894fda9e0be8c145aa
附贈(zèng):在Android測(cè)試中模擬final類
在Kotlin里,默認(rèn)情況下每個(gè)class都是final的,這使得mock變得復(fù)雜。 在第一部分中,我們看到了如何用Mockito模擬final類。
不幸的是,這種方法在Android真機(jī)測(cè)試中不起作用。 在這種情況下,我們有幾種解決方案, 其中之一是使用Kotlin all-open 插件。
這是一個(gè)編譯器插件,它允許我們創(chuàng)建一個(gè)注解,如果使用它,將會(huì)打開該類。
要使用它,我們需要添加以下依賴關(guān)系到我們項(xiàng)目(project)的build.gradle文件:
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
然后添加以下的內(nèi)容到app模塊的build.gradle文件中:
apply plugin: 'kotlin-allopen'
allOpen {
annotation("com.myapp.OpenClass")
}
現(xiàn)在我們只需要在我們指定的包中創(chuàng)建我們的注解:
@Target(AnnotationTarget.CLASS)
annotation class OpenClass
all-open插件的示例可以在此提交中找到:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8152c2065af2e0871ba1175cadecb92b3fa8417f
——————
我們到達(dá)了漫長的旅程的盡頭,覆蓋了我們應(yīng)用程序中的每一個(gè)代碼,并附帶了測(cè)試。 感謝您閱讀這篇文章,希望您能發(fā)現(xiàn)這些文章是有用的。
Thanks for reading my article.