协程单元测试可以单独通过,但不能同时通过

时间:2019-03-15 03:16:00

标签: android kotlin-coroutines

我有两个协程测试,分别运行时都通过,但是如果我一起运行它们,第二个协程测试始终会失败(即使我将它们切换!)。我得到的错误是:

  

希望但不被调用: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")))
    }
}

ViewModel

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

1 个答案:

答案 0 :(得分:3)

使用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()
  }
}

CoroutineScope内发生的所有发射都将成为该CoroutineScope的子项,因此,如果您取消了该job(实际上是在取消CoroutineScope),则您要取消在该范围内执行的所有协程。

因此,一旦清理了CoroutineScope实现,就可以使ViewModel函数只返回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")))
}

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