最近我創(chuàng)建了一個playground項目來了解更多關(guān)于Kotlin和RxJava的信息揍魂。 這是一個非常簡單的項目逊彭,但有一部分,我進(jìn)行了一些嘗試:測試。
在kotlin的測試上可能會有一些陷阱,而且由于它是新出的,所以沒有太多的例子畔乙。 我認(rèn)為分享我的經(jīng)驗幫助你來避免踩坑是一個好主意。
關(guān)于架構(gòu)
該應(yīng)用程序遵循基本MVP架構(gòu)翩概。 它使用Dagger2進(jìn)行依賴注入牲距,RxJava2用于數(shù)據(jù)流返咱。
這些庫根據(jù)不同的條件提供來自網(wǎng)絡(luò)或本地存儲的數(shù)據(jù)。 我們使用Retrofit進(jìn)行網(wǎng)絡(luò)請求牍鞠,以及Room作為本地數(shù)據(jù)庫咖摹。
我不會詳細(xì)講解架構(gòu)和這些工具。 我想大多數(shù)人已經(jīng)熟悉了他們难述。 您可以在此提交中查看:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/ca29cad1973cd434ffb0b0d23c4465fc54e05c0b
我們將從測試數(shù)據(jù)庫開始萤晴,然后向上層測試。
測試數(shù)據(jù)庫
對于數(shù)據(jù)庫胁后,我們使用Android架構(gòu)組件中的Room Persistence Library店读。 它是SQLite上的抽象層,可以減少樣板代碼攀芯。
這是最簡單的部分屯断。 我們不需要對Kotlin或RxJava做任何具體的事情。 我們先來看看UserDao界面的代碼侣诺,以決定我們應(yīng)該測試什么殖演。
@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函數(shù)根據(jù)頁碼從數(shù)據(jù)庫中請求下一個30個用戶。
insertAll
插入列表中的所有用戶年鸳。
我們可以從這里發(fā)現(xiàn)幾件事情趴久,需要測試什么:
- 檢查插入的用戶是否與檢索到的用戶相同。
- 檢查檢索用戶正確排序阻星。
- 檢查我們是否插入具有相同ID的用戶朋鞍,它將替換舊的記錄。
- 檢查是否查詢頁面妥箕,最多可以有30個用戶滥酥。
- 檢查我們是否查詢第二頁,我們將獲得正確數(shù)量的元素畦幢。
下面的代碼片段顯示了5例這樣的實現(xiàn)坎吻。
@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方法中,我們需要配置我們的數(shù)據(jù)庫宇葱。 在每次測試之前瘦真,我們使用Room的內(nèi)存數(shù)據(jù)庫創(chuàng)建一個干凈的數(shù)據(jù)庫。
測試在這里非常簡單黍瞧,不需要進(jìn)一步解釋诸尽。 我們在每個測試中遵循的基本模式如
下所示:
- 將數(shù)據(jù)插入數(shù)據(jù)庫
- 從數(shù)據(jù)庫查詢數(shù)據(jù)
- 對所檢索的數(shù)據(jù)作出斷言
我們可以使用Kotlin Collections API中的函數(shù)來簡化測試數(shù)據(jù)的創(chuàng)建,就像這部分代碼一樣:
val users = (1..40L).map { User(it, "Name $it", it *100, "url") }
我們創(chuàng)建了一個范圍印颤,然后將其映射到用戶列表您机。 這里有多個Kotlin概念:范圍,高階函數(shù),字符串模板际看。
Commit: https://github.com/kozmi55/Kotlin-MVP-Testing/commit/8cebc897b642cc843920a107f5f0be15d13a925c
測試UserRepository
對于repository和interactor咸产,我們將使用相同的工具。
- 使用Mockit模擬類的依賴仲闽。
- TestObserver用于測試Observables(在我們的例子中是Singles)
但首先我們需要啟用該選項來mock最終的類脑溢。 在kotlin里,默認(rèn)情況下每個class都是final的赖欣。 幸運的是屑彻,Mockito 2已經(jīng)支持模擬 final class,但是我們需要啟用它畏鼓。
我們需要在以下位置創(chuàng)建一個文本文件:test / resources / mockito-extensions /
酱酬,名稱為org.mockito.plugins.MockMaker
,并附帶以下文本:mock-maker-inline
現(xiàn)在我們可以開始使用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
方法中让禀,我們創(chuàng)建一個Single
,它會發(fā)送users或一個error陨界。 根據(jù)不同的條件巡揍,shouldUpdate
方法決定用戶是否應(yīng)該從網(wǎng)絡(luò)加載或從本地數(shù)據(jù)庫加載。
還有一點需要注意的是CalendarWrapper
字段菌瘪。 這是一個簡單的包裝器腮敌,有一個返回當(dāng)前時間的方法。 在它幫助下俏扩,我們可以模擬我們測試的時間糜工。
那么我們應(yīng)該在這里測試什么? 在這里最重要的測試是在shouldUpdate
方法背后的邏輯录淡。 讓我們?yōu)樗鲆恍y試捌木。
測試這個的方法是先調(diào)用getUsers
方法,并在返回的Single
去調(diào)用test
方法嫉戚。 test
方法會創(chuàng)建一個TestObserver
并將其訂閱到Single
刨裆。
TestObserver是一種特殊類型的Observer,它記錄事件并允許對它們進(jìn)行斷言彬檀。
我們還必須模擬UserRepository
的依賴關(guān)系帆啃,并且存儲一些他們的方法來返回指定的數(shù)據(jù)。 我們可以像在Java中一樣使用Mockito窍帝,或者使用Niek Haarman的Mockito-Kotlin庫努潘。 我們將在這個例子中使用Mockito,但如果您好奇,可以檢查Github資料庫慈俯。
如果我們要使用Mockito的when
方法渤刃,我們需要把它放在反引號之間,因為它是Kotlin中的保留字贴膘。 為了使這看起來更好卖子,我們可以使用as
關(guān)鍵字引入具有不同名稱的when
方法。
import org.mockito.Mockito.`when` as whenever
現(xiàn)在我們可以使用whenever
方法進(jìn)行stubbing刑峡。
class UserRepositoryTest {
@Mock
lateinit var mockUserService: UserService
@Mock
lateinit var mockUserDao: UserDao
@Mock
lateinit var mockConnectionHelper: ConnectionHelper
@Mock
lateinit var mockPreferencesHelper: PreferencesHelper
@Mock
lateinit var mockCalendarWrapper: CalendarWrapper
@Mock
lateinit var mockUserCall: Call<UserListModel>
@Mock
lateinit var mockUserResponse: Response<UserListModel>
lateinit var userRepository: UserRepository
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
userRepository = UserRepository(mockUserService, mockUserDao,
mockConnectionHelper, mockPreferencesHelper,
mockCalendarWrapper)
}
@Test
fun 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)
}
@Test
fun 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個步驟:
- 指定stubbed方法返回什么值诫舅。 我們使用
setUpStubbing
私有方法來避免我們的測試中的樣板代碼。 我們可以在每個具有不同參數(shù)的測試用例中調(diào)用此方法宫患,這取決于正在測試的狀態(tài)刊懈。 Kotlin的默認(rèn)參數(shù)在這里非常有用,因為有時我們不需要指定每個參數(shù)娃闲。 - 調(diào)用
getUsers
方法虚汛,并通過在返回的Single
上調(diào)用test
方法來獲取一個TestObserver。 - 在
TestObserver
或模擬對象上進(jìn)行一些斷言以驗證預(yù)期的行為皇帮。 在這個例子中卷哩,我們使用assertNoErrors
方法來驗證Single
不會發(fā)出錯誤。 我們使用的另一種方法是assertValue
属拾。 有了它的幫助将谊,我們可以斷言Single
發(fā)出的值是不是正確。 執(zhí)行此操作的方式是將lambda傳遞給assertValue
方法渐白,該方法返回一個布爾值尊浓。 如果它返回true,則斷言將通過礼预。 在這種情況下眠砾,我們驗證發(fā)出的列表包含1個元素。 有很多其他方法可以在TestObserver
上做出斷言托酸,這些可以在TestObserver的超類BaseTestConsumer的文檔中找到褒颈。
在此提交中可以找到這些更改:
https://github.com/kozmi55/Kotlin-MVP-Testing/commit/17fc4645bb446879a0e44560c19d6c2c36810a89
測試 GetUsers interactor
測試GetUsers
interactor的方法類似于我們用來測試UserRepository
的方法。
GetUsers
是一個非常簡單的類励堡,它的目的是將data層中的數(shù)據(jù)轉(zhuǎn)換為presentation層中的數(shù)據(jù)谷丸。
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中的一些轉(zhuǎn)換來實現(xiàn)想要的結(jié)果。
來看看我們的測試長什么樣:
class GetUsersTest {
@Mock
lateinit var mockUserRepository: UserRepository
lateinit var getUsers: GetUsers
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
getUsers = GetUsers(mockUserRepository)
}
@Test
fun 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") }
}
@Test
fun 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)
}
}
唯一的區(qū)別在于应结,我們創(chuàng)建一個假的從getUsers
方法返回的Single對象刨疼。 我們使用Single將UserListModel
發(fā)送給setUpStubbing
方法泉唁,在這里我們創(chuàng)建了假的Single,并將其設(shè)置為getUsers
方法的返回值揩慕。
剩下的代碼使用與UserRepositoryTest中相同的概念亭畜。
Commit在這:https://github.com/kozmi55/Kotlin-MVP-Testing/commit/49652a53813f004b2c11f962d8ba5666575365fc
這是第一部分。 我們學(xué)習(xí)了如何在Kotlin測試中使用RxJava來處理一些常見問題迎卤,如何利用一些Kotlin功能來編寫更簡單的測試拴鸵,并且還可以看看如何測試Room數(shù)據(jù)庫。
在第二部分中蜗搔,我將向您展示如何在TestScheduler的幫助下測試Presenter劲藐,以及如何使用Espresso和假數(shù)據(jù)來進(jìn)行UI測試。 敬請關(guān)注樟凄。
Thanks for reading my article.