本文介绍了如何使用Kotlin协程注入viewModelScope for Android单元测试?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

将CoroutineScope注入ViewModel进行单元测试时,即使生产代码中不需要使用flowOn,也应该使用CoroutineDispatcher进行注入和定义吗?

When the CoroutineScope is injected into a ViewModel for unit tests, should the CoroutineDispatcher also be injected and defined using flowOn even if it is not needed in production code?

在此用例中,生产代码中不需要

flowOn,因为Retrofit处理 SomeRepository.kt Dispatchers.IO上的线程,而viewModelScope返回Dispathers.Main上的数据,默认情况下都是如此.

flowOn is not needed in the production code in this use case as Retrofit handles the threading on Dispatchers.IO in SomeRepository.kt, and the viewModelScope returns the data on Dispathers.Main, both by default.

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

Run a unit test on Android's ViewModel view state values saved in a Kotlin Flow value.

在第一次对CoroutineScope进行硬编码的情况下,单元测试失败.使用viewModelScope以便启动的协程将维持ViewModel的生命周期.但是,viewModelScope是在ViewModel内部创建的,与可在ViewModel外部定义并作为参数传递的CoroutineDispatcher相比,它的注入更加复杂.

The unit test is failing on the first occurrence where a CoroutineScope is hardcoded. viewModelScope is utilized so that the coroutine launched will maintain the lifecycle of the ViewModel. However, viewModelScope is created from within the ViewModel, which makes it more complicated to inject compared to a CoroutineDispatcher that can be defined outside the ViewModel and passed in as an argument.

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测试扩展.

Note: A JUnit 5 test extension will be used in the final version.

推荐答案

在创建ViewModel时注入并确定CoroutineScope

在生产中,将使用null coroutineScopeProvider创建ViewModel,因为使用了ViewModel的viewModelScope.为了进行测试,将TestCoroutineScope作为ViewModel参数传递.

Inject and determine CoroutineScope on ViewModel creation

In production, the ViewModel is created with a null coroutineScopeProvider, as the ViewModel's viewModelScope is used. For testing, TestCoroutineScope is passed as the ViewModel argument.

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()
        ...
    }
}

这篇关于如何使用Kotlin协程注入viewModelScope for Android单元测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-29 21:01