Юнит тесты при использовании корутин в Android приложении

image

В этой статье не рассматривается принцип работы корутин. Если вы не знакомы с ними, то рекомендуем прочитать введение в kotlinx git repo.

Статья описывает трудности при написании юнит тестов для кода, использующего корутины. В конце мы покажем решение этой проблемы.

Типичная архитектура

Представьте, что у нас есть простая архитектура MVP в приложении. Activity выглядит так:

class ContentActivity : AppCompatActivity(), ContentView {
    private lateinit var textView: TextView
    private lateinit var presenter: ContentPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById(R.id.content_text_view)

        // emulation of dagger
        injectDependencies()

        presenter.onViewInit()
    }

    private fun injectDependencies() {
        presenter = ContentPresenter(ContentRepository(), this)
    }

    override fun displayContent(content: String) {
        textView.text = content
    }
}


// interface for View Presenter communication
interface ContentView {
    fun displayContent(content: String)
}

В Presenter мы используем корутины для асинхронных операций. Репозиторий просто эмулирует выполнение длительного запроса:

// Presenter class
class ContentPresenter(
        private val repository: ContentRepository,
        private val view: ContentView
) {

    fun onViewInit() {
        launch(UI) {
            // move to another Thread
            val content = withContext(CommonPool) {
                repository.requestContent()
            }
            view.displayContent(content)
        }
    }

}

// Repository class
class ContentRepository {

    suspend fun requestContent(): String {
        delay(1000L)
        return "Content"
    }
}

Юнит тесты

Все работает хорошо, но теперь нам нужно протестировать этот код. Хотя мы вводим все зависимости с явным использованием конструктора, протестировать наш код будет не совсем просто.Мы используем библиотеку Mockito для тестирования.

Также стоит обратить внимание на использование функции runBlocking. Это необходимо, чтобы дождаться результата выполнения теста и использовать supsend функции. Код теста выглядит так:

class ContentPresenterTest {
    @Test
    fun `Display content after receiving`() = runBlocking {
        // arrange
        val repository = mock(ContentRepository::class.java)
        val view = mock(ContentView::class.java)
        val presenter = ContentPresenter(repository, view)

        val expectedResult = "Result"
                `when`(repository.requestContent()).thenReturn(expectedResult)

        // act
        presenter.onViewInit()

        // assert
        verify(view).displayContent(expectedResult)
    }
}

Тест выполняется с ошибкой:

org.mockito.exceptions.base.MockitoException: Cannot mock/spy class sample.dev.coroutinesunittests.ContentRepository Mockito cannot mock/spy because : — final class

Нам необходимо добавить ключевое слово open к классу ContentRepository и к методу requestContent(), чтобы библиотека Mockito могла выполнить подмену вызова функции и подмену самого объекта.

open class ContentRepository {

   suspend open fun requestContent(): String {
       delay(1000L)
       return "Content"
   }
}

Тест опять выполняется с ошибкой. На этот раз это произошло из-за того, что контекст корутины UI использует элементы из библеотеки Android.. Так как мы выполняем тесты для JVM, это приводит к ошибке.

Мы нашли готовое решение этой проблемы. Вы можете видеть его по ссылке. Автор решает эту проблему, перемещая логику выполнения корутин в Activity. Нам кажется такой вариант не слишком правильным, т.к. Activity получает ответственность за управление потоками выполнения задач.

Использование класса CoroutineContextProvider

Вот еще одно решение: передать контекст выполнения корутин с помощью конструктора Presenter, а затем использовать этот контекст для запуска корутин. Нам нужно создать класс CoroutineContextProvider

open class CoroutineContextProvider() {
    open val Main: CoroutineContext by lazy { UI }
    open val IO: CoroutineContext by lazy { CommonPool }
}

Он имеет только два поля, которые ссылаются на тот же контекст, что и в предыдущем коде. Сам класс и его поля должны иметь модификатор open, чтобы иметь возможность наследовать этот класс и переопределять значения полей для целей тестирования. Также нам нужно использовать ленивую инициализацию, чтобы присвоить значение только тогда, когда мы будем использовать значение в первый раз. (Иначе класс всегда инициализирует значение UI и тесты по-прежнему падают)

// Presenter class
class ContentPresenter(
        private val repository: ContentRepository,
        private val view: ContentView,
        private val contextPool: CoroutineContextProvider = CoroutineContextProvider()
) {

    fun onViewInit() {
        launch(contextPool.Main) {
            // move to another Thread
            val content = withContext(contextPool.IO) {
                repository.requestContent()
            }
            view.displayContent(content)
        }
    }
}

Последним шагом является создание TestContextProvider и добавление его использования в тест.

Класс TestContextProvider:

class TestContextProvider : CoroutineContextProvider() {
    override val Main: CoroutineContext = Unconfined
    override val IO: CoroutineContext = Unconfined
}

Мы используем контекст Unconfied. Это означает, что корутины выполняются в том же потоке, в котором выполняется остальной код. Он похож на планировщик Trampoline в RxJava.

Наш последний шаг – передать TestContextProvider в конструктор Presenter в тесте:

class ContentPresenterTest {
    @Test
    fun `Display content after receiving`() = runBlocking {
        // arrange
        val repository = mock(ContentRepository::class.java)
        val view = mock(ContentView::class.java)
        val presenter = ContentPresenter(repository, view, TestContextProvider())

        val expectedResult = "Result"
        `when`(repository.requestContent()).thenReturn(expectedResult)

        // act
        presenter.onViewInit()

        // assert
        verify(view).displayContent(expectedResult)
    }
}

На этом всё. После следующего запуска тест выполнится успешно.

Болтовня ничего не стоит – покажи нам код! Пожалуйста – Ссылка на git

Перевод статьи «Mastering Coroutines. Android. Unit Tests»

31/08/2018

2 комментариев к статье "Юнит тесты при использовании корутин в Android приложении"

  1. видеоуроками лучше )

  2. ___123___Юнит тесты при использовании корутин в Android приложении – Devcolibri___123___

Добавить комментарий