协程-单元测试viewModelScope.launch方法 [英] Coroutines - unit testing viewModelScope.launch methods
问题描述
我正在为我的viewModel编写单元测试,但是在执行测试时遇到了麻烦. runBlocking { ... }
块实际上并不等待内部代码完成,这令我感到惊讶.
I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... }
block doesn't actually wait for the code inside to finish, which is surprising to me.
测试失败,因为result
是null
.为什么runBlocking { ... }
不能以阻塞方式运行ViewModel内部的launch
块?
The test fails because result
is null
. Why doesn't runBlocking { ... }
run the launch
block inside the ViewModel in blocking fashion?
我知道是否将其转换为返回Deferred
对象的async
方法,然后可以通过调用await()
来获取对象,或者可以返回Job
并调用join()
. 但是,我想通过将ViewModel方法保留为void
函数来做到这一点,有没有办法做到这一点?
I know if I convert it to a async
method that returns a Deferred
object, then I can get the object by calling await()
, or I can return a Job
and call join()
. But, I'd like to do this by leaving my ViewModel methods as void
functions, is there a way to do this?
// 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
}
}
推荐答案
您需要做的是将协同程序的启动包装到具有给定调度程序的块中.
What you need to do is wrap your launching of a coroutine into a block with given dispatcher.
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和背景.这里的所有内容都是顶级+扩展功能.
Notice ui, io and background at the top. Everything here is top-level + extension functions.
然后在viewModel中像这样启动协程:
Then in viewModel you start your coroutine like this:
uiJob {
when (val result = fetchRubyContributorsUseCase.execute()) {
// ... handle result of suspend fun execute() here
}
在测试中,您需要在@Before块中调用此方法:
And in test you need to call this method in @Before block:
@ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
ui = Dispatchers.Unconfined
io = Dispatchers.Unconfined
background = Dispatchers.Unconfined
}
(将其添加到诸如BaseViewModelTest之类的基类中要好得多)
(Which is much nicer to add to some base class like BaseViewModelTest)
这篇关于协程-单元测试viewModelScope.launch方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!