android 协同程序-单元测试视图ModelScope.启动方法

6za6bjd0  于 2022-12-16  发布在  Android
关注(0)|答案(8)|浏览(265)

我正在为我的viewModel编写单元测试,但是在执行测试时遇到了麻烦。runBlocking { ... }块实际上并没有等待里面的代码完成,这让我很惊讶。
测试失败是因为resultnull。为什么runBlocking { ... }不在ViewModel中以阻塞方式运行launch块?
我知道,如果我将其转换为返回Deferred对象的async方法,那么我可以通过调用await()来获取该对象,或者我可以返回Job并调用join()但是,我希望通过将ViewModel方法保留为void函数来实现这一点,有办法吗?

// MyViewModel.kt

class MyViewModel(application: Application) : AndroidViewModel(application) {

    val logic = Logic()
    val myLiveData = MutableLiveData<Result>()

    fun doSomething() {
        viewModelScope.launch(MyDispatchers.Background) {
            System.out.println("Calling work")
            val result = logic.doWork()
            System.out.println("Got result")
            myLiveData.postValue(result)
            System.out.println("Posted result")
        }
    }

    private class Logic {
        suspend fun doWork(): Result? {
          return suspendCoroutine { cont ->
              Network.getResultAsync(object : Callback<Result> {
                      override fun onSuccess(result: Result) {
                          cont.resume(result)
                      }

                     override fun onError(error: Throwable) {
                          cont.resumeWithException(error)
                      }
                  })
          }
    }
}
// MyViewModelTest.kt

@RunWith(RobolectricTestRunner::class)
class MyViewModelTest {

    lateinit var viewModel: MyViewModel

    @get:Rule
    val rule: TestRule = InstantTaskExecutorRule()

    @Before
    fun init() {
        viewModel = MyViewModel(ApplicationProvider.getApplicationContext())
    }

    @Test
    fun testSomething() {
        runBlocking {
            System.out.println("Called doSomething")
            viewModel.doSomething()
        }
        System.out.println("Getting result value")
        val result = viewModel.myLiveData.value
        System.out.println("Result value : $result")
        assertNotNull(result) // Fails here
    }
}
mkshixfv

mkshixfv1#

正如其他人提到的,runblocking只会阻止在它的作用域中启动的协程,它与viewModelScope是分开的。你可以做的是注入你的MyDispatchers.Background,并设置mainDispatcher使用dispatchers.unconfined。

ht4b089n

ht4b089n2#

您需要做的是用给定的调度程序将协程的启动封装到一个块中。

var ui: CoroutineDispatcher = Dispatchers.Main
var io: CoroutineDispatcher =  Dispatchers.IO
var background: CoroutineDispatcher = Dispatchers.Default

fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(ui) {
        block()
    }
}

fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(io) {
        block()
    }
}

fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(background) {
        block()
    }
}

注意最上面的ui,io和background,这里的一切都是顶级的扩展函数。
然后在viewModel中,你可以像这样启动协同程序:

uiJob {
    when (val result = fetchRubyContributorsUseCase.execute()) {
    // ... handle result of suspend fun execute() here         
}

在测试中,您需要在@Before块中调用此方法:

@ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
    ui = Dispatchers.Unconfined
    io = Dispatchers.Unconfined
    background = Dispatchers.Unconfined
}


(将其添加到BaseViewModelTest之类的基类中会更好)

yzckvree

yzckvree3#

作为 @Gergely Hegedusmentions above,需要将CoroutineScope注入到ViewModel中。使用此策略,CoroutineScope将作为参数传递,并带有生产的默认null值。对于单元测试,将使用TestCoroutineScope。

  • 一些实用工具.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
  • 某个视图模型.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)
    }

}
  • 一些测试.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()
        ...
    }
}
c86crjj0

c86crjj04#

我尝试了最好的答案并成功了,但是我不想检查我所有的启动,并在我的测试中添加一个调度器引用main或unconfined,所以我最终将这段代码添加到我的测试基类中。

class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
    private val mainThreadDispatcher = TestCoroutineDispatcher()

    override fun beforeEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance()
            .setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) = runnable.run()

                override fun postToMainThread(runnable: Runnable) = runnable.run()

                override fun isMainThread(): Boolean = true
            })

        Dispatchers.setMain(mainThreadDispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        ArchTaskExecutor.getInstance().setDelegate(null)
        Dispatchers.resetMain()
    }
}

