Модульное тестирование сопрограммы Kotlin с задержкой

Я пытаюсь выполнить модульное тестирование сопрограммы Kotlin, которая использует delay(). Что касается модульного теста, меня не волнует delay(), он просто замедляет тест. Я хотел бы провести тест таким образом, чтобы на самом деле не задерживалось при вызове delay().

Я попытался запустить сопрограмму, используя настраиваемый контекст, который делегирует CommonPool:

class TestUiContext : CoroutineDispatcher(), Delay {
    suspend override fun delay(time: Long, unit: TimeUnit) {
        // I'd like it to call this
    }

    override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
        // but instead it calls this
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        CommonPool.dispatch(context, block)
    }
}

Я надеялся, что смогу просто вернуться из метода delay() моего контекста, но вместо этого он вызывает мой метод scheduleResumeAfterDelay(), и я не знаю, как делегировать это планировщику по умолчанию.


person Erik Browne    schedule 08.11.2017    source источник
comment
@ s1m0nw1 Да, но я бы предпочел иметь общее решение, и мне не приходилось так изменять свой код.   -  person s1m0nw1    schedule 08.11.2017
comment
@eoinmullan, а можно на такой же минимальный проект git поделить?   -  person Erik Browne    schedule 08.11.2017
comment
И _1_ может быть просто _2_, что сохраняет выполнение в основном потоке.   -  person Tarun Lalwani    schedule 02.03.2018


Ответы (4)


Если вам не нужна задержка, почему бы вам просто не возобновить продолжение вызова по расписанию ?:

class TestUiContext : CoroutineDispatcher(), Delay {
    override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
        continuation.resume(Unit)
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        //CommonPool.dispatch(context, block)  // dispatch on CommonPool
        block.run()  // dispatch on calling thread
    }
}

Таким образом delay() возобновится без задержки. Обратите внимание, что это все еще приостанавливается при задержке, поэтому другие сопрограммы все еще могут работать (например, yield())

@Test
fun `test with delay`() {
    runBlocking(TestUiContext()) {
        launch { println("launched") }
        println("start")
        delay(5000)
        println("stop")
    }
}

Работает без задержек и распечатывает:

start
launched
stop

РЕДАКТИРОВАТЬ:

Вы можете контролировать, где будет запускаться продолжение, настроив функцию dispatch.

person bj0    schedule 02.03.2018
comment
Верно, и это может иметь смысл для тестирования, хотя он, похоже, хотел, чтобы он использовал CommonPool. - person Erik Browne; 03.03.2018
comment
Я ОП, и я бы предпочел использовать тот же поток. Внесите изменения, и я приму ваш ответ. - person bj0; 04.03.2018
comment
Интерфейс _1_ теперь является внутренним API в библиотеке сопрограмм KotlinX. Этот класс и любой класс, который его использует, должны быть помечены аннотацией _2_. - person Erik Browne; 05.03.2018
comment
@Erik Browne поделился отличными ресурсами! В частности, как бы вы реализовали Delay, @InternalCoroutinesApi или _3_ для обработки _4_ в модульном тесте? - person Erik Browne; 19.02.2019

В kotlinx.coroutines v1.2.1 они добавили kotlinx-coroutines-test модуль. Он включает в себя конструктор сопрограмм runBlockingTest, а также TestCoroutineScope и TestCoroutineDispatcher. Они позволяют автоматически продвигаться по времени, а также явно контролировать время для тестирования сопрограмм с delay.

person Erik Browne    schedule 05.09.2019
comment
Я расширил это решение с помощью конкретной реализации, здесь. - person Adam Hurwitz; 15.06.2020
comment
Быстрое обновление, что _1_ уже устарел в более поздних версиях библиотеки сопрограмм. - person Adam Hurwitz; 16.06.2020

В kotlinx.coroutines v0.23.0 они представили TestCoroutineContext.

Плюс: это делает возможным действительно тестирование сопрограмм с delay. Вы можете установить виртуальные часы CoroutineContext на определенный момент времени и проверить ожидаемое поведение.

Против: если ваш код сопрограммы не использует delay, и вы просто хотите, чтобы он выполнялся синхронно в вызывающем потоке, его немного сложнее использовать, чем ответ TestUiContext из @ bj0 (вам нужно вызвать triggerActions() в TestCoroutineContext, чтобы получить сопрограмма для выполнения).

Примечание: TestCoroutineContext теперь находится в модуле kotlinx-coroutines-test, начиная с версии сопрограмм 1.2.1, и будет помечен как устаревший или не существующий в стандартной библиотеке сопрограмм в версиях выше этой версии.

person Erik Browne    schedule 01.08.2018
comment
В v1.2.1 они добавили экспериментальный kotlinx-coroutines-test модуль, включая TestCoroutineContext, _2_ и _3_. - person Bwvolleyball; 24.07.2019
comment
если я использую runBlockingTest, я получаю эту ошибку незаконченных сопрограмм, и я не знаю, как ее решить. TestCoroutineScope хоть и хорошо работает - person Erik Browne; 25.07.2019

Осуществлять

TestCoroutineDispatcher, TestCoroutineScope или Delay могут использоваться для обработки delay в сопрограмме Kotlin, созданной в тестируемом производственном коде.

Ошибка без обработки задержки

В этом случае проверяется состояние представления SomeViewModel. В состоянии ERROR генерируется состояние просмотра с истинным значением ошибки. По истечении заданного времени Snackbar с использованием delay создается новое состояние просмотра со значением ошибки, установленным на false.

SomeViewModel.kt

