我正在为我的viewModel编写单元测试,但是在执行测试时遇到了麻烦。runBlocking { ... }
块实际上并没有等待里面的代码完成,这让我很惊讶。
测试失败是因为result
是null
。为什么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
}
}
8条答案
按热度按时间mkshixfv1#
正如其他人提到的,runblocking只会阻止在它的作用域中启动的协程,它与viewModelScope是分开的。你可以做的是注入你的MyDispatchers.Background,并设置mainDispatcher使用dispatchers.unconfined。
ht4b089n2#
您需要做的是用给定的调度程序将协程的启动封装到一个块中。
注意最上面的ui,io和background,这里的一切都是顶级的扩展函数。
然后在viewModel中,你可以像这样启动协同程序:
在测试中,您需要在@Before块中调用此方法:
型
(将其添加到BaseViewModelTest之类的基类中会更好)
yzckvree3#
作为 @Gergely Hegedusmentions above,需要将CoroutineScope注入到ViewModel中。使用此策略,CoroutineScope将作为参数传递,并带有生产的默认
null
值。对于单元测试,将使用TestCoroutineScope。c86crjj04#
我尝试了最好的答案并成功了,但是我不想检查我所有的启动,并在我的测试中添加一个调度器引用main或unconfined,所以我最终将这段代码添加到我的测试基类中。
在我的基本测试类中
6ju8rftf5#
我确实使用了mockk框架,它有助于模拟viewModelScope示例,如下所示
https://mockk.io/
hwamh0ep6#
有3个步骤,你需要遵循。
1.在Gradle文件中添加依赖关系。
1.创建规则类MainCoroutineRule
1.修改您的测试类以使用ExperimentalCoroutinesApi****runTest和advanceUntilIdle()
有关为什么要这样做的说明,您可以随时参考代码实验室https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3
f3temu5u7#
您遇到的问题不是源于runBlocking,而是源于LiveData在没有附加观察者的情况下不传播值。
我见过许多处理这种情况的方法,但最简单的方法是只使用
observeForever
和一个CountDownLatch
。这种模式非常常见,您可能会在许多项目中看到它的一些变体,如一些测试实用程序类/文件中的函数/方法,例如。
你可以这样称呼它:
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
hl0ma9xz8#
您不必更改ViewModel的代码,唯一需要更改的是在测试ViewModel时正确设置协程作用域(和分派器)。
将以下内容添加到单元测试中:
CoroutineTestRule.kt
由于替换了主调度器,代码将按顺序执行(您的测试代码,然后是视图模型代码,然后是启动的协程)。
上述方法的优点:
1.正常编写测试代码,无需使用
runBlocking
等;1.无论何时在协程中发生崩溃,测试都会失败(因为每次测试后都会调用
cleanupTestCoroutines()
)。1.您可以测试内部使用
delay
的协程。为此,测试代码应在coroutineTestRule.runBlockingTest { }
和advanceTimeBy()
中运行,以便将来使用。