简体   繁体   English

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

[英]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?关于为什么会发生这种情况的任何想法?

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

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

Repository存储库

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.

相关问题 ViewModel 单元测试一起运行时失败,但单独运行时通过 - ViewModel unit tests fail when run together but pass when run individually 为什么我的Android单元测试在一起运行时会失败,但是在单独运行时会通过? - Why do my Android unit tests fail when run together, but pass when run individually? 一些Robolectric测试在一起运行时失败但是单独通过 - some Robolectric tests fail when run all together but pass individually Android Studio 协程测试单独通过,一起运行失败 - Android Studio Coroutine Tests Passing individually, failing when run together 我的测试用例在 android studio 中一起运行但单独通过时失败 - My test cases fail when run together but pass individually, at android studio Android:带有协程的片状 ViewModel 单元测试 - Android: flaky ViewModel unit tests with coroutines 为什么带有协程的delay()单元测试失败? - Why Unit tests with coroutines delay() fails? 如何运行协同程序作为单元测试的阻止? - How do I run coroutines as blocking for unit testing? 如何设置Jenkins运行android单元测试? - How to setup Jenkins run android unit tests? 无法在Android Studio中运行单元测试 - Can't run unit tests in Android Studio
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM