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

最近我创建了一个playground项目来了解更多关于Kotlin和RxJava的信息。 这是一个非常简单的项目,但有一部分,我进行了一些尝试:测试。

在kotlin的测试上可能会有一些陷阱,而且由于它是新出的,所以没有太多的例子。 我认为分享我的经验帮助你来避免踩坑是一个好主意。

关于架构

该应用程序遵循基本MVP架构。 它使用Dagger2进行依赖注入,RxJava2用于数据流。

这些库根据不同的条件提供来自网络或本地存储的数据。 我们使用Retrofit进行网络请求,以及Room作为本地数据库。

我不会详细讲解架构和这些工具。 我想大多数人已经熟悉了他们。 您可以在此提交中查看:

github.com/kozmi55/Kot…

我们将从测试数据库开始,然后向上层测试。

测试数据库

对于数据库,我们使用Android架构组件中的Room Persistence Library。 它是SQLite上的抽象层,可以减少样板代码。

这是最简单的部分。 我们不需要对Kotlin或RxJava做任何具体的事情。 我们先来看看UserDao界面的代码,以决定我们应该测试什么。

@Dao
interface UserDao {@Query("SELECT * FROM user ORDER BY reputation DESC LIMIT (:arg0 - 1) * 30, 30")fun getUsers(page: Int) : List<User>@Insert(onConflict = OnConflictStrategy.REPLACE)fun insertAll(users: List<User>)
}复制代码

getUsers函数根据页码从数据库中请求下一个30个用户。

insertAll插入列表中的所有用户。

我们可以从这里发现几件事情,需要测试什么:

  • 检查插入的用户是否与检索到的用户相同。
  • 检查检索用户正确排序。
  • 检查我们是否插入具有相同ID的用户,它将替换旧的记录。
  • 检查是否查询页面,最多可以有30个用户。
  • 检查我们是否查询第二页,我们将获得正确数量的元素。

下面的代码片段显示了5例这样的实现。

@RunWith(AndroidJUnit4::class)
class UserDaoTest {lateinit var userDao: UserDaolateinit var database: AppDatabase@Beforefun setup() {val context = InstrumentationRegistry.getTargetContext()database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()userDao = database.userDao()}@Afterfun tearDown() {database.close()}@Testfun testInsertedAndRetrievedUsersMatch() {val users = listOf(User(1, "Name", 100, "url"), User())userDao.insertAll(users)val allUsers = userDao.getUsers(1)assertEquals(users, allUsers)}@Testfun testUsersOrderedByCorrectly() {val users = listOf(User(1, "Name", 100, "url"),User(2, "Name2", 500, "url"),User(3, "Name3", 300, "url"))userDao.insertAll(users)val allUsers = userDao.getUsers(1)val expectedUsers = users.sortedByDescending { it.reputation }assertEquals(expectedUsers, allUsers)}@Testfun testConflictingInsertsReplaceUsers() {val users = listOf(User(1, "Name", 100, "url"),User(2, "Name2", 500, "url"),User(3, "Name3", 300, "url"))val users2 = listOf(User(1, "Name", 1000, "url"),User(2, "Name2", 700, "url"),User(4, "Name3", 5500, "url"))userDao.insertAll(users)userDao.insertAll(users2)val allUsers = userDao.getUsers(1)val expectedUsers = listOf(User(4, "Name3", 5500, "url"),User(1, "Name", 1000, "url"),User(2, "Name2", 700, "url"),User(3, "Name3", 300, "url"))assertEquals(expectedUsers, allUsers)}@Testfun testLimitUsersPerPage_FirstPageOnly30Items() {val users = (1..40L).map { User(it, "Name $it", it *100, "url") }userDao.insertAll(users)val retrievedUsers = userDao.getUsers(1)assertEquals(30, retrievedUsers.size)}@Testfun testRequestSecondPage_LimitUsersPerPage_showOnlyRemainingItems() {val users = (1..40L).map { User(it, "Name $it", it *100, "url") }userDao.insertAll(users)val retrievedUsers = userDao.getUsers(2)assertEquals(10, retrievedUsers.size)}
}复制代码

