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

ditclear發表於2017-10-25

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

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

使用假資料和Espresso來建立UI測試

這是Android測試系列的最後一部分。 如果你錯過了前2個部分,不用擔心,即使你沒有閱讀過,也可以理解這一點。 如果你真的想看看,你可以從下面的連結找到它們。

Complete example of testing MVP architecture with Kotlin and RxJava — Part 1

Complete example of testing MVP architecture with Kotlin and RxJava — Part 2

在這部分中,您將學習如何使用假資料在Espresso中建立UI測試,如何模擬Mockito-Kotlin的依賴關係,以及如何模擬Android測試中的final 類。

用假資料編寫Espresso測試

如果我們想編寫始終產生相同的結果的UI測試,我們最需要做的事情就是使我們的測試獨立於來自網路或本地資料庫的任何資料。

在其他層面,我們可以通過模擬測試類的依賴來輕鬆實現這一點(正如你在前兩部分中看到的)。 這在UI測試中有所不同。 在前面的例子中,我們的類是從建構函式中得到了它們的依賴,所以我們可以很容易地將模擬物件傳遞給建構函式。 而Android元件是由系統例項化的,通常是通過欄位注入獲得它們的依賴。

使用假資料建立UI測試有多種方法。 首先讓我們看看如何在我們的測試中用FakeUserRepository替換UserRepository

實現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.

相關文章