简体   繁体   English

如何使用带有 runTest 的 Flows 和 LiveData 对 ViewModel 进行单元测试?

[英]How to Unit Test a ViewModel using Flows and LiveData with runTest?

Imagine my ViewModel looks like this:想象一下我的 ViewModel 看起来像这样:

class ViewModelA(
    repository: Repository,
    ioCoroutineDispatcher: CoroutineDispatcher,
) : ViewModel() {
    val liveData : LiveData<String> = repository.getFooBarFlow().asLiveData(ioCoroutineDispatcher)
}

Imagine my Repository implementation looks like this:想象一下我的 Repository 实现如下所示:

class RepositoryImpl : Repository() {
    override fun getFooBarFlow() : Flow<String> = flow {
        emit("foo")
        delay(300)
        emit("bar")
    }
}

How could I unit test the fact that "foo" is emitted immediately by the LiveData , then 300ms later (no more, no less) , that "bar" is emitted by the LiveData ?我如何对LiveData立即发出“foo”,然后300 毫秒后(不多也不少)LiveData发出“bar”这一事实进行单元测试?

You can use JUnit 4 or 5, but you have to use Kotlin 1.6 , kotlinx-coroutines-test 1.6 and runTest {} instead of runBlockingTest {} (I have no issues testing with runBlockingTest {} )您可以使用 JUnit 4 或 5,但您必须使用Kotlin 1.6kotlinx-coroutines-test 1.6runTest {}而不是runBlockingTest {} (我没有使用runBlockingTest {}进行测试)

The new TestCoroutineRule in coroutine-test 1.6 looks like this: coroutine-test 1.6 中的新TestCoroutineRule如下所示:

@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {

    val testCoroutineDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain()
        }
    }

    fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}

runTest behave a lot more differently than runBlockingTest : runTest的行为与runBlockingTest有很大不同:

runTest() will automatically skip calls to delay() and handle uncaught exceptions. runTest() 将自动跳过对 delay() 的调用并处理未捕获的异常。 Unlike runBlockingTest(), it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module.与 runBlockingTest() 不同,它将等待异步回调来处理某些代码在未与测试模块集成的调度程序中运行的情况。 ( source ) 来源

Take note, advanceTimeBy(n) doesn't really advance the virtual coroutine time by n .请注意, advanceTimeBy(n)并没有真正将虚拟协程时间提前 n The difference with the 1.5 version is it won't execute tasks scheduled at n, only tasks scheduled at n - 1 .与 1.5 版本的区别在于它不会执行在 n 安排的任务,只执行在 n - 1 安排的任务 To execute tasks schedule at n, you need to use the new function runCurrent() .n 执行任务计划,您需要使用新的 function runCurrent()

An example implementation for the test would be:测试的一个示例实现是:

@ExperimentalCoroutinesApi
class DetailViewModelTest {

    @get:Rule
    val testCoroutineRule = TestCoroutineRule()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun getFooBarFlow() = testCoroutineRule.runTest {
        // Given
        val fakeRepository = object : Repository {
            override fun getFooBarFlow(): Flow<String> = flow {
                emit("foo")
                delay(300)
                emit("bar")
            }
        }

        // When
        val liveData = ViewModelA(fakeRepository, testCoroutineRule.testCoroutineDispatcher).liveData
        liveData.observeForever { }

        // Then
        runCurrent()
        assertEquals("foo", liveData.value)

        advanceTimeBy(300)
        runCurrent()
        assertEquals("bar", liveData.value)
    }
}

A complete implementation with some helpers can be found here: https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6可以在这里找到带有一些助手的完整实现: https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM