Kotlin单元测试中的combine/collect不会在断言之前收集所有项目。

huangapple go评论56阅读模式
英文:

Kotlin Unit-Test for combine/collect doesn't collect all items before assert

问题

这个单元测试中有两个 Kotlin 单元测试。第一个可以正常工作,但第二个不能。也许有人知道为什么?

这个使用一个流的 collect 的单元测试可以工作:

    @Test
    fun testEmission1() = runBlockingTest {
        val flow = MutableSharedFlow<Boolean>(replay = 1)
        val flow2 = MutableSharedFlow<Boolean>(replay = 1)
        val collectedValues = mutableListOf<Boolean>()
        val job: Job = launch {
            flow.collect {
                collectedValues.add(it)
            }
        }

        val job2 = CoroutineScope(testDispatcher).launch {
            // WORKS:
            flow2.collect {
                flow.emit(it)
            }
        }

        flow2.emit(false)
        flow2.emit(true)

        job.cancel()
        job2.cancel()

        val expectedValues = listOf(false, true)
        assertEquals(expectedValues, collectedValues)
    }

但是这个使用两个流的 combine 和 collect 的单元测试,在达到断言语句时只能获取到 collectedValues 中的第一个项并且失败:

    @Test
    fun testEmission2() = runBlockingTest {
        val flow = MutableSharedFlow<Boolean>(replay = 1)
        val flow2 = MutableSharedFlow<Boolean>(replay = 1)
        val flow3 = MutableSharedFlow<Boolean>(replay = 1)
        val collectedValues = mutableListOf<Boolean>()
        val job: Job = launch {
            flow.collect {
                collectedValues.add(it)
            }
        }

        val job2 = CoroutineScope(testDispatcher).launch {
            // 不适用 combine/collect:
            combine(
                flow2,
                flow3,
                transform = { a, b ->
                    Pair(a, b)
                }
            ).collect {
                val (a, b) = it
                flow.emit(a)
            }
        }

        flow3.emit(false)

        flow2.emit(false)
        flow2.emit(true)

        yield() // 似乎没有改变什么
        advanceUntilIdle() // 似乎没有改变什么
        job.cancel()
        job2.cancel()

        val expectedValues = listOf(false, true)
        assertEquals(expectedValues, collectedValues)
    }

我的 testDispatcher 是从单元测试的父类中获取的:

open class BaseCoroutineTest {

    @get:Rule
    val liveDataThreadTestRule = InstantTaskExecutorRule()

    val testDispatcher = TestCoroutineDispatcher()

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun replaceMainDispatcher() {
        Dispatchers.setMain(testDispatcher)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun resetMainDispatcher() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @After
    fun clearMocks() {
        Mockito.framework().clearInlineMocks()
    }
}

我尝试向 flow2 中发射了两个值,这应该触发 combine 中的 collect 两次。但是当单元测试达到断言语句时,collectedValues 中只有一个项。问题似乎出现在 combine 函数的行为中。

我使用的是 Kotlin 1.6.10 版本。

英文:

I have here two Kotlin unit-tests. The first one works, but the second doesn't. Maybe someone knows why?

This unit-test with collect on one flow works:

    @Test
    fun testEmission1() = runBlockingTest {
        val flow = MutableSharedFlow&lt;Boolean&gt;(replay = 1)
        val flow2 = MutableSharedFlow&lt;Boolean&gt;(replay = 1)
        val collectedValues = mutableListOf&lt;Boolean&gt;()
        val job: Job = launch {
            flow.collect {
                collectedValues.add(it)
            }
        }

        val job2 = CoroutineScope(testDispatcher).launch {
            // WORKS:
            flow2.collect {
                flow.emit(it)
            }
        }

        flow2.emit(false)
        flow2.emit(true)

        job.cancel()
        job2.cancel()

        val expectedValues = listOf(false, true)
        assertEquals(expectedValues, collectedValues)
    }

But this unit-test with combine of two flows and collect gets just the first item inside the collectedValues when it reaches the assert statement and fails:

