单元测试协程 runBlockingTest:此作业尚未完成

sun*_*ns9 37 android unit-testing kotlin mockk kotlin-coroutines

请在下面找到一个使用协程替换回调的函数:

override suspend fun signUp(authentication: Authentication): AuthenticationError {
    return suspendCancellableCoroutine {
        auth.createUserWithEmailAndPassword(authentication.email, authentication.password)
            .addOnCompleteListener(activityLifeCycleService.getActivity()) { task ->
                if (task.isSuccessful) {
                    it.resume(AuthenticationError.SignUpSuccess)
                } else {
                    Log.w(this.javaClass.name, "createUserWithEmail:failure", task.exception)
                    it.resume(AuthenticationError.SignUpFail)
                }
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我想对这个函数进行单元测试。我正在使用 Mockk :

  @Test
  fun `signup() must be delegated to createUserWithEmailAndPassword()`() = runBlockingTest {

      val listener = slot<OnCompleteListener<AuthResult>>()
      val authentication = mockk<Authentication> {
        every { email } returns "email"
        every { password } returns "pswd"
      }
      val task = mockk<Task<AuthResult>> {
        every { isSuccessful } returns true
      }

      every { auth.createUserWithEmailAndPassword("email", "pswd") } returns
          mockk {
            every { addOnCompleteListener(activity, capture(listener)) } returns mockk()
          }

    service.signUp(authentication)

      listener.captured.onComplete(task)
    }
Run Code Online (Sandbox Code Playgroud)

不幸的是,由于以下异常,此测试失败: java.lang.IllegalStateException: This job has not completed yet

我试图替换为runBlockingTestrunBlocking但测试似乎在无限循环中等待。

有人可以帮我解决这个 UT 吗?

提前致谢

Mik*_*rin 12

如果进行Flow测试:

  • 不要flow.collect直接在里面使用runBlockingTest。它应该被包裹在launch
  • TestCoroutineScope不要忘记在测试结束时取消。它将停止Flow收集。

例子:

class CoroutinesPlayground {

    private val job = Job()
    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(job + testDispatcher)

    @Test
    fun `play with coroutines here`() = testScope.runBlockingTest {

        val flow = MutableSharedFlow<Int>()

        launch {
            flow.collect { value ->
                println("Value: $value")
            }
        }

        launch {
            repeat(10) { value ->
                flow.emit(value)
                delay(1000)
            }
            job.cancel()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是第一个对测试流程有实际帮助的有用答案。谢谢你! (3认同)

azi*_*ian 8

正如在这篇文章中可以看到的:

此异常通常意味着您的测试中的某些协程被安排在测试范围之外(更具体地说是测试调度程序)。

而不是执行此操作:

private val networkContext: CoroutineContext = TestCoroutineDispatcher()

private val sut = Foo(
  networkContext,
  someInteractor
)

fun `some test`() = runBlockingTest() {
  // given
  ...

  // when
  sut.foo()

  // then
  ...
}
Run Code Online (Sandbox Code Playgroud)

创建一个通过测试调度程序的测试范围:

private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val networkContext: CoroutineContext = testDispatcher

private val sut = Foo(
  networkContext,
  someInteractor
)
Run Code Online (Sandbox Code Playgroud)

然后在测试执行 testScope.runBlockingTest

fun `some test`() = testScope.runBlockingTest {
  ...
}

Run Code Online (Sandbox Code Playgroud)

另请参阅 Craig Russell 的“使用 TestCoroutineDispatcher 进行单元测试协程挂起函数”

  • 在实施解决方案之前,它会说“这项工作尚未完成”,在实施解决方案之后,它也会说同样的话。(但它适用于 Android) (4认同)
  • 是的。我尝试过但失败了。从逻辑上讲,它应该有效。但事实并非如此。 (2认同)

Hei*_*elo 6

这不是官方解决方案,因此请自行承担使用风险。

这类似于@azizbekian 发布的内容,但不是调用runBlocking,而是调用launch。由于这是使用TestCoroutineDispatcher,任何计划立即执行的任务都会立即执行。如果您有多个异步运行的任务,这可能不合适。

它可能并不适合所有情况,但我希望它对简单的情况有所帮助。

您也可以在此处跟进此问题:

如果您知道如何使用现有的runBlockingTest和来解决这个问题runBlocking,请善待并与社区分享。

class MyTest {
    private val dispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(dispatcher)

    @Test
    fun myTest {
       val apiService = mockk<ApiService>()
       val repository = MyRepository(apiService)
       
       testScope.launch {
            repository.someSuspendedFunction()
       }
       
       verify { apiService.expectedFunctionToBeCalled() }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您确实不应该用 Launch 替换 runBlocking。如果测试在协程完成之前完成,则测试可能会给出误报。RunBlocking 的存在就是为了解决这个问题。 (2认同)

Sir*_*Lam 6

根据我的理解,当您在代码块内的代码中使用runBlockingTest { }与启动的调度程序不同的调度程序时,就会发生此异常runBlockingTest { }

因此,为了避免这种情况,您首先必须确保在代码中注入调度程序,而不是在整个应用程序中对其进行硬编码。如果您还没有这样做,则无处可开始,因为您无法将测试调度程序分配给您的测试代码。

然后,在你的 中BaseUnitTest,你应该有这样的东西:

@get:Rule
val coroutineRule = CoroutineTestRule()
Run Code Online (Sandbox Code Playgroud)
@ExperimentalCoroutinesApi
class CoroutineTestRule(
    val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

Run Code Online (Sandbox Code Playgroud)

下一步实际上取决于您如何进行依赖注入。要点是确保您的测试代码coroutineRule.testDispatcher在注入后正在使用。

最后,runBlockingTest { }从此 testDispatcher 调用:

@Test
fun `This should pass`() = coroutineRule.testDispatcher.runBlockingTest {
    //Your test code where dispatcher is injected
}
Run Code Online (Sandbox Code Playgroud)


and*_*aso 6

此问题有一个未解决的问题:https://github.com/Kotlin/kotlinx.coroutines/issues/1204

解决方案是使用 CoroutineScope 代替 TestCoroutinScope 直到问题解决,您可以通过替换

@Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() = 
runBlockingTest {
Run Code Online (Sandbox Code Playgroud)

@Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() = 
runBlocking {
Run Code Online (Sandbox Code Playgroud)


las*_*ase 6

由于协程 API 的频繁更改,这些答案都不适合我的设置。

这特别适用于kotlin-coroutines-test 1.6.0 版本,并作为testImplementation依赖项添加。

    @Test
    fun `test my function causes flow emission`() = runTest {
        // calling this function will result in my flow emitting a value
        viewModel.myPublicFunction("1234")
        
        val job = launch {
            // Force my flow to update via collect invocation
            viewModel.myMemberFlow.collect()
        }
        // immediately cancel job
        job.cancel()

        assertEquals("1234", viewModel.myMemberFlow.value)
    }
Run Code Online (Sandbox Code Playgroud)