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

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

假设我的 ViewModel 看起来像这样:

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

想象一下我的存储库实现如下所示:

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

我如何对 LiveData立即 发出“foo”这一事实进行单元测试,然后 300 毫秒后(不再,不less),那个“bar”是由 LiveData ?

发出的

您可以使用 JUnit 4 或 5,但您必须使用 Kotlin 1.6kotlinx-coroutines-test 1.6runTest {} 而不是 runBlockingTest {}(我用 runBlockingTest {} 测试没有问题)

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 的行为与 runBlockingTest 有很大不同:

runTest() will automatically skip calls to delay() and handle uncaught exceptions. 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. (source)

请注意,advanceTimeBy(n) 并没有真正将虚拟协程时间提前 n。与1.5版本的区别是它不会执行第n个调度的任务,只执行第n - 1个调度的任务。要在at n 执行任务计划,您需要使用新函数runCurrent().

测试的示例实现是:

@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)
    }
}

可以在此处找到带有一些助手的完整实现:​​https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6