    @Test
    fun testEmission2() = runBlockingTest {
        val flow = MutableSharedFlow&lt;Boolean&gt;(replay = 1)
        val flow2 = MutableSharedFlow&lt;Boolean&gt;(replay = 1)
        val flow3 = MutableSharedFlow&lt;Boolean&gt;(replay = 1)
        val collectedValues = mutableListOf&lt;Boolean&gt;()
        val job: Job = launch {
            flow.collect {
                collectedValues.add(it)
            }
        }

        val job2 = CoroutineScope(testDispatcher).launch {
            // doesn&#39;t work with combine/collect:
            combine(
                flow2,
                flow3,
                transform = { a, b -&gt;
                    Pair(a, b)
                }
            ).collect {
                val (a, b) = it
                flow.emit(a)
            }
        }

        flow3.emit(false)

        flow2.emit(false)
        flow2.emit(true)

        yield() // seems to change nothing
        advanceUntilIdle() // seems to change nothing
        job.cancel()
        job2.cancel()

        val expectedValues = listOf(false, true)
        assertEquals(expectedValues, collectedValues)
    }

My testDispatcher comes from my parent class pof the unit-test:

open class BaseCoroutineTest {

    @get:Rule
    val liveDataThreadTestRule = InstantTaskExecutorRule()

    val testDispatcher = TestCoroutineDispatcher()

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun replaceMainDispatcher() {
        Dispatchers.setMain(testDispatcher)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun resetMainDispatcher() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @After
    fun clearMocks() {
        Mockito.framework().clearInlineMocks()
    }
}

I tried to emit two values into flow2 which should trigger the collect on the combine two times. But when the unit-test reaches the assert statement there is just one item inside the collectedValues. The problem seems to be in the behaviour of the combine-function.

I'm using Kotlin 1.6.10

答案1

得分: 1

在尝试了几乎一整天的不同方法后,我找到了解决方案。
也许对其他人有用。

我已经更新了以下软件包:

testImplementation "androidx.test:core:1.5.0" // 从 1.4.0
testImplementation "androidx.arch.core:core-testing:2.2.0" // 从 2.1.0
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1" // 从 1.3.0

我将测试分发器更改为 UnconfinedTestDispatcher

open class BaseCoroutineTest {

    @get:Rule
    val liveDataThreadTestRule = InstantTaskExecutorRule()

    val testDispatcher = UnconfinedTestDispatcher() // 曾是: TestCoroutineDispatcher()

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun replaceMainDispatcher() {
        Dispatchers.setMain(testDispatcher)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun resetMainDispatcher() {
        Dispatchers.resetMain()
        // testDispatcher.cleanupTestCoroutines() // 已注释掉
    }

    @After
    fun clearMocks() {
        Mockito.framework().clearInlineMocks()
    }
}

信息:此更新后应替换 runBlockingTest 函数,因为它已弃用。

英文:

After trying almost a day different approaches, I found a solution.
Maybe it will be useful for someone else.

I've updated the following packages:

testImplementation &quot;androidx.test:core:1.5.0&quot; // from 1.4.0
testImplementation &quot;androidx.arch.core:core-testing:2.2.0&quot; // from 2.1.0
testImplementation &quot;org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1&quot; // from 1.3.0

I changed the testDispatcher to UnconfinedTestDispatcher:

open class BaseCoroutineTest {

    @get:Rule
    val liveDataThreadTestRule = InstantTaskExecutorRule()

    val testDispatcher = UnconfinedTestDispatcher() // was: TestCoroutineDispatcher()

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun replaceMainDispatcher() {
        Dispatchers.setMain(testDispatcher)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun resetMainDispatcher() {
        Dispatchers.resetMain()
        // testDispatcher.cleanupTestCoroutines() // commented out
    }

    @After
    fun clearMocks() {
        Mockito.framework().clearInlineMocks()
    }
}

Info: The runBlockingTest-function should be replaced after this update, because it is deprecated.

huangapple
  • 本文由 发表于 2023年6月5日 15:09:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76404183.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定