【问题标题】:Kotlin coroutines: Switching context when testing an Android PresenterKotlin 协程:在测试 Android Presenter 时切换上下文
【发布时间】:2018-01-11 10:40:12
【问题描述】:

我最近开始在我的 Android 项目中使用 kotlin 协程,但我遇到了一些问题。许多人会称之为代码异味。

我正在使用 MVP 架构,其中协程在我的 Presenter 中启动,如下所示:

// WorklistPresenter.kt
...
override fun loadWorklist() {
    ...
    launchAsync { mViewModel.getWorklist() }
    ...

launchAsync函数是这样实现的(在我的 WorklistPresenter 类扩展的 BasePresenter 类中):

@Synchronized
protected fun launchAsync(block: suspend CoroutineScope.() -> Unit): Job {
    return launch(UI) { block() }
}

问题在于我使用了一个依赖于 Android 框架的 UI 协程上下文。如果不遇到ViewRootImpl$CalledFromWrongThreadException,我无法将其更改为另一个协程上下文。为了能够对此进行单元测试,我创建了一个 BasePresenter 的副本,其中包含 launchAsync 的不同实现:

protected fun launchAsync(block: suspend CoroutineScope.() -> Unit): Job {
    runBlocking { block() }
    return mock<Job>()
}

对我来说这是一个问题,因为现在我的 BasePresenter 必须在两个地方进行维护。所以我的问题是。如何更改我的实现以支持简单的测试?

【问题讨论】:

    标签: android unit-testing kotlin kotlinx.coroutines


    【解决方案1】:

    我最近了解了 Kotlin 协程,教我的人向我展示了解决这个问题的好方法。

    您创建一个提供上下文的接口,并使用默认实现:

    interface CoroutineContextProvider {
        val main: CoroutineContext
            get() = Dispatchers.Main
        val io: CoroutineContext
            get() = Dispatchers.IO
    
        class Default : CoroutineContextProvider
    }
    

    然后您将这个 (CoroutineContextProvider.Default()) 手动或使用注入框架注入到您的演示者构造函数中。然后在您的代码中使用它提供的上下文:provider.main; provider.io;或任何你想定义的东西。现在,您可以使用提供程序对象中的这些上下文愉快地使用 launch 和 withContext,知道它可以在您的应用程序中正常工作,但您可以在测试期间提供不同的上下文。

    从您的测试中注入此提供程序的不同实现,其中所有上下文都是Dispatchers.Unconfined

    class TestingCoroutineContextProvider : CoroutineContextProvider {
        @ExperimentalCoroutinesApi
        override val main: CoroutineContext
            get() = Dispatchers.Unconfined
        @ExperimentalCoroutinesApi
        override val io: CoroutineContext
            get() = Dispatchers.Unconfined
    }
    

    当你模拟挂起函数时,用runBlocking 包裹它,这将确保所有操作都发生在调用线程(你的测试)中。它在here 中进行了解释(参见“Unconfined 与受限 Dispatcher”一节)。

    【讨论】:

      【解决方案2】:

      我建议将launchAsync 逻辑提取到一个单独的类中,您可以在测试中简单地模拟它。

      class AsyncLauncher{
      
          @Synchronized
          protected fun execute(block: suspend CoroutineScope.() -> Unit): Job {
              return launch(UI) { block() }
          }
      
      }
      

      它应该是您的活动构造函数的一部分,以使其可替换。

      【讨论】:

      • 我认为最好的解决方案。
      • 对不起,太无聊了?
      • 这不是你的错。有时编程就是 ;)
      • @s1m0nw1 在哪里编写类以及如何使用它。我正在使用 MVVM 模式和编写单元测试用例的新手
      【解决方案3】:

      供其他人使用,这是我最终完成的实现。

      interface Executor {
          fun onMainThread(function: () -> Unit)
          fun onWorkerThread(function: suspend () -> Unit) : Job
      }
      
      object ExecutorImpl : Executor {
          override fun onMainThread(function: () -> Unit) {
              launch(UI) { function.invoke() }
          }
      
          override fun onWorkerThread(function: suspend () -> Unit): Job {
              return async(CommonPool) { function.invoke() }
          }
      }
      

      我在构造函数中注入Executor 并使用 kotlins 委托来避免样板代码:

      class SomeInteractor @Inject constructor(private val executor: Executor)
          : Interactor, Executor by executor {
          ...
      }
      

      现在可以互换使用Executor-方法:

      override fun getSomethingAsync(listener: ResultListener?) {
          job = onWorkerThread {
              val result = repository.getResult().awaitResult()
              onMainThread {
                  when (result) {
                      is Result.Ok -> listener?.onResult(result.getOrDefault(emptyList())) :? job.cancel()
                      // Any HTTP error
                      is Result.Error -> listener?.onHttpError(result.exception) :? job.cancel()
                      // Exception while request invocation
                      is Result.Exception -> listener?.onException(result.exception) :? job.cancel()
                  }
              }
          }
      }
      

      在我的测试中,我用这个切换了Executor 实现。

      对于单元测试:

      /**
       * Testdouble of [Executor] for use in unit tests. Runs the code sequentially without invoking other threads
       * and wraps the code in a [runBlocking] coroutine.
       */
      object TestExecutor : Executor {
          override fun onMainThread(function: () -> Unit) {
              Timber.d("Invoking function on main thread")
              function()
          }
      
          override fun onWorkerThread(function: suspend () -> Unit): Job {
              runBlocking {
                  Timber.d("Invoking function on worker thread")
                  function()
              }
              return mock<Job>()
          }
      }
      

      对于仪器测试:

      /**
       * Testdouble of [Executor] for use in instrumentations tests. Runs the code on the UI thread.
       */
      object AndroidTestExecutor : Executor {
          override fun onMainThread(function: () -> Unit) {
              Timber.d("Invoking function on worker thread")
              function()
      
          }
      
          override fun onWorkerThread(function: suspend () -> Unit): Job {
              return launch(UI) {
                  Timber.d("Invoking function on worker thread")
                  function()
              }
          }
      }
      

      【讨论】:

        【解决方案4】:

        您还可以让演示者不知道 UI 上下文。 相反,演示者应该是无上下文的。 演示者应该只公开suspend 函数并让调用者指定上下文。 然后当你从 View 调用这个 Presenter 协程函数时,你用UI 上下文launch(UI) { presenter.somethingAsync() } 调用它。 这样,在测试演示者时,您可以使用 runBlocking { presenter.somethingAsync() } 运行测试

        【讨论】:

        • 我起初尝试过帽子,但发现因为演示者中的逻辑可能会在 somethingAsync 方法期间调用视图,所以我遇到了 ViewRootImpl$CalledFromWrongThreadExceptions。这只能通过将所有视图方法包装在启动(UI)调用中来解决。
        • 使用公认的解决方案,演示者实际上并不知道上下文。 AsyncLauncher 也可以是一个完全解耦它的接口。
        • 但是通过这种方法,您可以安全地从演示者调用视图。看看这个演示者github.com/deva666/NewsReader/blob/master/app/src/main/kotlin/… 的第一个挂起功能。 Presenter 调用一个协程,在协程之后,它与 View 交互。异步调用 get 之后的所有内容都在 UI 上下文中执行。
        • 我知道这在你的情况下是如何工作的,但在我的情况下它不会。这是因为您的视图包含一些在我的情况下由演示者控制的逻辑(例如 noNetworkErrors)。
        • 顺便说一句,真的很好看的项目。非常容易阅读代码。
        猜你喜欢
        • 1970-01-01
        • 2020-07-19
        • 1970-01-01
        • 2019-08-23
        • 1970-01-01
        • 1970-01-01
        • 2019-06-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多