Android 协同程序-单元测试viewModelScope.launch方法
我正在为我的viewModel编写单元测试,但在执行测试时遇到了问题。Android 协同程序-单元测试viewModelScope.launch方法,android,kotlin,android-livedata,kotlin-coroutines,Android,Kotlin,Android Livedata,Kotlin Coroutines,我正在为我的viewModel编写单元测试,但在执行测试时遇到了问题。runBlocking{…}块实际上并不等待内部代码完成,这让我感到惊讶 测试失败,因为结果为空。为什么runBlocking{…}不在ViewModel中以阻塞方式运行launchblock 我知道如果我将它转换为一个async方法,返回一个Deferred对象,那么我可以通过调用wait()来获取该对象,或者我可以返回一个Job并调用join()但是,我想通过将ViewModel方法保留为void函数来实现这一点,有什么方
runBlocking{…}
块实际上并不等待内部代码完成,这让我感到惊讶
测试失败,因为结果
为空
。为什么runBlocking{…}
不在ViewModel中以阻塞方式运行launch
block
我知道如果我将它转换为一个async
方法,返回一个Deferred
对象,那么我可以通过调用wait()
来获取该对象,或者我可以返回一个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)
}
})
}
}
}
您遇到的问题并非源于运行阻塞,而是源于LiveData在没有附加观察者的情况下没有传播值 我见过很多处理这种情况的方法,但最简单的方法就是使用
observeForever
和countdownlock
@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
}
@Throws(InterruptedException::class)
有趣的LiveData.getTestValue():T?{
变量值:T?=null
val闩锁=倒计时闩锁(1)
观察者{
值=它
倒数计时
}
等待(2,时间单位秒)
观察者(观察者)
removeObserver(观察员)
返回值
}
你可以这样称呼它:
uiJob {
when (val result = fetchRubyContributorsUseCase.execute()) {
// ... handle result of suspend fun execute() here
}
val result=viewModel.myLiveData.getTestValue()
其他项目将其作为断言库的一部分
有人专门写了LiveData测试
您可能还想调查
或以下项目:
正如其他人提到的,runblocking只会阻止在其作用域中启动的协同程序,它与viewModelScope是分开的。
您可以插入MyDispatchers.Background并将MainDispatchers设置为使用dispatchers.unconfined。您需要做的是将协同程序的启动包装到具有给定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和背景。这里的一切都是顶级+扩展功能
然后在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之类的基类中会更好)我尝试了最上面的答案并成功了,但我不想检查所有的启动,并在测试中添加对main或unconfined的dispatcher引用。因此,我最终将此代码添加到我的基本测试类中。我将调度程序定义为TestCoroutineDispatcher() 在我的基础测试课上,我有
@ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {
@BeforeAll
private fun doOnBeforeAll() {
MockitoAnnotations.initMocks(this)
}
}
正如@Gergely Hegedus一样,需要将CoroutineScope注入到ViewModel中。使用此策略,将CoroutineScope作为参数传递,并为生产提供默认的null
值。对于单元测试,将使用TestCoroutineScope
SomeUtils.kt
SomeViewModel.kt
SomeTest.kt
@experimentalRoutinesAPI
类FeedTest:BeforeAllCallback,AfterAllCallback{
private val testDispatcher=TestCoroutineDispatcher()
私有val testScope=TestCoroutineScope(testDispatcher)
私有val存储库=mockkClass(FeedRepository::class)
私有变量loadNetworkIntent=MutableStateFlow(null)
覆盖以前的乐趣(上下文:ExtensionContext?){
//设置协程调度器。
Dispatchers.setMain(testDispatcher)
}
覆盖乐趣(上下文:ExtensionContext?){
Dispatchers.resetMain()
//重置协程调度器和作用域。
testDispatcher.cleanupTestCoroutines()文件
testScope.cleanupTestCoroutines()文件
}
@试验
fun topCafesPoc()=testDispatcher.runBlockingTest{
...
val viewModel=FeedViewModel(testScope,存储库)
viewmodel.getSomeData()
...
}
}
您不必更改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()
}
}
}
由于更换了主调度器,代码将按顺序执行(您的测试代码,然后查看模型代码,然后启动协同程序)
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()
}
}
上述方法的优点:
runBlocking
等李>
cleanupTestCoroutines()
)延迟的协同程序。对于该测试,应在coroutinetesture.runBlockingTest{}
中运行代码,并使用advanceTimeBy()
移动到未来
请确保您的问题显示了一个最小的、完整的、可验证的示例(),这将使回答您的问题更容易。runBlocking将只等待子协同路由。使用viewModelScope创建的协同路由与runBlocking中的作用域无关。更好的方法是将dispatcher上下文传递给viewmodel,这样您就可以通过测试dispatcher。。在你的测试中!我不认为问题在于实时数据没有传播价值。如果将viewmodel中的执行器更改为同步执行器,则测试通过。因此,它肯定与协同程序有关,
InstantTaskExecutorRule
规则确保livedata发布值instantlyDid是否尝试?InstantTaskExecutorRule只确保AAC同步运行,您仍然需要一个观察者。谢谢@Gergely Hegedus。概述了一个。您是否有任何简单的示例来展示如何组织生产/测试代码?下面是一个将viewModelScope
注入到ViewModel中的策略:。@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()
...
}
}
@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)
}
@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()
}
}
}