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

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

最近我建立了一個playground項目來了解更多關於Kotlin和RxJava的信息。 這是一個很是簡單的項目,但有一部分,我進行了一些嘗試:測試。java

在kotlin的測試上可能會有一些陷阱,並且因爲它是新出的,因此沒有太多的例子。 我認爲分享個人經驗幫助你來避免踩坑是一個好主意。react

關於架構

該應用程序遵循基本MVP架構。 它使用Dagger2進行依賴注入,RxJava2用於數據流。android

這些庫根據不一樣的條件提供來自網絡或本地存儲的數據。 咱們使用Retrofit進行網絡請求,以及Room做爲本地數據庫。git

我不會詳細講解架構和這些工具。 我想大多數人已經熟悉了他們。 您能夠在此提交中查看:github

github.com/kozmi55/Kot…數據庫

咱們將從測試數據庫開始,而後向上層測試。網絡

測試數據庫

對於數據庫,咱們使用Android架構組件中的Room Persistence Library。 它是SQLite上的抽象層,能夠減小樣板代碼。架構

這是最簡單的部分。 咱們不須要對Kotlin或RxJava作任何具體的事情。 咱們先來看看UserDao界面的代碼,以決定咱們應該測試什麼。app

@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的內存數據庫建立一個乾淨的數據庫。

測試在這裏很是簡單,不須要進一步解釋。 咱們在每一個測試中遵循的基本模式如
下所示:

  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
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字段。 這是一個簡單的包裝器,有一個返回當前時間的方法。 在它幫助下,咱們能夠模擬咱們測試的時間。

那麼咱們應該在這裏測試什麼? 在這裏最重要的測試是在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 {

    @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個步驟:

  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 {

    @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)
    }
}複製代碼

惟一的區別在於,咱們建立一個假的從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.

相關文章
相關標籤/搜索