Модульное тестирование вызова модификации, использующего сопрограммы kotlin

Я использую сопрограммы kotlin и модифицированные сопрограммы kotlin для выполнения сетевых запросов в проекте, над которым я сейчас работаю. Но я не могу понять, как сделать мои юнит-тесты для прохождения логики.

Вот мой код:

class WorklistInteractor @Inject
    constructor(private val worklistRepository: WorklistRepository,
        private val preferenceManager: PreferenceManager)
: NetworkInteractor, WorklistDialogContract.Interactor {

    private var job = Job()

    override fun getWorklist(listener: OnWorklistResultListener) {
        job = launch(UI) {
            val result = async {
                worklistRepository.getWorklist(
                    ip = preferenceManager.worklistIp,
                    port = preferenceManager.worklistPort).awaitResult()
            }.await()

            when (result) {
            //Successful HTTP result
                is Result.Ok -> listener.onWorklistResult(result.value)
            // Any HTTP error
                is Result.Error -> {
                    Timber.e(result.exception, "HTTP error with code %s}", result.exception.code())
                    when(result.exception.code()) {
                        401 -> listener.onInvalidCredentialsFailure()
                        500 -> listener.internalServerError()
                        503 -> listener.noServerResponseFailure()
                        else -> listener.onError(result.exception.cause.toString())
                    }
                }
            // Exception while request invocation
                is Result.Exception -> {
                    Timber.e(result.exception.cause, "Exception with cause %s", result.exception.cause.toString())
                    when(result.exception) {
                        is ConnectException -> listener.connectionRefused()
                        is SocketTimeoutException -> listener.failedToConnectToHost()
                        else -> listener.onError(result.exception.cause.toString())
                    }
                }
            }
        }
    }

    override fun cancel() {
        job.cancel()
    }
}

Вот один из моих юниттестов:

@Test
fun `when worklistquery returns result, pass result back through listeners onWorklistResult`()
        = runBlocking {

    whenever(mWorklistRepositoryMock.getWorklist(anyString(), anyInt(), anyString()))
            .thenReturn(Calls.response(expectedWorklistResult))

    mInteractor.getWorklist(mOnWorklistResultListenerMock)

    verify(mOnWorklistResultListenerMock).onWorklistResult(expectedWorklistResult)
    verifyNoMoreInteractions(mOnWorklistResultListenerMock)
}

Я продолжаю получать следующее сообщение при запуске:

Требуются, но не вызываются: onWorklistResultListener.onWorklistResult(); -> в com.example.dialogs.worklistdialog.WorklistInteractorTest$, когда worklistquery возвращает результат, передать результат обратно через прослушиватели OnWorklistResult()$1.doResume(WorklistInteractorTest.kt:58)

На самом деле с этим макетом не было никаких взаимодействий.


person Bohsen    schedule 04.01.2018    source источник
comment
Я также пытался запустить его как инструментальный тест Android с бегуном AndroidJUnit4, но это тоже не работает.   -  person Bohsen    schedule 04.01.2018
comment
Изменение запуска (UI) на runBlocking делает unittest пройденным!   -  person Bohsen    schedule 04.01.2018


Ответы (2)


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

Поэтому невозможно протестировать код, не работающий в основном потоке. На самом деле, это даже плохая практика. Поскольку нет гарантии, когда код, работающий в другом потоке, завершится (если вообще когда-либо завершится), мы не знаем, в каком состоянии будет основной поток в это время (основной поток может даже больше не существовать, как в случае с помощью средства запуска модульных тестов).

Даже если бы каким-то образом можно было тестировать код, выполняемый в других потоках, каждый запуск теста неизбежно был бы другим (как следствие предыдущего абзаца), и тесты могли бы давать разные результаты при каждом запуске. Это прямо противоречит идеологии надежности тестов и получения одинаковых результатов при каждом запуске.

Поэтому при тестировании весь тестируемый код должен выполняться в основном потоке, и никогда не следует пытаться вызывать какой-либо асинхронный код в тестах.

Неудивительно, что сопрограмма runBlocking запускает код внутри нее блокирующим образом в потоке, из которого она запускается (в отличие от launch, что приведет к асинхронному запуску кода).

Во всем этом есть одна оговорка, что сопрограммы не являются потоками, но если вы замените слово thread в приведенном выше тексте на asynchronous coroutine, текст не потеряет своего значения. То, что я сказал, применимо как при использовании традиционных потоков, так и при использовании сопрограмм.

person Lukas1    schedule 07.01.2018
comment
Просто чтобы уточнить, если вы измените реализацию для использования runBlcoking, вы получите ошибку ANR. - person Bohsen; 08.01.2018
comment
это означает, что вы пытаетесь протестировать какое-то очень сложное ресурсоемкое вычисление... Возможно, ваши макеты не работают так, как вы ожидаете... Из тестового кода, которым вы поделились, не очевидно, какая часть кода выполняет ресурсоемкие вычисления. Вы случайно не используете реальные объекты, которые пытаются записывать в базу данных или читать из базы данных, или выполнять настоящие сетевые запросы? - person Lukas1; 09.01.2018
comment
Я выполняю сетевой запрос, используя модификацию. На самом деле я только что узнал, что если я закончу свой метод с помощью runBlocking { job?.join() }, все мои тесты пройдут. Но у меня возникли проблемы с поиском решения этой проблемы. Может быть связано с этой проблемой - person Bohsen; 09.01.2018
comment
Это не похоже на меня. Я все еще думаю, что ваша проблема в том, что вы выполняете ресурсоемкие вычисления или реальные сетевые вызовы в своих тестах, и это неправильно. Я предлагаю использовать отладчик, чтобы увидеть, что вызывается и что вызывает ANR. Скорее всего, это какой-то сетевой вызов, чтение базы данных или что-то подобное. Вам нужно издеваться над вашими сетевыми вызовами. Еще одна вещь, я не вижу, чтобы вы использовали await() в своем тестовом коде, обернутом внутри runBlocking, и это также может быть проблемой. Корутина ожидает, что каким-то образом закончит работу, await() — один из таких способов. Тест выглядит некорректно. - person Lukas1; 09.01.2018

Пришлось довольно сильно изменить реализацию. Оказывается, использование построителя сопрограмм launch из обычной функции не так просто, если оно не используется из действия/фрагмента Android.

Вместо использования обычной функции я заменил функцию getWorklist() функцией приостановки и использовал withContext coroutine-builder. Вот новая реализация:

override suspend fun getWorklist(listener: OnWorklistResultListener) {
        withContext(CommonPool) {
            Timber.i("Loading worklist")
            val result = worklistRepository.getWorklist(
                    ip = preferenceManager.worklistIp,
                    port = preferenceManager.worklistPort,
                    aeTitle = preferenceManager.worklistAeTitle)
                    .awaitResult()
            when (result) {
            //Successful HTTP result
           ... left out for brevity (it's the same as before) ...

Теперь все тесты проходят.

person Bohsen    schedule 11.01.2018