在setup方法中,我们需要配置我们的数据库。 在每次测试之前,我们使用Room的内存数据库创建一个干净的数据库。

测试在这里非常简单,不需要进一步解释。 我们在每个测试中遵循的基本模式如
下所示:

  1. 将数据插入数据库
  2. 从数据库查询数据
  3. 对所检索的数据作出断言

我们可以使用Kotlin Collections API中的函数来简化测试数据的创建,就像这部分代码一样:

val users = (1..40L).map { User(it, "Name $it", it *100, "url") }复制代码

我们创建了一个范围,然后将其映射到用户列表。 这里有多个Kotlin概念:范围,高阶函数,字符串模板。

Commit: github.com/kozmi55/Kot…

测试UserRepository

对于repository和interactor,我们将使用相同的工具。

  • 使用Mockit模拟类的依赖。
  • TestObserver用于测试Observables(在我们的例子中是Singles)

但首先我们需要启用该选项来mock最终的类。 在kotlin里,默认情况下每个class都是final的。 幸运的是,Mockito 2已经支持模拟 final class,但是我们需要启用它。

我们需要在以下位置创建一个文本文件:test / resources / mockito-extensions /,名称为org.mockito.plugins.MockMaker,并附带以下文本:mock-maker-inline

Place of the file in Project view

现在我们可以开始使用Mockito来编写我们的测试。 首先,我们将添加最新版本的Mockito和JUnit。

testImplementation 'org.mockito:mockito-core:2.8.47'
testImplementation 'junit:junit:4.12'复制代码

UserRepository的代码如下:

class UserRepository(private val userService: UserService,private val userDao: UserDao,private val connectionHelper: ConnectionHelper,private val preferencesHelper: PreferencesHelper,private val calendarWrapper: CalendarWrapper) {private val LAST_UPDATE_KEY = "last_update_page_"fun getUsers(page: Int, forced: Boolean): Single<UserListModel> {return Single.create<UserListModel> { emitter: SingleEmitter<UserListModel> ->if (shouldUpdate(page, forced)) {loadUsersFromNetwork(page, emitter)} else {loadOfflineUsers(page, emitter)}}}private fun shouldUpdate(page: Int, forced: Boolean) = when {forced -> true!connectionHelper.isOnline() -> falseelse -> {val lastUpdate = preferencesHelper.loadLong(LAST_UPDATE_KEY + page)val currentTime = calendarWrapper.getCurrentTimeInMillis()lastUpdate + Constants.REFRESH_LIMIT < currentTime}}private fun loadUsersFromNetwork(page: Int, emitter: SingleEmitter<UserListModel>) {try {val users = userService.getUsers(page).execute().body()if (users != null) {userDao.insertAll(users.items)val currentTime = calendarWrapper.getCurrentTimeInMillis()preferencesHelper.saveLong(LAST_UPDATE_KEY + page, currentTime)emitter.onSuccess(users)} else {emitter.onError(Exception("No data received"))}} catch (exception: Exception) {emitter.onError(exception)}}private fun loadOfflineUsers(page: Int, emitter: SingleEmitter<UserListModel>) {val users = userDao.getUsers(page)if (!users.isEmpty()) {emitter.onSuccess(UserListModel(users))} else {emitter.onError(Exception("Device is offline"))}}
}复制代码

getUsers方法中,我们创建一个Single,它会发送users或一个error。 根据不同的条件,shouldUpdate方法决定用户是否应该从网络加载或从本地数据库加载。

还有一点需要注意的是CalendarWrapper字段。 这是一个简单的包装器,有一个返回当前时间的方法。 在它帮助下,我们可以模拟我们测试的时间。

那么我们应该在这里测试什么? 在这里最重要的测试是在shouldUpdate方法背后的逻辑。 让我们为它做一些测试。

测试这个的方法是先调用getUsers方法,并在返回的Single去调用test方法。 test方法会创建一个TestObserver并将其订阅到Single

TestObserver是一种特殊类型的Observer,它记录事件并允许对它们进行断言。

