📜  在 Android 中使用 Kotlin 协程和 LiveData 对 ViewModel 进行单元测试

📅  最后修改于: 2022-05-13 01:55:22.060000             🧑  作者: Mango

在 Android 中使用 Kotlin 协程和 LiveData 对 ViewModel 进行单元测试

官方文档说协程是轻量级线程。通过轻量级,这意味着创建协程不会分配新线程。相反,他们使用预定义的线程池和智能调度来执行下一个任务以及稍后执行哪些任务。在本文中,我们将学习如何使用 Kotlin Coroutines 和 LiveData 为 ViewModel 编写单元测试,并遵循基本的 MVVM 架构。我们正在编写一个单元测试来检查上述数据。为了简单起见,该项目采用了基本的 MVVM 架构。博客中提到的单元测试的完整代码可以在项目本身中找到。

分步实施

这个 SingleNetworkCallViewModel 本质上是一个与 SingleNetworkCallActivity 关联的 ViewModel,这会导致 ViewModel 获取要呈现到 UI 中的用户列表。 SingleNetworkCallViewModel 然后使用 ApiHelper 查询数据层以获取用户列表。如下所示,ViewModel 使用了 Kotlin Coroutines 和 LiveData。

Kotlin
class SingleNetworkCallViewModel(
    private val gfgApi: GfgApi,
    private val gfgDB: DatabaseHelper
) : ViewModel() {
    private val gfgUsers = MutableLiveData>>()
    init {
        fetchGfgUsers()
    }
    private fun fetchGfgUsers() {
        gfgVM.launch {
            gfgUsers.postValue(Resource.loading(null))
            try {
                val gfgUsersFromApi = gfgApi.getGfgUsers()
                gfgUsers.postValue(Resource.success(gfgUsersFromApi))
            } catch (e: Exception) {
                gfgUsers.postValue(Resource.error(e.toString(), null))
            }
        }
    }
    fun getGfgUsers(): LiveData>> {
        return gfgUsers
    }
}


Kotlin
@ExperimentalCoroutinesApi
class GfgCourtine : TestRule {
    private val gfgCourtineDispatcher = GfgCourtineDispatcher()
    private val gfgCourtineDispatcherScope = GfgCourtineDispatcherScope(gfgCourtineDispatcher)
    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(gfgCourtineDispatcher)
            base.evaluate()
            Dispatchers.resetMain()
            gfgCourtineDispatcherScope.cleanupTestCoroutines()
        }
    }
    fun runBlockingTest(block: suspend GfgCourtineDispatcherScope.() -> Unit) =
        gfgCourtineDispatcherScope.runBlockingTest { block() }
}


Kotlin
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class GfgSingle {
    
    @get:Rule
    val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()
    @get:Rule
    val gfgTest = TestCoroutineRule()
    @Mock
    private lateinit var gfgApi: GfgApi
    @Mock
    private lateinit var gfgDBHelper: GfgDBHelper
    @Mock
    private lateinit var apiUsersObserver: Observer>>
    @Before
    fun doSomeSetup() {
        // do something if required
    }
      
    @Test
    fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
        testCoroutineRule.runBlockingTest {
            doReturn(emptyList())
                .`when`(gfgApi)
                .getGfgUser()
            val viewModel = SingleNetworkCallViewModel(gfgApi, gfgDBHelper)
            viewModel.getGfgUser().observeForever(apiUsersObserver)
            verify(gfgApi).getGfgUser()
            verify(apiUsersObserver).onChanged(Resource.success(emptyList()))
            viewModel.getGfgUser().removeObserver(apiUsersObserver)
        }
    }
      
    @Test
    fun givenServerResponseError_whenFetch_shouldReturnError() {
        testCoroutineRule.runBlockingTest {
            val someGeekyError = "Something is not right"
            doThrow(RuntimeException(someGeekyError))
                .`when`(gfgApi)
                .getGfgUser()
            val viewModel = SingleNetworkCallViewModel(gfgApi, gfgDBHelper)
            viewModel.getGfgUser().observeForever(apiUsersObserver)
            verify(gfgApi).getGfgUser()
            verify(apiUsersObserver).onChanged(
                Resource.error(
                    RuntimeException(someGeekyError).toString(),
                    null
                )
            )
            viewModel.getGfgUser().removeObserver(apiUsersObserver)
        }
    }
      
    @After
    fun tearDown() {
        // do something if required
    }
      
}


