1. 程式人生 > >【譯】使用Kotlin和RxJava測試MVP架構的完整示例 - 第1部分

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

原文連結:https://android.jlelse.eu/complete-example-of-testing-mvp-architecture-with-kotlin-and-rxjava-part-1-816e22e71ff4

最近我建立了一個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,歡迎大家加入交流,暢談!本群有免費學習資料視訊’並且免費分享原始碼解析視訊

測試在這裡非常簡單,不需要進一步解釋。 我們在每個測試中遵循的基本模式如
下所示:

  1. 將資料插入資料庫
  2. 從資料庫查詢資料
  3. 對所檢索的資料作出斷言

我們可以使用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
來源:簡書