原文連接:android.jlelse.eu/complete-ex…html
簡書譯文地址:www.jianshu.com/p/f56d8d1ce…java
這是Android測試系列的最後一部分。 若是你錯過了前2個部分,不用擔憂,即便你沒有閱讀過,也能夠理解這一點。 若是你真的想看看,你能夠從下面的連接找到它們。android
Complete example of testing MVP architecture with Kotlin and RxJava — Part 1git
Complete example of testing MVP architecture with Kotlin and RxJava — Part 2github
在這部分中,您將學習如何使用假數據在Espresso中建立UI測試,如何模擬Mockito-Kotlin的依賴關係,以及如何模擬Android測試中的final 類。數據庫
若是咱們想編寫始終產生相同的結果的UI測試,咱們最須要作的事情就是使咱們的測試獨立於來自網絡或本地數據庫的任何數據。網絡
在其餘層面,咱們能夠經過模擬測試類的依賴來輕鬆實現這一點(正如你在前兩部分中看到的)。 這在UI測試中有所不一樣。 在前面的例子中,咱們的類是從構造函數中獲得了它們的依賴,因此咱們能夠很容易地將模擬對象傳遞給構造函數。 而Android組件是由系統實例化的,一般是經過字段注入得到它們的依賴。app
使用假數據建立UI測試有多種方法。 首先讓咱們看看如何在咱們的測試中用FakeUserRepository
替換UserRepository
。ide
FakeUserRepository
是一個簡單的類,它爲咱們提供了假數據。 它實現了UserRepository
接口。 DefaultUserRepository
也實現了它,但它爲咱們提供應用程序中的真實數據。函數
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)
}
}
}複製代碼
我認爲這個代碼不須要太多的解釋。 咱們建立了一個Single
來發送一串假的users數據。 雖然值得一提的是這部分代碼:
val users = (1..10L).map複製代碼
咱們可使用map
函數從一個範圍裏建立列表。 這在這種狀況下可能很是有用。
如今咱們有了假的UserRepository
實現,但咱們如何在咱們的測試中使用它呢? 當使用Dagger時,咱們一般有一個ApplicationComponent
和一個ApplicationModule
來提供應用程序級的依賴關係。 咱們在自定義Application
類中初始化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()
}
}複製代碼
如今咱們將建立一個FakeApplicationModule
和一個FakeApplicationComponent
,這將爲咱們提供FakeUserRepository
。 在咱們的UI測試中,咱們將component
字段設置爲FakeApplicationComponent
。
來看一下這個例子:
@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()
}複製代碼
咱們不須要在這裏提供任何其餘東西,由於大多數提供的依賴關係用於真正的UserRepository實現。
@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複製代碼
前兩個片斷已經在上面解釋過了。 這裏有趣的部分是MainActivityTest
類。來看看這裏發生了什麼。
在setUp
方法中,咱們獲得了一個CustomApplication
類的實例,建立了咱們的FakeApplicationComponent
,接着啓動了MainActivity
。
在設置component
後,啓動Activity很重要。 能夠經過將另外一個構造函數參數傳遞給ActivityTestRule
的構造函數來實現。 第三個參數是一個布爾值,它決定了測試運行程序是否應當即啓動該Activity。
如今咱們能夠開始寫一些測試。 我不想過多描述如何用Espresso來編寫測試用例的細節,已經有了不少教程,可是咱們先來看一個簡單的例子。
首先咱們須要添加依賴關係到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'
}複製代碼
如今咱們的測試看起來是這樣:
@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()));
}複製代碼
發生了什麼?
首先,咱們找到RecyclerView
而後在RecyclerViewActions
的幫助下,點擊它的第一個(0索引)項。
在咱們做出斷言以後,一個Snackbar顯示出了User 1: 100 pts
的文本。
這是一個很是簡單的測試用例。 您能夠在Github倉庫中找到更多測試用例的示例。 該部分的代碼更改能夠在此提交中找到:
若是咱們想測試如下情景,該怎麼辦?
咱們不能在這裏使用咱們的假實現,由於它老是成功返回一個user list。 咱們能夠修改實現,對於第二個頁面,讓它返回一個會發送錯誤的Single,但這並很差。 若是咱們要添加另外一個測試用例,咱們須要一次又一次地進行修改。
這種狀況咱們能夠模擬getUsers
方法的行爲。 爲此,咱們須要對FakeApplicationModule
進行一些修改。
@Module
class FakeApplicationModule(val userRepository: UserRepository) {
@Provides
@Singleton
fun provideUserRepository() : UserRepository {
return userRepository
}
...
}複製代碼
如今咱們在構造函數中傳遞UserRepository
,因此在測試中,咱們能夠建立一個mock對象,並使用它來構建咱們的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
}
...
}複製代碼
這是咱們修改後的測試類。 使用了我在第一部分中提到過的用來模擬UserRepository
的mockito-kotlin庫。 咱們須要添加如下依賴關係到build.gradle
,而後使用它。
androidTestImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0"
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.2.0'複製代碼
如今咱們能夠修改模擬的行爲了。 我爲此建立了兩個私有的工具方法,能夠在測試用例中重用它們。
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)
}複製代碼
咱們須要作的另外一個改變是在創建模擬對象以後,在測試用例中啓動Activity,而不是在setUp
方法中去啓動。
有了這個變化,咱們前面的測試用例以下所示:
@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倉庫中還有一些其它的測試用例,包括錯誤時的狀況。 此部分中的更改能夠在此提交中看到:
在Kotlin裏,默認狀況下每一個class都是final的,這使得mock變得複雜。 在第一部分中,咱們看到了如何用Mockito模擬final類。
不幸的是,這種方法在Android真機測試中不起做用。 在這種狀況下,咱們有幾種解決方案, 其中之一是使用Kotlin all-open 插件。
這是一個編譯器插件,它容許咱們建立一個註解,若是使用它,將會打開該類。
要使用它,咱們須要添加如下依賴關係到咱們項目(project)的build.gradle文件:
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"複製代碼
而後添加如下的內容到app模塊的build.gradle
文件中:
apply plugin: 'kotlin-allopen'
allOpen {
annotation("com.myapp.OpenClass")
}複製代碼
如今咱們只須要在咱們指定的包中建立咱們的註解:
@Target(AnnotationTarget.CLASS)
annotation class OpenClass複製代碼
all-open插件的示例能夠在此提交中找到:
——————
咱們到達了漫長的旅程的盡頭,覆蓋了咱們應用程序中的每個代碼,並附帶了測試。 感謝您閱讀這篇文章,但願您能發現這些文章是有用的。
Thanks for reading my article.