3

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

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

// 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 必须在两个地方进行维护。所以我的问题是。如何更改我的实现以支持简单的测试?

4

4 回答 4

7

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

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

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

    class Default : CoroutineContextProvider
}

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

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

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

当您模拟挂起函数时,用 包裹调用它runBlocking,这将确保所有操作都发生在调用线程(您的测试)中。此处对其进行了解释(请参阅有关“Unconfined 与受限 Dispatcher”的部分)。

于 2019-03-16T02:41:41.727 回答
2

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

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互换使用 -methods:

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()
        }
    }
}
于 2018-01-12T10:21:06.953 回答
2

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

class AsyncLauncher{

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

}

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

于 2018-01-11T11:22:17.923 回答
1

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

于 2018-01-11T12:47:44.980 回答