我们还必须模拟UserRepository的依赖关系,并且存储一些他们的方法来返回指定的数据。 我们可以像在Java中一样使用Mockito,或者使用Niek Haarman的Mockito-Kotlin库。 我们将在这个例子中使用Mockito,但如果您好奇,可以检查Github资料库。

如果我们要使用Mockito的when方法,我们需要把它放在反引号之间,因为它是Kotlin中的保留字。 为了使这看起来更好,我们可以使用as关键字引入具有不同名称的when方法。

import org.mockito.Mockito.`when` as whenever复制代码

现在我们可以使用whenever方法进行stubbing。

class UserRepositoryTest {@Mocklateinit var mockUserService: UserService@Mocklateinit var mockUserDao: UserDao@Mocklateinit var mockConnectionHelper: ConnectionHelper@Mocklateinit var mockPreferencesHelper: PreferencesHelper@Mocklateinit var mockCalendarWrapper: CalendarWrapper@Mocklateinit var mockUserCall: Call<UserListModel>@Mocklateinit var mockUserResponse: Response<UserListModel>lateinit var userRepository: UserRepository@Beforefun setup() {MockitoAnnotations.initMocks(this)userRepository = UserRepository(mockUserService, mockUserDao, mockConnectionHelper, mockPreferencesHelper, mockCalendarWrapper)}@Testfun testGetUsers_isOnlineReceivedOneItem_emitListWithOneItem() {val userListModel = UserListModel(listOf(User()))setUpStubbing(true, 1000 * 60 * 60 * 12 + 1, 0, modelFromUserService = userListModel)val testObserver = userRepository.getUsers(1, false).test()testObserver.assertNoErrors()testObserver.assertValue { userListModelResult: UserListModel -> userListModelResult.items.size == 1 }verify(mockUserDao).insertAll(userListModel.items)}@Testfun testGetUsers_isOfflineOneItemInDatabase_emitListWithOneItem() {val modelFromDatabase = listOf(User())setUpStubbing(false, 1000 * 60 * 60 * 12 + 1, 0, modelFromDatabase = modelFromDatabase)val testObserver = userRepository.getUsers(1, false).test()testObserver.assertNoErrors()testObserver.assertValue { userListModelResult: UserListModel -> userListModelResult.items.size == 1 }}private fun setUpStubbing(isOnline: Boolean, currentTime: Long, lastUpdateTime: Long,modelFromUserService: UserListModel = UserListModel(emptyList()),modelFromDatabase: List<User> = emptyList()) {whenever(mockConnectionHelper.isOnline()).thenReturn(isOnline)whenever(mockCalendarWrapper.getCurrentTimeInMillis()).thenReturn(currentTime)whenever(mockPreferencesHelper.loadLong("last_update_page_1")).thenReturn(lastUpdateTime)whenever(mockUserService.getUsers(1)).thenReturn(mockUserCall)whenever(mockUserCall.execute()).thenReturn(mockUserResponse)whenever(mockUserResponse.body()).thenReturn(modelFromUserService)whenever(mockUserDao.getUsers(1)).thenReturn(modelFromDatabase)}
}复制代码

以上我们可以看到UserRepositoryTest的代码。 我们在这个例子中使用Mockito注解来初始化mocks,但是可以用不同的方法来完成。 每个测试包括3个步骤:

  1. 指定stubbed方法返回什么值。 我们使用setUpStubbing私有方法来避免我们的测试中的样板代码。 我们可以在每个具有不同参数的测试用例中调用此方法,这取决于正在测试的状态。 Kotlin的默认参数在这里非常有用,因为有时我们不需要指定每个参数。
  2. 调用getUsers方法,并通过在返回的Single上调用test方法来获取一个TestObserver。
  3. TestObserver或模拟对象上进行一些断言以验证预期的行为。 在这个例子中,我们使用assertNoErrors方法来验证Single不会发出错误。 我们使用的另一种方法是assertValue。 有了它的帮助,我们可以断言Single发出的值是不是正确。 执行此操作的方式是将lambda传递给assertValue方法,该方法返回一个布尔值。 如果它返回true,则断言将通过。 在这种情况下,我们验证发出的列表包含1个元素。 有很多其他方法可以在TestObserver上做出断言,这些可以在TestObserver的超类BaseTestConsumer的文档中找到。

