【譯】使用Kotlin和RxJava測試MVP架構的完整示例 - 第3部分

原文連接:android.jlelse.eu/complete-ex…html

簡書譯文地址:www.jianshu.com/p/f56d8d1ce…java

使用假數據和Espresso來建立UI測試

這是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 類。數據庫

用假數據編寫Espresso測試

若是咱們想編寫始終產生相同的結果的UI測試,咱們最須要作的事情就是使咱們的測試獨立於來自網絡或本地數據庫的任何數據。網絡

在其餘層面,咱們能夠經過模擬測試類的依賴來輕鬆實現這一點(正如你在前兩部分中看到的)。 這在UI測試中有所不一樣。 在前面的例子中,咱們的類是從構造函數中獲得了它們的依賴,因此咱們能夠很容易地將模擬對象傳遞給構造函數。 而Android組件是由系統實例化的,一般是經過字段注入得到它們的依賴。app

使用假數據建立UI測試有多種方法。 首先讓咱們看看如何在咱們的測試中用FakeUserRepository替換UserRepositoryide

實現FakeUserRepository

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函數從一個範圍裏建立列表。 這在這種狀況下可能很是有用。

將FakeUserRepository注入咱們的測試

如今咱們有了假的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示例

如今咱們能夠開始寫一些測試。 我不想過多描述如何用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倉庫中找到更多測試用例的示例。 該部分的代碼更改能夠在此提交中找到:

github.com/kozmi55/Kot…

在UI測試中模擬UserRepository

若是咱們想測試如下情景,該怎麼辦?

  • 加載第一頁數據成功
  • 加載第二頁錯誤
  • 驗證當咱們嘗試加載第二頁時是否在屏幕上顯示了Toast

咱們不能在這裏使用咱們的假實現,由於它老是成功返回一個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
    }

  ...
}複製代碼

這是咱們修改後的測試類。 使用了我在第一部分中提到過的用來模擬UserRepositorymockito-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倉庫中還有一些其它的測試用例,包括錯誤時的狀況。 此部分中的更改能夠在此提交中看到:

github.com/kozmi55/Kot…

附贈:在Android測試中模擬final類

在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插件的示例能夠在此提交中找到:

github.com/kozmi55/Kot…

——————

咱們到達了漫長的旅程的盡頭,覆蓋了咱們應用程序中的每個代碼,並附帶了測試。 感謝您閱讀這篇文章,但願您能發現這些文章是有用的。

Thanks for reading my article.

相關文章
相關標籤/搜索