协同程序单元测试单独通过,但当 运行 一起时则不通过
Coroutines unit tests pass individually but not when run together
我有两个协程测试,它们在单独 运行 时都通过了,但是如果我 运行 它们在一起,第二个总是失败(即使我将它们调换!)。我得到的错误是:
Wanted but not invoked: observer.onChanged([SomeObject(someValue=test2)]);
Actually, there were zero interactions with this mock.
关于协同程序(或一般测试),可能有一些我不了解的基本知识,并且做错了。
如果我调试测试,我发现失败的测试没有等待内部 runBlocking
完成。实际上,我首先拥有内部 runBlocking
的原因是为了解决这个确切的问题,它似乎适用于个别测试。
关于为什么会发生这种情况有什么想法吗?
测试class
@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()
}
}
当您使用 launch
时,您正在创建一个协程,它将 异步执行 。使用 runBlocking
对此没有任何影响。
你的测试失败了,因为你的发射中的东西会发生,但还没有发生。
在执行任何断言之前确保启动已执行的最简单方法是对它们调用 .join()
。
fun someLaunch() : Job = launch {
foo()
}
@Test
fun `test some launch`() = runBlocking {
someLaunch().join()
verify { foo() }
}
不是在 ViewModel
中保存个人 Jobs
,在 onCleared()
中,您可以像这样实现 CoroutineScope
:
class MyViewModel : ViewModel(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext : CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
在 CoroutineScope
内发生的所有启动都会成为 CoroutineScope
的子级,因此如果您取消 job
(这实际上是取消 CoroutineScope
),那么您取消在该范围内执行的所有协程。
所以,一旦你清理了你的 CoroutineScope
实现,你就可以让你的 ViewModel
函数只是 return Job
s:
fun loadSomeObjects1() = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
现在您可以使用 .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")))
}
我还注意到您正在为您的 ViewModel
使用 Dispatchers.Main
。这意味着您将默认在主线程上执行所有协程。您应该考虑这是否真的是您想要做的事情。毕竟,Android 中很少有非 UI 的事情需要在主线程上完成,您的 ViewModel 不应该直接操作 UI。
我有两个协程测试,它们在单独 运行 时都通过了,但是如果我 运行 它们在一起,第二个总是失败(即使我将它们调换!)。我得到的错误是:
Wanted but not invoked: observer.onChanged([SomeObject(someValue=test2)]); Actually, there were zero interactions with this mock.
关于协同程序(或一般测试),可能有一些我不了解的基本知识,并且做错了。
如果我调试测试,我发现失败的测试没有等待内部 runBlocking
完成。实际上,我首先拥有内部 runBlocking
的原因是为了解决这个确切的问题,它似乎适用于个别测试。
关于为什么会发生这种情况有什么想法吗?
测试class
@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()
}
}
当您使用 launch
时,您正在创建一个协程,它将 异步执行 。使用 runBlocking
对此没有任何影响。
你的测试失败了,因为你的发射中的东西会发生,但还没有发生。
在执行任何断言之前确保启动已执行的最简单方法是对它们调用 .join()
。
fun someLaunch() : Job = launch {
foo()
}
@Test
fun `test some launch`() = runBlocking {
someLaunch().join()
verify { foo() }
}
不是在 ViewModel
中保存个人 Jobs
,在 onCleared()
中,您可以像这样实现 CoroutineScope
:
class MyViewModel : ViewModel(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext : CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
在 CoroutineScope
内发生的所有启动都会成为 CoroutineScope
的子级,因此如果您取消 job
(这实际上是取消 CoroutineScope
),那么您取消在该范围内执行的所有协程。
所以,一旦你清理了你的 CoroutineScope
实现,你就可以让你的 ViewModel
函数只是 return Job
s:
fun loadSomeObjects1() = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
现在您可以使用 .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")))
}
我还注意到您正在为您的 ViewModel
使用 Dispatchers.Main
。这意味着您将默认在主线程上执行所有协程。您应该考虑这是否真的是您想要做的事情。毕竟,Android 中很少有非 UI 的事情需要在主线程上完成,您的 ViewModel 不应该直接操作 UI。