在此提交中可以找到这些更改:

github.com/kozmi55/Kot…

测试 GetUsers interactor

测试GetUsers interactor的方法类似于我们用来测试UserRepository的方法。

GetUsers是一个非常简单的类,它的目的是将data层中的数据转换为presentation层中的数据。

class GetUsers(private val userRepository: UserRepository) {fun execute(page: Int, forced: Boolean) : Single<List<UserViewModel>> {val usersList = userRepository.getUsers(page, forced)return usersList.map { userListModel: UserListModel? ->val items = userListModel?.items ?: emptyList()items.map { UserViewModel(it.userId, it.displayName, it.reputation, it.profileImage) }}}
}复制代码

我们使用RxJava和Kotlin Collection API中的一些转换来实现想要的结果。

来看看我们的测试长什么样:

class GetUsersTest {@Mocklateinit var mockUserRepository: UserRepositorylateinit var getUsers: GetUsers@Beforefun setup() {MockitoAnnotations.initMocks(this)getUsers = GetUsers(mockUserRepository)}@Testfun testExecute_userListModelWithOneItem_emitListWithOneViewModel() {val userListModel = UserListModel(listOf(User(1, "Name", 100, "Image url")))setUpStubbing(userListModel)val testObserver = getUsers.execute(1, false).test()testObserver.assertNoErrors()testObserver.assertValue { userViewModels: List<UserViewModel> -> userViewModels.size == 1 }testObserver.assertValue { userViewModels: List<UserViewModel> ->userViewModels.get(0) == UserViewModel(1, "Name", 100, "Image url") }}@Testfun testExecute_userListModelEmpty_emitEmptyList() {val userListModel = UserListModel(emptyList())setUpStubbing(userListModel)val testObserver = getUsers.execute(1, false).test()testObserver.assertNoErrors()testObserver.assertValue { userViewModels: List<UserViewModel> -> userViewModels.isEmpty() }}private fun setUpStubbing(userListModel: UserListModel) {val fakeSingle = Single.create { e: SingleEmitter<UserListModel>? ->e?.onSuccess(userListModel) }whenever(mockUserRepository.getUsers(1, false)).thenReturn(fakeSingle)}
}复制代码

唯一的区别在于,我们创建一个假的从getUsers方法返回的Single对象。 我们使用Single将UserListModel发送给setUpStubbing方法,在这里我们创建了假的Single,并将其设置为getUsers方法的返回值。

剩下的代码使用与UserRepositoryTest中相同的概念。

Commit在这:github.com/kozmi55/Kot…

这是第一部分。 我们学习了如何在Kotlin测试中使用RxJava来处理一些常见问题,如何利用一些Kotlin功能来编写更简单的测试,并且还可以看看如何测试Room数据库。

在第二部分中,我将向您展示如何在TestScheduler的帮助下测试Presenter,以及如何使用Espresso和假数据来进行UI测试。 敬请关注。

Thanks for reading my article.

