Kotlin协同程序:测试Android Presenter时切换上下文

我最近在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 。 为了能够unit testing,我创建了一个具有不同的launchAsync实现的launchAsync

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

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

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

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

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

您还可以让演示者不知道UI上下文。 相反,主持人应该没有背景。 演示者应该公开suspendfunction,并让调用者指定上下文。 然后,当您从视图中调用此演示者协同函数时,可以使用UI上下文launch(UI) { presenter.somethingAsync() }调用它。 这样,测试演示者时,您可以使用runBlocking { presenter.somethingAsync() }运行测试

对于其他人来说,这是我最终实现的实现。

 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实现。

对于unit testing:

 /** * 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() } } 

对于仪器测试:

 /** * 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() } } }