private fun loadNetwork() {
    repository.getData(...).onEach {
        when (it.status) {
            LOADING -> ...
            SUCCESS ...
            ERROR -> {
                _viewState.value = FeedViewState.SomeFeedViewState(
                    isLoading = false,
                    feed = it.data,
                    isError = true
                )
                delay(SNACKBAR_LENGTH)
                _viewState.value = FeedViewState.SomeFeedViewState(
                    isLoading = false,
                    feed = it.data,
                    isError = false
                )
            }
        }
    }.launchIn(coroutineScope)
}

Есть множество способов справиться с delay. advanceUntilIdle хорош, потому что не требует указания жестко заданной длины. Кроме того, при внедрении TestCoroutineDispatcher, как обрисовано в общих чертах Крейгом Рассел, это будет обрабатывать тот же диспетчер, который используется внутри ViewModel.

SomeTest.kt

private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)

// Code that initiates the ViewModel emission of the view state(s) here.

testDispatcher.advanceUntilIdle()

Они также будут работать:

  • testScope.advanceUntilIdle()
  • testDispatcher.delay(SNACKBAR_LENGTH)
  • delay(SNACKBAR_LENGTH)
  • testDispatcher.resumeDispatcher()
  • testScope.resumeDispatcher()
  • testDispatcher.advanceTimeBy(SNACKBAR_LENGTH)
  • testScope.advanceTimeBy(SNACKBAR_LENGTH)

Разве вы не можете просто настроить тайм-аут задержки, чтобы в своем тесте вы могли выбрать очень маленький ?!

kotlinx.coroutines.test.UncompletedCoroutinesError: Незавершенные сопрограммы во время разрыва. Убедитесь, что все сопрограммы завершены или отменены вашим тестом.

Используйте TestCoroutineDispatcher, TestCoroutineScope или Delay

person Adam Hurwitz    schedule 15.06.2020
comment
Приятно слышать, что testDispatcher.advanceTimeBy делает эту работу за вас. Выполнение теста в testDispatcher.advanceUntilIdle может разрешить незавершенную ошибку сопрограммы как изложено здесь Крейгом Расселом. - person hmac; 17.06.2020
comment
по адресу kotlinx.coroutines.test.TestCoroutineDispatcher.cleanupTestCoroutines (TestCoroutineDispatcher.kt: 178) по адресу app.topcafes.FeedTest.cleanUpTest (FeedTest.kt: 127) по адресу app.topcafes.FeedTest.ktTaccess (28) app.topcafes.FeedTest $ topCafesTest $ 1.invokeSuspend (FeedTest.kt: 106) в app.topcafes.FeedTest $ topCafesTest $ 1.invoke (FeedTest.kt) в kotlinx.coroutines.test.TestBuildersKt $ runBlockingTest ($ TestBuildersKt $ runBlockingTest 1. .kt: 50) в kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt: 33) в kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt: 56) в kotlinx.coroutines.test.TestCoroutineDispatcher ( TestCoroutineDispatcher.kt: 50) в kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith (DispatchedContinuation.kt: 288) в kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable (CancellableKt.startCoroutineCancellable (CancellableStart.CoroutineCancellable (CancellableStart.CoroutineCancellable (Cancellable.Cart.CoroutineCancellable) (Cancellable.Cart.CoroutineCancellable (Cancellable.cart.CoroutineCancellable) (CancellableStart.CoroutineCancellable (Cancellable.cart.Coroutinecancellable) (CancellableStart.Coroutine) oroutineStart.kt: 109) на kotlinx.coroutines.AbstractCoroutine.start (AbstractCoroutine.kt: 158) на kotlinx.coroutines.BuildersKt__Builders_commonKt.async (Builders.common.kt: 91) на kotlinx.coroutines.BuildersKt. по адресу kotlinx.coroutines.BuildersKt__Builders_commonKt.async $ default (Builders.common.kt: 84) по адресу kotlinx.coroutines.BuildersKt.async $ default (Неизвестный источник) по адресу kotlinx.coroutines.test.TestBuildersKt.runBlockingTest (TestBuildersKt.runBlocking). в kotlinx.coroutines.test.TestBuildersKt.runBlockingTest (TestBuilders.kt: 80) в app.topcafes.FeedTest.topCafesTest (FeedTest.kt: 41) в sun.reflect.NativeMethodAccessorImpl.invoke0.NativeMethodAccessorImpl.invoke0. .invoke (NativeMethodAccessorImpl.java:62) в sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) в java.lang.reflect.Method.invoke (Method.java:498) в org.jpg. FrameworkMethod $ 1. запустить .junit.internal.runners.statements.InvokeMethod.evaluate (InvokeMethod.java:17) в org.junit.runners.ParentRunner.runLeaf (ParentRunner.java:325) в org.junit.runners.BlockRunner.JUnnerit4Cloud : 78) на сайте org.junit.runners. BlockJUnit4ClassRunner.runChild (BlockJUnit4ClassRunner.java:57) в org.junit.runners.ParentRunner $ 3.run (ParentRunner.java:290) в org.junit.runners.ParentRunner $ 1.schedule.java.java:71 .runners.ParentRunner.runChildren (ParentRunner.java:288) в org.junit.runners.ParentRunner.access $ 000 (ParentRunner.java:58) в org.junit.runners.ParentRunner $ 2.evaluate (ParentRunner) atjava org.junit.runners.ParentRunner.run (ParentRunner.java:363) по адресу org.junit.runner.JUnitCore.run (JUnitCore.java:137) по адресу com.intellij.junit4.JUnit4IdeaTestRunner.junit4.JUnit4IdeaTestRunner.jUnitRunner.jUnitRunner.jUnitRunner (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JUnitRunner) (JU) в com.intellij.rt.junit.IdeaTestRunner $ Repeater.startRunnerWithArgs (IdeaTestRunner.java:33) в com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart (JUnitStarter.java:230.JUnitStarter.java:230. .main (JUnitStarter.java:58) - person Adam Hurwitz; 17.06.2020