在我的基本测试类中

@ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {

    @BeforeAll
    private fun doOnBeforeAll() {
        MockitoAnnotations.initMocks(this)
    }
}
6ju8rftf

6ju8rftf5#

我确实使用了mockk框架,它有助于模拟viewModelScope示例,如下所示
https://mockk.io/

viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
hwamh0ep

hwamh0ep6#

有3个步骤,你需要遵循。
1.在Gradle文件中添加依赖关系。

testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1") 
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }

1.创建规则类MainCoroutineRule

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
    TestWatcher() {

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

1.修改您的测试类以使用ExperimentalCoroutinesApi****runTestadvanceUntilIdle()

@OptIn(ExperimentalCoroutinesApi::class) // New addition
    internal class ConnectionsViewModelTest {

    @ExperimentalCoroutinesApi
    @get:Rule
    var mainCoroutineRule = MainCoroutineRule() // New addition
    ...
    @Test
    fun test_abcd() {
        runTest { // New addition
            ...
            val viewModel = MyViewModel()
            viewModel.foo()
            advanceUntilIdle()  // New addition
            verify { mockObject.footlooseFunction() }
        }
    }

有关为什么要这样做的说明,您可以随时参考代码实验室https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3

f3temu5u

f3temu5u7#

您遇到的问题不是源于runBlocking,而是源于LiveData在没有附加观察者的情况下不传播值。
我见过许多处理这种情况的方法,但最简单的方法是只使用observeForever和一个CountDownLatch

@Test
fun testSomething() {
    runBlocking {
        viewModel.doSomething()
    }
    val latch = CountDownLatch(1)
    var result: String? = null
    viewModel.myLiveData.observeForever {
        result = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    assertNotNull(result)
}

这种模式非常常见,您可能会在许多项目中看到它的一些变体,如一些测试实用程序类/文件中的函数/方法,例如。

@Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
    var value: T? = null
    val latch = CountDownLatch(1)
    val observer = Observer<T> {
        value = it
        latch.countDown()
    }
    latch.await(2, TimeUnit.SECONDS)
    observeForever(observer)
    removeObserver(observer)
    return value
}

你可以这样称呼它:
val result = viewModel.myLiveData.getTestValue()
其他项目将其作为Assert库的一部分。
Here is a library某人专门为LiveData测试编写的。
您可能还想了解一下Kotlin Coroutine CodeLab
或以下项目:
https://github.com/googlesamples/android-sunflower
https://github.com/googlesamples/android-architecture-components

hl0ma9xz

hl0ma9xz8#

您不必更改ViewModel的代码,唯一需要更改的是在测试ViewModel时正确设置协程作用域(和分派器)。
将以下内容添加到单元测试中:

@get:Rule
    open val coroutineTestRule = CoroutineTestRule()

    @Before
    fun injectTestCoroutineScope() {
        // Inject TestCoroutineScope (coroutineTestRule itself is a TestCoroutineScope)
        // to be used as ViewModel.viewModelScope fro the following reasons:
        // 1. Let test fail if coroutine launched in ViewModel.viewModelScope throws exception;
        // 2. Be able to advance time in tests with DelayController.
        viewModel.injectScope(coroutineTestRule)
    }

CoroutineTestRule.kt

@Suppress("EXPERIMENTAL_API_USAGE")
    class CoroutineTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() {

    val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher

    override fun apply(
        base: Statement,
        description: Description?
    ) = object : Statement() {

        override fun evaluate() {
            Dispatchers.setMain(dispatcher)
            base.evaluate()

            cleanupTestCoroutines()
            Dispatchers.resetMain()
        }
    }
}

由于替换了主调度器,代码将按顺序执行(您的测试代码,然后是视图模型代码,然后是启动的协程)。
上述方法的优点:
1.正常编写测试代码,无需使用runBlocking等;
1.无论何时在协程中发生崩溃,测试都会失败(因为每次测试后都会调用cleanupTestCoroutines())。
1.您可以测试内部使用delay的协程。为此,测试代码应在coroutineTestRule.runBlockingTest { }advanceTimeBy()中运行,以便将来使用。

相关问题