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
上下文。 相反,主持人应该没有背景。 演示者应该公开suspend
function,并让调用者指定上下文。 然后,当您从视图中调用此演示者协同函数时,可以使用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() } } }