协程-单元测试viewModelScope.launch方法

Pre*_*rem 7 android kotlin android-livedata kotlin-coroutines

我正在为我的viewModel编写单元测试,但是执行测试时遇到麻烦。该runBlocking { ... }块实际上并不等待内部代码完成,这令我感到惊讶。

测试失败,因为resultnull。为什么不以阻塞方式在ViewModel内部runBlocking { ... }运行该launch块?

我知道如果将其转换为async返回Deferred对象的方法,则可以通过调用来获取对象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)
                      }
                  })
          }
    }
}
Run Code Online (Sandbox Code Playgroud)
// 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
    }
}

Run Code Online (Sandbox Code Playgroud)

Sta*_*nzl 5

您需要做的是将协程的启动包装到具有给定调度程序的块中。

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

注意顶部的 ui、io 和背景。这里的一切都是顶级+扩展函数。

然后在 viewModel 中像这样启动你的协程:

uiJob {
    when (val result = fetchRubyContributorsUseCase.execute()) {
    // ... handle result of suspend fun execute() here         
}
Run Code Online (Sandbox Code Playgroud)

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

@ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
    ui = Dispatchers.Unconfined
    io = Dispatchers.Unconfined
    background = Dispatchers.Unconfined
}
Run Code Online (Sandbox Code Playgroud)

(添加到像 BaseViewModelTest 这样的基类中会更好)

  • viewModeScope 的默认调度程序已经是主调度程序@EdgarKhimich (2认同)

And*_*tus -1

您遇到的问题不是源于 runBlocking,而是源于 LiveData 在没有附加观察者的情况下不传播值。

我见过很多处理这个问题的方法,但最简单的就是使用observeForeverCountDownLatch.

@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)
}
Run Code Online (Sandbox Code Playgroud)

这种模式很常见,您可能会看到许多项目在某些测试实用程序类/文件中将其作为函数/方法进行一些变体,例如

@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
}
Run Code Online (Sandbox Code Playgroud)

你可以这样称呼:

val result = viewModel.myLiveData.getTestValue()

其他项目将其作为断言库的一部分。

这是某人编写的专门用于 LiveData 测试的库。

您可能还想查看Kotlin Coroutine CodeLab

或者以下项目:

https://github.com/googlesamples/android-sunflower

https://github.com/googlesamples/android-architecture-components