现在我们必须为这个 ViewModel 编写一个单元测试,它使用 Kotlin Coroutines 和 LiveData。首先,我们要配置测试的依赖,如下图:

testImplementation 'junit:junit:4.12'
testImplementation "org.mockito:mockito-core:3.3.2"
testImplementation 'androidx.arch.core:core-testing:2.1.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2'

确保您使用的是阅读本文时可用的最新版本。这很重要,因为每个版本都包含许多错误修复。让我们继续测试包,我们将在其中编写 ViewModel 单元测试。现在我们必须创建 TestRule,我们将其称为 TestCoroutineRule,并将其放入 utils 包中。

科特林

@ExperimentalCoroutinesApi
class GfgCourtine : TestRule {
    private val gfgCourtineDispatcher = GfgCourtineDispatcher()
    private val gfgCourtineDispatcherScope = GfgCourtineDispatcherScope(gfgCourtineDispatcher)
    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(gfgCourtineDispatcher)
            base.evaluate()
            Dispatchers.resetMain()
            gfgCourtineDispatcherScope.cleanupTestCoroutines()
        }
    }
    fun runBlockingTest(block: suspend GfgCourtineDispatcherScope.() -> Unit) =
        gfgCourtineDispatcherScope.runBlockingTest { block() }
}

为什么要使用前面的 TestRule?

它允许主调度程序在单元测试期间使用 TestCoroutineDispatcher。它在测试后重置并清理。现在,我们将 SingleNetworkCallViewModelTest 添加到测试包中的适当位置,如下所示:

科特林

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class GfgSingle {
    
    @get:Rule
    val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()
    @get:Rule
    val gfgTest = TestCoroutineRule()
    @Mock
    private lateinit var gfgApi: GfgApi
    @Mock
    private lateinit var gfgDBHelper: GfgDBHelper
    @Mock
    private lateinit var apiUsersObserver: Observer>>
    @Before
    fun doSomeSetup() {
        // do something if required
    }
      
    @Test
    fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
        testCoroutineRule.runBlockingTest {
            doReturn(emptyList())
                .`when`(gfgApi)
                .getGfgUser()
            val viewModel = SingleNetworkCallViewModel(gfgApi, gfgDBHelper)
            viewModel.getGfgUser().observeForever(apiUsersObserver)
            verify(gfgApi).getGfgUser()
            verify(apiUsersObserver).onChanged(Resource.success(emptyList()))
            viewModel.getGfgUser().removeObserver(apiUsersObserver)
        }
    }
      
    @Test
    fun givenServerResponseError_whenFetch_shouldReturnError() {
        testCoroutineRule.runBlockingTest {
            val someGeekyError = "Something is not right"
            doThrow(RuntimeException(someGeekyError))
                .`when`(gfgApi)
                .getGfgUser()
            val viewModel = SingleNetworkCallViewModel(gfgApi, gfgDBHelper)
            viewModel.getGfgUser().observeForever(apiUsersObserver)
            verify(gfgApi).getGfgUser()
            verify(apiUsersObserver).onChanged(
                Resource.error(
                    RuntimeException(someGeekyError).toString(),
                    null
                )
            )
            viewModel.getGfgUser().removeObserver(apiUsersObserver)
        }
    }
      
    @After
    fun tearDown() {
        // do something if required
    }
      
}

在这种情况下,我们使用了 InstantTaskExecutorRule,这是使用 LiveData 测试代码时所必需的。如果我们不使用这个,我们会在Android中得到一个与Looper相关的RuntimeException。我们模拟了 ApiHelperDatabaseHelper和其他组件并编写了两个测试:

  1. 当服务器返回 200 时,UI 层应该会收到成功。
  2. 当服务器返回错误时,UI 层也应该收到错误。

首先,我们模拟了 ApiHelper,使其返回成功并返回一个空列表。然后我们获取并验证。同样,在第二个中,我们模拟了 ApiHelper 以返回错误。然后我们获取并验证。我们已经成功地模拟了数据,现在我们准备好使用所有这些方法了!