unit-testing - 如何对 Kotlin 挂起函数进行单元测试

标签 unit-testing kotlin mockito mvp coroutine

我遵循 MVP 模式 + UseCases 与模型层进行交互。这是我要测试的 Presenter 中的一种方法:

fun loadPreviews() {
    launch(UI) {
        val items = previewsUseCase.getPreviews() // a suspending function
        println("[method] UseCase items: $items")

        println("[method] View call")
        view.showPreviews(items)
    }
}

我的简单 BDD 测试:
fun <T> givenSuspended(block: suspend () -> T) = BDDMockito.given(runBlocking { block() })

infix fun <T> BDDMockito.BDDMyOngoingStubbing<T>.willReturn(block: () -> T) = willReturn(block())

@Test
fun `load previews`() {
    // UseCase and View are mocked in a `setUp` method

    val items = listOf<PreviewItem>()
    givenSuspended { previewsUseCase.getPreviews() } willReturn { items }

    println("[test] before Presenter call")
    runBlocking { presenter.loadPreviews() }
    println("[test] after Presenter call")

    println("[test] verify the View")
    verify(view).showPreviews(items)
}

测试成功通过,但日志中有一些奇怪的东西。我希望它是:
  • “[测试]在演示者通话之前”
  • “[方法]用例项:[]”
  • “【方法】查看通话”
  • “[测试]在演示者通话后”
  • "[测试] 验证 View "

  • 但事实证明:
  • [测试] Presenter 通话前
  • [测试] Presenter 通话后
  • [测试] 验证查看
  • [方法] 用例项:[]
  • 【方法】查看电话

  • 这种行为的原因是什么,我应该如何解决它?

    最佳答案

    我发现这是因为 CoroutineDispatcher .我曾经 mock UI EmptyCoroutineContext 的上下文.切换到 Unconfined解决了问题

    更新 02.04.20

    问题的名称表明将有一个详尽的解释如何对挂起函数进行单元测试。所以让我再解释一下。

    测试挂起函数的主要问题是线程。假设我们要测试这个在不同线程中更新属性值的简单函数:

    class ItemUpdater(val item: Item) {
      fun updateItemValue() {
        launch(Dispatchers.Default) { item.value = 42 }
      }
    }
    

    我们需要以某种方式替换Dispatchers.Default与另一个调度员一起仅用于测试目的。我们有两种方法可以做到这一点。每种都有其优点和缺点,选择哪一种取决于您的项目和编码风格:

    1. 注入(inject) Dispatcher .

    class ItemUpdater(
        val item: Item,
        val dispatcher: CoroutineDispatcher  // can be a wrapper that provides multiple dispatchers but let's keep it simple
    ) {
      fun updateItemValue() {
        launch(dispatcher) { item.value = 42 }
      }
    }
    
    // later in a test class
    
    @Test
    fun `item value is updated`() = runBlocking {
      val item = Item()
      val testDispatcher = Dispatchers.Unconfined   // can be a TestCoroutineDispatcher but we still keep it simple
      val updater = ItemUpdater(item, testDispatcher)
    
      updater.updateItemValue()
    
      assertEquals(42, item.value)
    }
    

    2. 替换调度员。

    class ItemUpdater(val item: Item) {
      fun updateItemValue() {
        launch(DispatchersProvider.Default) { item.value = 42 }  // DispatchersProvider is our own global wrapper
      }
    }
    
    // later in a test class
    
    // -----------------------------------------------------------------------------------
    // --- This block can be extracted into a JUnit Rule and replaced by a single line ---
    // -----------------------------------------------------------------------------------
    @Before
    fun setUp() {
      DispatchersProvider.Default = Dispatchers.Unconfined
    }
    
    @After
    fun cleanUp() {
      DispatchersProvider.Default = Dispatchers.Default
    }
    // -----------------------------------------------------------------------------------
    
    @Test
    fun `item value is updated`() = runBlocking {
      val item = Item()
      val updater = ItemUpdater(item)
    
      updater.updateItemValue()
    
      assertEquals(42, item.value)
    }
    

    他们都在做同样的事情——他们替换了原来的Dispatchers.Default。在测试课上。唯一的区别是他们如何做到这一点。选择哪一个真的完全取决于你,所以不要被我自己的想法所左右。

    恕我直言:第一种方法有点太麻烦了。到处注入(inject)调度程序将导致污染大多数类的构造函数,并带有额外的DispatchersWrapper。仅用于测试目的。然而谷歌recommends this way至少现在。第二种风格使事情变得简单,并且不会使生产类复杂化。这就像 RxJava 的测试方式,您必须通过 RxJavaPlugins 替换调度程序。顺便说一句,kotlinx-coroutines-test will bring the exact same functionality future 的某一天。

    关于unit-testing - 如何对 Kotlin 挂起函数进行单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49865054/

    相关文章:

    java - 使用 ArgumentCaptor<List> 和 hamcrest.hasSize

    java - Spring @Value 注释不适用于 mockito 模拟

    android - 如何从使用 PowerMockRunner 运行的 android 单元测试加载 json 文件?

    安卓。如何从 Espresso 测试库中滑动 NavigationDrawer?

    javascript - Jasmine - 如何监视函数内的函数调用?

    android - 如何通过将其他对象字段保存为索引来按对象字段对 MutableMap 进行排序?

    mockito - 是否可以在 Kotlin 中通过 Mockito 模拟访问器?

    android - 更新到 3.1.0 后 ObservableField 返回 null

    java - 如何让jOOQ在单元测试时使用测试表?

    python - Django-Rest-Framework JWT 单元测试说 "No Authentication Provided"