【问题标题】:Coroutines unit tests pass individually but not when run together协程单元测试单独通过,但一起运行时不通过
【发布时间】:2019-03-15 03:15:59
【问题描述】:

我有两个协程测试在单独运行时都通过了,但是如果我一起运行它们,第二个总是失败(即使我切换它们!)。我得到的错误是:

需要但未调用:observer.onChanged([SomeObject(someValue=test2)]); 实际上,与此模拟的交互为零。

对于协程(或一般测试)和做错事,我可能不了解一些基本的东西。

如果我调试测试,我发现失败的测试没有等待内部 runBlocking 完成。实际上,我首先使用内部 runBlocking 的原因是为了解决这个确切的问题,它似乎适用于个人测试。

关于为什么会发生这种情况的任何想法?

测试类

@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()
    }
}

【问题讨论】:

    标签: android kotlin-coroutines


    【解决方案1】:

    当您使用launch 时,您正在创建一个将异步执行的协程。使用 runBlocking 不会对此产生任何影响。

    你的测试失败了,因为你的发布中的东西会发生,但还没有发生。

    在执行任何断言之前确保您的启动已执行的最简单方法是对它们调用.join()

    fun someLaunch() : Job = launch {
      foo()
    }
    
    @Test
    fun `test some launch`() = runBlocking {
      someLaunch().join()
    
      verify { foo() }
    }
    

    您可以像这样在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()
      }
    }
    

    CoroutineScope 内发生的所有启动都成为该CoroutineScope 的子级,因此如果您取消该job(这实际上取消了CoroutineScope),那么您将取消在该范围内执行的所有协程。

    所以,一旦你清理了你的 CoroutineScope 实现,你可以让你的 ViewModel 函数只返回 Jobs:

    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")))
    }
    

    我还注意到您将Dispatchers.Main 用作您的ViewModel。这意味着默认情况下您将在主线程上执行所有协程。你应该考虑一下这是否真的是你想做的事情。毕竟,Android 中很少有非 UI 的事情需要在主线程上完成,而且您的 ViewModel 不应该直接操作 UI。

    【讨论】:

    • 哇,谢谢。有用!协程大师只有 28 分。最好为你添加一些:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-08
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多