10

问题

  1. viewModelScope使用 Kotlin 协程为 Android 单元测试注入的最佳策略是什么?

  2. 当 CoroutineScope 被注入到 ViewModel 进行单元测试时,flowOn即使在生产代码中不需要 CoroutineDispatcher 是否也应该被注入和定义?

flowOn在这个用例中,生产代码中不需要,因为 RetrofitDispatchers.IOSomeRepository.kt中处理线程,并在默认情况下viewModelScope返回数据。Dispathers.Main

预期的

对保存在 Kotlin Flow 值中的 Android 的 ViewModel 视图状态值运行单元测试。

观察到的

带有主调度程序的模块未能初始化。对于测试,可以使用来自 kotlinx-coroutines-test 模块的 Dispatchers.setMain

单元测试在 CoroutineScope 被硬编码的第一次出现时失败。viewModelScope被利用,以便启动的协程将维持 ViewModel 的生命周期。但是,viewModelScope它是从 ViewModel 内部创建的,与可以在 ViewModel 外部定义并作为参数传入的 CoroutineDispatcher 相比,这使得注入更加复杂。

执行

SomeViewModel.kt

fun bindIntents(view: FeedView) {
    view.initStateIntent().onEach {
        initState(view)
    }.launchIn(viewModelScope)        
}

SomeTest.kt

@ExperimentalCoroutinesApi
class SomeTest : BeforeAllCallback, AfterAllCallback {

    private val testDispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(testDispatcher)
    private val repository = mockkClass(FeedRepository::class)
    private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)

    override fun beforeAll(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterAll(context: ExtensionContext?) {
        Dispatchers.resetMain()
        // Reset Coroutine Dispatcher and Scope.
        testDispatcher.cleanupTestCoroutines()
        testScope.cleanupTestCoroutines()
    }

    @Test
    fun topCafesPoc() = testDispatcher.runBlockingTest {
        coEvery {
            repository.getInitialCafes(any())
        } returns mockGetInitialCafes(mockCafesList, SUCCESS)

        val viewModel = FeedViewModel(repository)
        viewModel.bindIntents(object : FeedView {
            @ExperimentalCoroutinesApi
            override fun initStateIntent() = MutableStateFlow(true)

            @ExperimentalCoroutinesApi
            override fun loadNetworkIntent() = loadNetworkIntent.filterNotNull()

            override fun render(viewState: FeedViewState) {
                // TODO: Test viewState
            }

        })
        loadNetworkIntent.value = LoadNetworkIntent(true)
        // TODO
        // assertEquals(4, 2 + 2)
    }
}

注意:最终版本中将使用 JUnit 5 测试扩展。

完整的错误日志

线程“main @coroutine#1”java.lang.IllegalStateException 中的异常:带有 Main 调度程序的模块未能初始化。对于测试,来自 kotlinx-coroutines-test 模块的 Dispatchers.setMain 可以在 kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:113) 在 kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.isDispatchNeeded(MainDispatchers.kt:91) 上使用kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:285) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) at kotlinx.coroutines .AbstractCoroutine.start(AbstractCoroutine.kt:158) at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders. 68)在 com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) 在 com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) 在 com.intellij.rt.junit .JUnitStarter.main(JUnitStarter.java:58) 原因:java.lang.RuntimeException:android.os.Looper 中的方法 getMainLooper 未模拟。看http://g.co/androidstudio/not-mocked详情。在 android.os.Looper.getMainLooper(Looper.java) 在 kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:55) 在 kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:52) 在 kotlinx。 coroutines.internal.MainDispatchersKt.tryCreateDispatcher(MainDispatchers.kt:57) at kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) at kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt: 32) 在 androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) ... 40 更多线程“main @coroutine#1”java.lang.IllegalStateException 中的异常:主调度程序的模块未能初始化。用于测试调度程序。com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) 上的 prepareStreamsAndStart(JUnitStarter.java:230) 原因:java.lang.RuntimeException:android.os.Looper 中的方法 getMainLooper 未模拟。看http://g.co/androidstudio/not-mocked了解详情。在 android.os.Looper.getMainLooper(Looper.java) 在 kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:55) 在 kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:52) 在 kotlinx。 coroutines.internal.MainDispatchersKt.tryCreateDispatcher(MainDispatchers.kt:57) at kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) at kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt: 32) 在 androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) 在 app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:38) ... 39 更多

4

1 回答 1

13

在创建 ViewModel 时注入并确定 CoroutineScope

在生产中,ViewModel 是使用 null 创建的coroutineScopeProvider,因为使用的是 ViewModel viewModelScope。对于测试,TestCoroutineScope作为 ViewModel 参数传递。

SomeUtils.kt

/**
 * Configure CoroutineScope injection for production and testing.
 *
 * @receiver ViewModel provides viewModelScope for production
 * @param coroutineScope null for production, injects TestCoroutineScope for unit tests
 * @return CoroutineScope to launch coroutines on
 */
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
    if (coroutineScope == null) this.viewModelScope
    else coroutineScope

SomeViewModel.kt

class FeedViewModel(
    private val coroutineScopeProvider: CoroutineScope? = null,
    private val repository: FeedRepository
) : ViewModel() {

    private val coroutineScope = getViewModelScope(coroutineScopeProvider)

    fun getSomeData() {
        repository.getSomeDataRequest().onEach {
            // Some code here.            
        }.launchIn(coroutineScope)
    }

}

SomeTest.kt

@ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {

    private val testDispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(testDispatcher)
    private val repository = mockkClass(FeedRepository::class)
    private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)

    override fun beforeAll(context: ExtensionContext?) {
        // Set Coroutine Dispatcher.
        Dispatchers.setMain(testDispatcher)
    }

    override fun afterAll(context: ExtensionContext?) {
        Dispatchers.resetMain()
        // Reset Coroutine Dispatcher and Scope.
        testDispatcher.cleanupTestCoroutines()
        testScope.cleanupTestCoroutines()
    }

    @Test
    fun topCafesPoc() = testDispatcher.runBlockingTest {
        ...
        val viewModel = FeedViewModel(testScope, repository)
        viewmodel.getSomeData()
        ...
    }
}
于 2020-06-11T21:08:44.570 回答