Чистое использование 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 запускает все родительские тесты Встроенная функция Kotlin не работает должным образом Доступ к полю другого экземпляра того же класса в Котлине Kotlin, как объявить скованные поля с одинаковым типом данных Функция average () в массиве не работает Kotlin – Полезность «вычисленных» свойств var? Почему я не получаю правильный результат, когда я использую оригинальную функцию parseList в Kotlin? Dagger2 и Kotlin запускают неудачные причины: app: compileDebugKotlinAfterJava Актер Котлин к актерскому общению MailCore: результат htmlRenderingOperation всегда равен нулю объекты закрытого класса, загадочно становящиеся нулевыми, если ссылаются на другие объекты-компаньоны Kotlin: Что я могу сделать, если в библиотеке Java есть перегрузка как примитивного, так и вложенного типа?