Переключение контекста kotlin coroutine при тестировании Android-презентатора

Недавно я начал использовать kotlin coroutines в своем Android-проекте, но у меня есть некоторые проблемы с ним. Многие назвали бы это запахом кода.

Я использую MVP-архитектуру, где в моем ведущем запускаются сопрограммы:

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

Функция launchAsync реализована таким образом (в моем классе BasePresenter, который расширяется мой класс WorklistPresenter):

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

Проблема заключается в том, что я использую контекст coroutine пользовательского интерфейса, который зависит от платформы Android. Я не могу изменить это на другой контекст coroutine, не запускаясь в ViewRootImpl$CalledFromWrongThreadException . Чтобы иметь возможность тестировать модуль, я создал копию моего BasePresenter с другой launchAsync :

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

Для меня это проблема, потому что теперь мой BasePresenter должен поддерживаться в двух местах. Так что мой вопрос. Как я могу изменить свою реализацию для поддержки простого тестирования?

Я бы рекомендовал извлечь логику launchAsync в отдельный класс, который вы можете просто высмеять в своих тестах.

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

Он должен быть частью вашего конструктора действий, чтобы сделать его сменным.

Вы также можете заставить своего докладчика не знать о контексте UI . Вместо этого ведущий должен быть без контекста. Ведущий должен просто выставить функцию suspend и позволить вызывающим абонентам указать контекст. Затем, когда вы вызываете эту функцию сопроводителя презентаций из представления, вы вызываете ее с launch(UI) { presenter.somethingAsync() } контекста 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 interchange:

 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 следующим образом:

 class SomeInteractorTest { class TestExecutor : Executor { override fun onMainThread(function: () -> Unit) { runBlocking { function.invoke() } } override fun onWorkerThread(function: suspend () -> Unit): Job { runBlocking { function.invoke() } return mock<Job>() } } private val interactor = SomeInteractor(TestExecutor()) @Test fun `test something`() = runBlocking { ... }