Чистое использование Coroutines в Котлине с поддержкой тестирования модулей

Поскольку мы работаем с Kotlin, и одна из вещей, на которых мы сейчас фокусируемся, – это использование Coroutines для выполнения операций, которые мы хотим запустить async.

Хотя примеры использования понятны, и это работает, у меня есть некоторые проблемы, которые интегрируют это в чистом виде в нашей архитектуре. При взгляде на реализацию метода для класса, ориентированного на домен, идея состоит в том, что его легко читать, и от асинхронных функций существует как можно меньше «шума». Я знаю, что у меня не может быть асинхронного, без фактического использования. Поэтому написать что-то подобное мне хотелось бы:

val data = someService.getData().await() // work with data 

Но это то, что я хотел бы предотвратить:

 launch(UI) { val data val job = async(CommonPool) { data = someService.getData() } job.await() // work with data } 

Это я хотел бы сочетать с практическими модульными тестами для этих классов, ориентированных на домен, но я не могу заставить их работать. Давайте посмотрим на пример:

 // Some dependency doing heavy work class ApiClient { suspend fun doExpensiveOperation(): String { delay(1000) return "Expensive Result Set" } } // Presenter Class class Presenter(private val apiClient: ApiClient, private val view: TextView) { private lateinit var data: String fun start() { log("Starting Presenter") runBlocking { log("Fetching necessary data") data = apiClient.doExpensiveOperation() log("Received necessary data") } workWithData() log("Started Presenter") } fun workWithData() { log(data) } private fun log(text: String) { view.append(text+"\n") } } // In an Activity val presenter = Presenter(ApiClient(), someTextView) presenter.start() 

Это работает (снимок экрана: https://imgur.com/a/xG9Xw ). Теперь давайте посмотрим на тест.

 class PresenterTest { // ... Declared fields @Before fun setUp() { // Init mocks (apiClient, textView) MockitoAnnotations.initMocks(this) // Set mock responses runBlocking { given(apiClient.doExpensiveOperation()).willReturn("Some Value") } presenter = Presenter(apiClient, textView) } @Test @Throws(Exception::class) fun testThat_whenPresenterStarts_expectedResultShows() { // When presenter.start() // Then Mockito.verify(textView).text = "Some Value\n" } } 

Теперь этот тест менее идеален, но, несмотря на это, он даже не доходит до того, что он может проверить, что все работает так, как предполагалось, потому что данные lateinit var не были инициализированы. Теперь, в конечном счете, эстетика и удобочитаемость наших классов домена – это просто, как далеко я хочу идти, и у меня есть некоторые практические рабочие примеры, для которых я доволен. Но заставить мои тесты работать, кажется, сложно.

Теперь есть разные рецензии в Интернете о таких вещах, но для меня ничего не получилось. Это ( https://medium.com/@tonyowen/android-kotlin-coroutines-unit-test-16e984ba35b4 ) кажется интересным, но мне не нравится идея вызова класса, запускающего контекст для ведущего, потому что это turn имеет зависимость, которая выполняет некоторую асинхронную работу. Хотя в качестве абстрактного представления мне нравится идея «Привет, ведущий, что бы вы ни делали, отчитывайтесь мне в контексте пользовательского интерфейса», скорее он чувствует себя как исправление, чтобы заставить все работать, что приводит к общей заботе о асинхронной функциональности для разных объектов ,

Во всяком случае, мой вопрос: уклоняясь от коротких примеров, есть ли у кого-нибудь какие-либо указания о том, как интегрировать сопрограммы в более крупной архитектуре с рабочими модульными тестами? Я также очень открыт для аргументов, которые заставляют меня изменить способ просмотра вещей, учитывая, что это убедительно на другом уровне, чем «Если вы хотите, чтобы все работало, вы должны пожертвовать». Этот вопрос выходит за рамки простого использования примера, поскольку это всего лишь изолированный пример, в то время как я ищу надежную интеграцию в рамках большого проекта.

Ждем ваших предложений. Заранее спасибо.

Я бы предложил подход с каким-то интерфейсом AsyncRunner и имел две реализации этого интерфейса AsyncRunner . Один из них будет реализован для Android, используя launch(UI) , а другой будет некоторой блокирующей реализацией, используя runBlocking .

Передача правильного типа AsyncRunner в код, выполняемый в приложении, и запуск кода в модульном тесте должны выполняться путем инъекции зависимостей. Тогда в вашем коде вы не будете использовать сопрограммы напрямую, вместо этого вы должны использовать вложенные AsyncRunner для запуска асинхронного кода.

Пример реализации этого AsyncRunner может выглядеть так:

 interface AsyncRunner { fun <T>runAsync(task: () -> T, completion: (T) -> Unit) } class AndroidCoroutineAsyncRunner: AsyncRunner { override fun <T>runAsync(task: () -> T, completion: (T) -> Unit) { launch(UI) { completion(async(CommonPool) { task() }.await()) } } } class BlockingCoroutineAsyncRunner: AsyncRunner { override fun <T>runAsync(task: () -> T, completion: (T) -> Unit) { runBlocking { completion(async(CommonPool) { task() }.await()) } } } 

где параметр task представляет собой код блокировки потока (например, выборку данных из API), а параметр completion будет получать данные из задачи и что-то делать с ними.

Intereting Posts
Обработка аннотации kotlin: проверьте, задан ли TypeElement из класса kotlin Использование Kotlin в библиотечном модуле без использования его в модуле приложения Нет отчета о покрытии для встроенных методов Котлина Как я могу автоматически присвоить переменную переменной по типу в реальном шаблоне? Нулевой указатель в поле зрения, когда запускается проверка андроида. Собственная собственность Котлина Преобразование единиц измерения в километры до миль не возвращает ожидаемый результат Kotlin / Java, строковый массив еще не инициализирован при вызове метода Невозможно установить видимость в группе ограничений Контрактное тестирование DSL неправильное поведение / ошибка Могу ли я использовать два xml-макета для использования одного и того же зрителя с использованием синтетических расширений Kotlin? Единый восклицательный знак в Котлине Классы данных Kotlin содержат открытые функции Android: Kotlin TypeCastException: null не может быть применено к ненулевому типу kotlin.String Anko + Content Provider, не обновляет данные об изменении