我正在为viewModel编写单元测试,但是执行测试时遇到麻烦。 runBlocking { ... }
块实际上并不等待内部代码完成,这令我感到惊讶。
测试失败,因为result
是null
。为什么runBlocking { ... }
不能以阻塞方式在ViewModel内部运行launch
块?
我知道是否将其转换为返回async
对象的Deferred
方法,然后可以通过调用await()
来获取对象,或者可以返回{{1} },然后致电Job
。 但是,我想通过将ViewModel方法保留为join()
函数来做到这一点,有没有办法做到这一点?
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)
}
})
}
}
}
答案 0 :(得分:2)
作为 @Gergely Hegedus mentions above,CoroutineScope需要注入到ViewModel中。使用此策略,CoroutineScope作为带有默认null
值的参数传递给生产。对于单元测试,将使用TestCoroutineScope。
SomeUtils.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
SomeViewModel.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)
}
}
SomeTest.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()
...
}
}
答案 1 :(得分:1)
正如其他人提到的那样,runblocking只是阻止了在它的作用域中启动的协程,它与您的viewModelScope是分开的。 您可以做的是注入MyDispatchers.Background并将mainDispatcher设置为使用dispatchers.unconfined。
答案 2 :(得分:0)
您需要做的是将协同程序的启动包装到具有给定调度程序的块中。
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这样的基类中要好得多)
答案 3 :(得分:0)
我尝试了最佳答案并工作了,但是我不想遍历所有发布内容,并在测试中添加对main或unconfined的调度程序引用。因此,我最终将此代码添加到了基础测试类中。我将调度程序定义为TestCoroutineDispatcher()
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)
}
}
答案 4 :(得分:0)
您不必更改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()
}
}
}
由于替换了主调度程序,因此代码将按顺序执行(您的测试代码,然后查看模型代码,然后启动协程)。
上述方法的优点:
runBlocking
左右; cleanupTestCoroutines()
)。delay
的协程。为此,应在coroutineTestRule.runBlockingTest { }
中运行测试代码,并使用advanceTimeBy()
迈向未来。答案 5 :(得分:-1)
您遇到的问题不是由于runBlocking,而是由于LiveData在没有附加观察者的情况下没有传播值。
我已经看到了许多解决方法,但是最简单的方法就是只使用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()
您可以这样拨打电话:
&
其他项目使其成为其断言库的一部分。
Here is a library有人专门写了LiveData测试。
您可能还想研究Kotlin Coroutine CodeLab
或以下项目:
https://github.com/googlesamples/android-sunflower
https://github.com/googlesamples/android-architecture-components