Pre*_*rem 7 android kotlin android-livedata kotlin-coroutines
我正在为我的viewModel编写单元测试,但是执行测试时遇到麻烦。该runBlocking { ... }块实际上并不等待内部代码完成,这令我感到惊讶。
测试失败,因为result是null。为什么不以阻塞方式在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)
您需要做的是将协程的启动包装到具有给定调度程序的块中。
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 这样的基类中会更好)
And*_*tus -1
您遇到的问题不是源于 runBlocking,而是源于 LiveData 在没有附加观察者的情况下不传播值。
我见过很多处理这个问题的方法,但最简单的就是使用observeForever和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)
}
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
| 归档时间: |
|
| 查看次数: |
1094 次 |
| 最近记录: |