首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Kotlin协程:在测试Android Presenter时切换上下文

Kotlin协程:在测试Android Presenter时切换上下文
EN

Stack Overflow用户
提问于 2018-01-11 18:40:12
回答 4查看 2.5K关注 0票数 3

我最近开始在我的Android项目中使用kotlin协程,但我对它有一些问题。许多人会称其为代码气味。

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

代码语言:javascript
复制
// WorklistPresenter.kt
...
override fun loadWorklist() {
    ...
    launchAsync { mViewModel.getWorklist() }
    ...

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

代码语言:javascript
复制
@Synchronized
protected fun launchAsync(block: suspend CoroutineScope.() -> Unit): Job {
    return launch(UI) { block() }
}

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

代码语言:javascript
复制
protected fun launchAsync(block: suspend CoroutineScope.() -> Unit): Job {
    runBlocking { block() }
    return mock<Job>()
}

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

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2018-01-11 19:22:18

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

代码语言:javascript
复制
class AsyncLauncher{

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

}

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

票数 2
EN

Stack Overflow用户

发布于 2019-03-16 10:41:42

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

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

代码语言:javascript
复制
interface CoroutineContextProvider {
    val main: CoroutineContext
        get() = Dispatchers.Main
    val io: CoroutineContext
        get() = Dispatchers.IO

    class Default : CoroutineContextProvider
}

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

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

代码语言:javascript
复制
class TestingCoroutineContextProvider : CoroutineContextProvider {
    @ExperimentalCoroutinesApi
    override val main: CoroutineContext
        get() = Dispatchers.Unconfined
    @ExperimentalCoroutinesApi
    override val io: CoroutineContext
        get() = Dispatchers.Unconfined
}

在模拟挂起函数时,使用runBlocking调用它,这将确保所有操作都发生在调用线程(您的测试)中。它解释了here (请参阅“无约束调度程序与受限调度程序”一节)。

票数 6
EN

Stack Overflow用户

发布于 2018-01-12 18:21:07

为了让其他人使用,下面是我最终使用的实现。

代码语言:javascript
复制
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委托来避免样板代码:

代码语言:javascript
复制
class SomeInteractor @Inject constructor(private val executor: Executor)
    : Interactor, Executor by executor {
    ...
}

现在可以互换使用Executor-methods:

代码语言:javascript
复制
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实现。

对于单元测试:

代码语言:javascript
复制
/**
 * 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>()
    }
}

对于工具测试:

代码语言:javascript
复制
/**
 * 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()
        }
    }
}
票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/48205095

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档