美文网首页单元测试单元测试
【译】使用Kotlin和RxJava测试MVP架构的完整示例 -

【译】使用Kotlin和RxJava测试MVP架构的完整示例 -

作者: ditclear | 来源:发表于2017-10-25 22:34 被阅读73次

    原文链接:https://android.jlelse.eu/complete-example-of-testing-mvp-architecture-with-kotlin-and-rxjava-part-3-df4cf3838581

    使用假数据和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仓库中找到更多测试用例的示例。 该部分的代码更改可以在此提交中找到:

    https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8152c2065af2e0871ba1175cadecb92b3fa8417f

    在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仓库中还有一些其它的测试用例,包括错误时的情况。 此部分中的更改可以在此提交中看到:

    https://github.com/kozmi55/Kotlin-MVP-Testing/commit/3889286528ad5a88035358894fda9e0be8c145aa

    附赠:在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插件的示例可以在此提交中找到:

    https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8152c2065af2e0871ba1175cadecb92b3fa8417f

    ——————

    我们到达了漫长的旅程的尽头,覆盖了我们应用程序中的每一个代码,并附带了测试。 感谢您阅读这篇文章,希望您能发现这些文章是有用的。

    Thanks for reading my article.

    相关文章

      网友评论

      本文标题:【译】使用Kotlin和RxJava测试MVP架构的完整示例 -

      本文链接:https://www.haomeiwen.com/subject/cxjcpxtx.html