最近我创建了一个playground项目来了解更多关于Kotlin和RxJava的信息。 这是一个非常简单的项目,但有一部分,我进行了一些尝试:测试。
在kotlin的测试上可能会有一些陷阱,而且由于它是新出的,所以没有太多的例子。 我认为分享我的经验帮助你来避免踩坑是一个好主意。
注‘Android技术交流群878873098,欢迎大家加入交流,畅谈!本群有免费学习资料视频’并且免费分享源码解析视频
关于架构
该应用程序遵循基本MVP架构。 它使用Dagger2进行依赖注入,RxJava2用于数据流。
这些库根据不同的条件提供来自网络或本地存储的数据。 我们使用Retrofit进行网络请求,以及Room作为本地数据库。
我不会详细讲解架构和这些工具。 我想大多数人已经熟悉了他们。 您可以在此提交中查看:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/ca29cad1973cd434ffb0b0d23c4465fc54e05c0b
我们将从测试数据库开始,然后向上层测试。
测试数据库
对于数据库,我们使用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: UserDao
lateinit var database: AppDatabase
@Before
fun setup() {
val context = InstrumentationRegistry.getTargetContext()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
userDao = database.userDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun testInsertedAndRetrievedUsersMatch() {
val users = listOf(User(1, "Name", 100, "url"), User())
userDao.insertAll(users)
val allUsers = userDao.getUsers(1)
assertEquals(users, allUsers)
}
@Test
fun 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)
}
@Test
fun 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)
}
@Test
fun 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)
}
@Test
fun 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的内存数据库创建一个干净的数据库。
注‘Android技术交流群878873098,欢迎大家加入交流,畅谈!本群有免费学习资料视频’并且免费分享源码解析视频
测试在这里非常简单,不需要进一步解释。 我们在每个测试中遵循的基本模式如
下所示:
- 将数据插入数据库
- 从数据库查询数据
- 对所检索的数据作出断言
我们可以使用Kotlin Collections API中的函数来简化测试数据的创建,就像这部分代码一样:
val users = (1..40L).map { User(it, "Name $it", it *100, "url") }
我们创建了一个范围,然后将其映射到用户列表。 这里有多个Kotlin概念:范围,高阶函数,字符串模板。
Commit: https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8cebc897b642cc843920a107f5f0be15d13a925c
测试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() -> false
else -> {
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
字段。 这是一个简单的包装器,有一个返回当前时间的方法。 在它帮助下,我们可以模拟我们测试的时间。
注‘Android技术交流群878873098,欢迎大家加入交流,畅谈!本群有免费学习资料视频’并且免费分享源码解析视频
作者:ditclear
链接:https://www.jianshu.com/p/6d88998316b1
來源:简书