【译】使用Kotlin和RxJava测试MVP架构的完整示例 - 第1部分相关推荐

  1. Android —MVP架构—登录页面示例

    1.MVP的诞生 以下内容都是从安卓的角度分析: 首先要了解什么是MVC架构: 图片来源网络 View:对应XML文件及Activity或fragment,因为许多修改视图的操作在Activity中实 ...

  2. Kotlin+MVP架构仿开眼App---Photogenic

    项目介绍 最近学习了一下kotlin和MVP架构模式,但苦于无处施展,就想着自己写个项目玩玩.整体架构MVP+Retrofit+RxJava2,通用组件模块化,话不多说,先上图,没图一切按骗流量处理! ...

  3. Android官方MVP架构详解

    综述 对于MVP (Model View Presenter)架构是从著名的MVC(Model View Controller)架构演变而来的.而对于Android应用的开发中本身可视为一种MVC架构 ...

  4. 谈谈 Android MVP 架构 | 掘金技术征文

    前言:本文所写的是博主的个人见解,如有错误或者不恰当之处,欢迎私信博主,加以改正!原文链接,demo链接 MVP 架构简介 说起 MVP 架构,相信很多朋友都看过,网上也有很多这方面的资料.博主使用 ...

  5. 关于当前所用的MVP架构的所思所想

    当前只是构想期,说实话,当前还并没有找到很完美的方式来实现,但是害怕自己忘记所以先记下一笔. 注:这只是一个小白的想法,如果大佬看了感觉过于幼稚,请"温柔地进行批评",也很乐意进行 ...

  6. [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)

    简述: 不知道是否有小伙伴还记得我们之前的Effective Kotlin翻译系列,之前一直忙于赶时髦研究Kotlin 1.3中的新特性.把此系列耽搁了,赶完时髦了还是得踏实探究本质和基础,从今天开始 ...

  7. mvp架构 java_GitHub - AnthonyCoder/MvpForJava: 一个可直接快速引入的 Java 版本的模块化 MVP 框架...

    使用Java构建的一个模块化的MVP的项目 个人项目仅供学习使用,感谢以下开放Api: Demo简介 该 Demo 更加详细的封装思路,请点击文章如何既装逼又优雅的设计一个模块化的MVP架构 提供给初 ...

  8. RxJava+Retrofit+MVP+Dagger2

    传说中的谷歌四件套,按顺序来哈~ 2017.2.20更新:对于用了一段时间的谷歌四件套的开发者们来说,基础应该都已经掌握的差不多了,但是四件套确实很博大精深,要想完全掌握,一是要学习使用技巧,二是要在 ...

  9. Android框架式编程之MVP架构

    MVP是Google官方发布的Android开发相关的架构知识.本文要讲解的是一种最基本的MVP的实现方式,它使用手动的依赖注入来提供具有本地和远程数据源的存储库.异步任务处理回调. 基本的MVP示例 ...

最新文章

  1. Redis初学:6(List类型)
  2. ROW_NUMBER() 分页
  3. mysql数据库优化课程---6、mysql结构化查询语言有哪些
  4. 如何保证access_token长期有效
  5. html5中切换图片怎么做,HTML5编程实战之二:用动画的形式切换图片
  6. Windows Server 2003 AD域升级至Windows Server 2008 R2实战案例
  7. Java之抽象类(Abstract Class)与抽象方法(Abstract Method)
  8. 安全合规/等级保护--13--我们通过了等级保护三级认证
  9. 使用composer安装laravel
  10. 计算机桌面调音量的图标不见了,如何解决电脑音量图标不见了
  11. grafana mysql 变量_grafana之Variables变量的使用
  12. kvm多电脑切换器发展史
  13. python批量打印_python 批量打印PDF
  14. ip iq 谐波检测matlab仿真,基于Matlab的低压电力系统谐波检测方法仿真研究
  15. 达梦dmrman dmap备份报[-7103]:创建命名管道失败
  16. 《Python编程:从入门到实践》读书笔记:第6章 字典
  17. 计算机公共基础知识病毒,病毒通过什么途径传播到您的计算机上?
  18. ELO-Merchant-Category-Recommendation(上篇)
  19. maven项目如何打包运行指定java程序(maven-shade-plugin插件的使用)
  20. [机缘参悟-38]:鬼谷子-第五飞箝篇 - 警示之一:有一种杀称为“捧杀”

热门文章

  1. php 的 危 险 参 数
  2. Js_Span 滑动手型鼠标样式
  3. Delphi XE5 常用功具与下载
  4. Create a restful app with AngularJS/Grails(4)
  5. InfBox V7.0 企业绩效助手客户端使用简介
  6. ie和firefox操作table对象的异同
  7. 决策树算法原理(ID3,C4.5)
  8. 《微信跳一跳》安卓手机刷分软件搭建及攻略
  9. 12. 17 哈理工网络赛
  10. BZOJ 2440: [中山市选2011]完全平方数 [容斥原理 莫比乌斯函数]