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

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

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

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

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

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 мы используем корутины для асинхронных операций. Репозиторий просто эмулирует выполнение длительного запроса:

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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 функции. Код теста выглядит так:

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 могла выполнить подмену вызова функции и подмену самого объекта.

 

1
2
3
4
5
6
7
 open class ContentRepository {

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

 

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

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

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

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

 

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

 

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

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 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:

 

1
2
3
4
class TestContextProvider : CoroutineContextProvider() {
    override val Main: CoroutineContext = Unconfined
    override val IO: CoroutineContext = Unconfined
}

 

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

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

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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.

22/01/2018

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

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

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

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