Как провести модульное тестирование kotlin coroutine с вложенными приостановленными функциями

Ниже приведен фрагмент кода, который я пытаюсь протестировать. Код перед async(CommonPool) первой функцией приостановки можно протестировать, но после этого тесты продолжают давать сбой. Я пробовал использовать runBlocking, но все равно не могу проверить вложенную приостановленную асинхронную функцию.

interface Listener {
        fun onLoading(user: User?)

        fun onSuccess(user: User)
}

execute(listener: Listener) {

        listener.onLoading(null)
        val service = UserService.getInstance(context, "someurl")
        val database = UserDatabase.getInstance(context)

        launch(UI) {

            val user = async(CommonPool) {
                userDatabase.getUser()
            }.await()

            listener.onLoading(user)

            val response = service.getUsersSelf(oauthToken).await()

            async(CommonPool) {database.saveUser(userResponse.data.user)}.await()

            val user = async(CommonPool) {database.getUser()}.await()

            listener.onSuccess(user)
        }
    }

Ниже мой модульный тест, я использую mockito для издевательства над моим слушателем и проверки количества взаимодействий.

@Test
    fun execute() {
        runBlocking {
            userDatabase.saveUser(user)

            val listener = mock(GetUser.Listener::class.java)

            getUser.execute(listener)

            verify(listener, times(1)).onLoading(null) // Success

            verify(listener, times(1)).onLoading(user) // Fails

            verify(listener, times(1)).onSuccess(user) // Fails
        }
    }

Но два последних verify теста не проходят. Может ли кто-нибудь помочь мне с тестированием?


person Omkar Amberkar    schedule 06.04.2018    source источник


Ответы (2)


Ваш execute - это функция "запустил и забыл". Если бы это было сделано с потоками, это было бы Thread { <code> }.start() и полностью избавилось бы от ссылки на поток. Единственное, что у вас осталось, - это экземпляр Listener, который получит уведомление, когда вызов завершится, но не в случае ошибки.

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

Поэтому вы должны реализовать логику модульного тестирования внутри слушателя. Учитывая отсутствие обратного вызова при ошибке, вы можете подождать некоторое ограниченное время для вызова методов. Один подход:

fun execute() {
    val latch = CountDownLatch(3)
    runBlocking {
        val x = object : Listener {

            override fun onLoading(user: User?) {
                if (user == null) {
                    require(latch.count == 3L)
                    latch.countDown()
                } else {
                    require(latch.count == 2L)
                    latch.countDown()
                }
            }

            override fun onSuccess(user: User) {
                require (latch.count == 1L)
                latch.countDown()
            }
        }
    }
    require(latch.await(10, TimeUnit.SECONDS))
}
person Marko Topolnik    schedule 07.04.2018

Вы можете заменить UI Dispatcher на Dispatchers.Unconfined во время тестирования, используя внедрение зависимостей. Этот тест успешен:

@Test
fun test() {
    var result = false
    GlobalScope.launch (Dispatchers.Unconfined) {
        async(Dispatchers.Unconfined) {
            sleep(1000)
            result = true
        }.await()
    }
    assertTrue(result)
}
person findusl    schedule 02.07.2019