[英]Coroutines unit tests pass individually but not when run together
I have two coroutines tests that both pass when run individually, but if I run them together the second one always fails (even if I switch them around!).我有两个协程测试,它们在单独运行时都通过,但是如果我一起运行它们,第二个总是失败(即使我切换它们!)。 The error I get is:
我得到的错误是:
Wanted but not invoked: observer.onChanged([SomeObject(someValue=test2)]);
想要但未调用:observer.onChanged([SomeObject(someValue=test2)]); Actually, there were zero interactions with this mock.
实际上,与此模拟的交互为零。
There's probably something fundamental I don't understand about coroutines (or testing in general) and doing something wrong.关于协程(或一般测试)和做错事,我可能不了解一些基本的东西。
If I debug the tests I find that the failing test is not waiting for the inner runBlocking
to complete.如果我调试测试,我会发现失败的测试不会等待内部
runBlocking
完成。 Actually the reason I have the inner runBlocking
in the first place is to solve this exact problem and it seemed to work for individual tests.实际上,我首先使用内部
runBlocking
的原因是为了解决这个确切的问题,它似乎适用于单个测试。
Any ideas as to why this might be happening?关于为什么会发生这种情况的任何想法?
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var mainThreadSurrogate: ExecutorCoroutineDispatcher
@Mock
lateinit var repository: DataSource
@Mock
lateinit var observer: Observer<List<SomeObject>>
private lateinit var viewModel: SomeViewModel
@Before
fun setUp() {
mainThreadSurrogate = newSingleThreadContext("UI thread")
Dispatchers.setMain(mainThreadSurrogate)
viewModel = SomeViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
@Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeobjects1()
}
verify(observer).onChanged(listOf(SomeObject("test1")))
}
@Test
fun `loadObjects2 should get objects2`() = runBlocking {
viewModel.someObjects2.observeForever(observer)
val expectedResult = listOf(SomeObject("test2"))
`when`(repository.getSomeObjects2Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeObjects2()
}
verify(observer).onChanged(listOf(SomeObject("test2")))
}
}
class SomeViewModel constructor(private val repository: DataSource) :
ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var objects1Job: Job? = null
private var objects2Job: Job? = null
val someObjects1 = MutableLiveData<List<SomeObject>>()
val someObjects2 = MutableLiveData<List<SomeObject>>()
fun loadSomeObjects1() {
objects1Job = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
}
fun loadSomeObjects2() {
objects2Job = launch {
val objects2Result = repository.getSomeObjects2Async()
objects2.value = objects2Result
}
}
override fun onCleared() {
super.onCleared()
objects1Job?.cancel()
objects2Job?.cancel()
}
}
class Repository(private val remoteDataSource: DataSource) : DataSource {
override suspend fun getSomeObjects1Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects1Async()
}
override suspend fun getSomeObjects2Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects2Async()
}
}
When you use launch
, you're creating a coroutine which will execute asynchronously .当您使用
launch
,您正在创建一个将异步执行的协程。 Using runBlocking
does nothing to affect that.使用
runBlocking
没有任何影响。
Your tests are failing because the stuff inside your launches will happen , but hasn't happened yet.你的测试失败了,因为你的发布里面的东西会发生,但还没有发生。
The simplest way to ensure that your launches have executed before doing any assertions is to call .join()
on them.在执行任何断言之前确保您的启动已执行的最简单方法是对它们调用
.join()
。
fun someLaunch() : Job = launch {
foo()
}
@Test
fun `test some launch`() = runBlocking {
someLaunch().join()
verify { foo() }
}
Instead of saving off individual Jobs
in your ViewModel
, in onCleared()
you can implement your CoroutineScope
like so:您可以在
onCleared()
中实现您的CoroutineScope
,而不是在ViewModel
中保存单个Jobs
,如下所示:
class MyViewModel : ViewModel(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext : CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
All launches which happen within a CoroutineScope
become children of that CoroutineScope
, so if you cancel that job
(which is effectively cancelling the CoroutineScope
), then you cancel all coroutines executing within that scope.在
CoroutineScope
内发生的所有启动都成为该CoroutineScope
,因此如果您取消该job
(这实际上取消了CoroutineScope
),那么您将取消在该范围内执行的所有协程。
So, once you've cleaned up your CoroutineScope
implementation, you can make your ViewModel
functions just return Job
s:所以,一旦你清理了你的
CoroutineScope
实现,你就可以让你的ViewModel
函数只返回Job
s:
fun loadSomeObjects1() = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
and now you can test them easily with a .join()
:现在您可以使用
.join()
轻松测试它们:
@Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
viewModel.loadSomeobjects1().join()
verify(observer).onChanged(listOf(SomeObject("test1")))
}
I also noticed that you're using Dispatchers.Main
for your ViewModel
.我还注意到您正在将
Dispatchers.Main
用于您的ViewModel
。 This means that you will by default execute all coroutines on the main thread.这意味着默认情况下您将在主线程上执行所有协程。 You should think about whether that's really something that you want to do.
你应该考虑这是否真的是你想做的事情。 After all, very few non-UI things in Android need to be done on the main thread, and your ViewModel shouldn't be manipulating the UI directly.
毕竟,Android 中很少有非 UI 的事情需要在主线程上完成,您的 ViewModel 不应该直接操作 UI。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.