代码之家  ›  专栏  ›  技术社区  ›  Michael Vescovo

协同程序单元测试单独通过,但在一起运行时不通过

  •  0
  • Michael Vescovo  · 技术社区  · 5 年前

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

    需要但未调用:observer.onChanged([SomeObject(someValue=test2)]; 实际上,这个模拟没有交互作用。

    对于协同工作(或者一般的测试)和做错事,我可能有一些根本性的不理解。

    如果我调试测试,我会发现失败的测试不会等待内部 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()
        }
    }
    
    0 回复  |  直到 5 年前
        1
  •  3
  •   RBusarow    5 年前

    当你使用 launch ,您正在创建一个将执行 异步 . 使用 runBlocking 不会影响到这一点。

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

    在执行任何断言之前,确保启动已执行的最简单方法是调用 .join() 在他们身上。

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

    而不是拯救个人 Jobs 在你的 ViewModel ,在 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()
      }
    }
    

    所有发生在 协同视野 成为那个的孩子 协同视野 ,所以如果你取消 job (有效地取消了 协同视野 ),然后取消在该范围内执行的所有协同程序。

    所以,一旦你把你的 协同视野 实施,你可以 视图模型 函数只是返回 Job 学生:

    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 为了你的 视图模型 . 这意味着默认情况下,您将在主线程上执行所有协程。你应该考虑一下这是否真的是你想做的事情。毕竟,Android中很少有非UI操作需要在主线程上完成,而且ViewModel不应该直接操作UI。