协程测试何时发出两个事件以及设置和重置状态

问题描述 投票:0回答:1

待测试代码

fun updateAccount() {
    coroutineScope.launch {
        startLoading()
        val isAccountUpdated = updateAccountUseCase()
        if (isAccountUpdated) {
            navigator.navigateUp()
        } else {
            completeLoading()
            // TODO: Show Error
        }
    }
}

上下文

val refreshSignal: MutableSharedFlow<Unit> = MutableSharedFlow(
    replay = 0,
    extraBufferCapacity = 1,
)
fun startLoading() {
    isLoading = true
    refreshSignal.tryEmit(Unit)
}
fun completeLoading() {
    isLoading = false
    refreshSignal.tryEmit(Unit)
}

测试成功

@Test
fun `updateAccount when updateAccountUseCase returns true`() = testScope.runTest {
    whenever(updateAccountUseCase()).thenReturn(true)

    SUT.refreshSignal.test {
        SUT.updateAccount()

        assertEquals(Unit, awaitItem())
        assertEquals(true, SUT.isLoading)
        verify(navigator).navigateUp()
        expectNoEvents()
    }
}

测试失败

@Test
fun `updateAccount when updateAccountUseCase returns false`() = testScope.runTest {
    whenever(updateAccountUseCase()).thenReturn(false)

    SUT.refreshSignal.test {
        SUT.updateAccount()

        assertEquals(Unit, awaitItem())
        assertEquals(true, SUT.isLoading) // This assertion fails as the isLoading is reset by this time
    
        assertEquals(Unit, awaitItem())
        assertEquals(false, SUT.isLoading)
        expectNoEvents()
    }
}

有关调试尝试的其他信息,

  1. 尝试使用
    yield()
    delays()

yield()
不起作用。

使用时

delay()
,

如果产品代码被这样修改,

fun updateAccount() {
    coroutineScope.launch {
        startLoading()
        delay(100) // Added delay here
        val isAccountUpdated = updateAccountUseCase()
        if (isAccountUpdated) {
            navigator.navigateUp()
        } else {
            completeLoading()
            // TODO: Show Error
        }
    }
}

测试方法修改如下,

@Test
fun `updateAccount when updateAccountUseCase returns true`() = testScope.runTest {
    whenever(updateAccountUseCase()).thenReturn(true)

    SUT.refreshSignal.test {
        SUT.updateAccount()

        assertEquals(Unit, awaitItem())
        assertEquals(true, SUT.isLoading)
        advanceUntilIdle()  // Added this advance
        expectNoEvents()
    }

    
    verify(navigator).navigateUp()
}

@Test
fun `updateAccount when updateAccountUseCase returns false`() = testScope.runTest {
    whenever(updateAccountUseCase()).thenReturn(false)

    SUT.refreshSignal.test {
        SUT.updateAccount()

        assertEquals(Unit, awaitItem())
        assertEquals(true, SUT.isLoading) 

        advanceUntilIdle() // Added this advance
        assertEquals(Unit, awaitItem())
        assertEquals(false, SUT.isLoading)
        expectNoEvents()
    }
}
android kotlin unit-testing kotlin-coroutines turbine
1个回答
0
投票

我不确定我是否正确理解了您的示例,但感觉您希望生产者以某种方式等待消费者,因此只有在我们从流程中消费完一个项目后,

isLoading
值才会发生变化。实际上,生产者和消费者是同时运行的。在第二个示例中,生产者调用
startLoading
,然后几乎立即调用
completeLoading
。当消费者获得第一个项目时,生产者已经完成了这两个功能。但这个执行顺序可能无法保证 - 这是生产者和消费者之间的竞争条件。

使用流的同时共享可变状态感觉很奇怪(

isLoading
)。使用流等框架的要点之一是避免共享可变状态,因为这很容易出错并导致上述问题。

典型的解决方案是使用流发出布尔值。我没有提供完整的示例,因为您可能知道此类解决方案,并且出于某种原因您决定不使用它。

© www.soinside.com 2019 - 2024. All rights reserved.