--- title: Тестирование корутин description: runTest, виртуальное время, TestDispatcher, backgroundScope, тестирование Flow и Turbine tags: - kotlin - testing - coroutines --- Корутины ломают привычную модель теста: результат появляется не в момент вызова, а когда-то позже, а `delay` делает тест медленным. Библиотека `kotlinx-coroutines-test` решает обе проблемы - она даёт `runTest` с виртуальным временем, где `delay(10.seconds)` выполняется мгновенно. ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds class FirstCoroutineTest { @Test fun `delay не замедляет тест`() = runTest { val start = currentTime delay(10.seconds) assertEquals(10_000, currentTime - start) } } ``` Тест завершается за миллисекунды, хотя по виртуальным часам прошло десять секунд. ## Подключение ```kotlin // build.gradle.kts dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0") testImplementation("app.cash.turbine:turbine:1.2.1") } tasks.test { useJUnitPlatform() } ``` | Артефакт | Зачем | |---|---| | `kotlinx-coroutines-test` | `runTest`, `TestDispatcher`, `TestScope` | | `app.cash.turbine:turbine` | удобная проверка `Flow` по элементам | | `kotlinx-coroutines-debug` | дампы корутин при зависании | Версия `kotlinx-coroutines-test` должна совпадать с версией `kotlinx-coroutines-core` - иначе возможен `NoSuchMethodError` в рантайме. ## runTest ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class RunTestControlTest { @Test fun `runCurrent выполняет уже запланированное`() = runTest { val log = mutableListOf() launch { log += "сразу" } // корутина запланирована, но ещё не запущена assertEquals(emptyList(), log) runCurrent() assertEquals(listOf("сразу"), log) } @Test fun `advanceTimeBy продвигает часы на заданное время`() = runTest { val log = mutableListOf() launch { delay(100); log += "a" delay(100); log += "b" } advanceTimeBy(150) assertEquals(listOf("a"), log) advanceUntilIdle() assertEquals(listOf("a", "b"), log) } @Test fun `testScheduler доступен напрямую`() = runTest { assertEquals(0, testScheduler.currentTime) delay(5) assertEquals(5, testScheduler.currentTime) } } ``` | Инструмент | Что делает | |---|---| | `runTest { }` | запускает тело как корутину с виртуальным временем | | `currentTime` | виртуальные часы в миллисекундах | | `runCurrent()` | выполняет задачи, запланированные на текущий момент | | `advanceTimeBy(ms)` | продвигает часы, выполняя всё по пути | | `advanceUntilIdle()` | продвигает часы до исчерпания задач | | `testScheduler` | планировщик, общий для диспетчеров теста | | `backgroundScope` | scope, отменяемый по завершении теста | | `runTest(timeout = 5.seconds)` | реальный таймаут на весь тест, по умолчанию 60 с | `runTest` возвращает `TestResult`, поэтому тело теста пишется как `= runTest { }`, а не `{ runTest { } }` - на JVM это `Unit`, но форма с `=` работает и в Multiplatform. После выхода из тела `runTest` ждёт завершения всех дочерних корутин, запущенных в его scope. Это именно то, что нужно в большинстве тестов: явный `advanceUntilIdle` в конце не требуется. ## Диспетчеры теста ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class DispatchersTest { @Test fun `StandardTestDispatcher откладывает запуск`() = runTest(StandardTestDispatcher()) { val log = mutableListOf() launch { log += "дочерняя" } assertEquals(emptyList(), log) runCurrent() assertEquals(listOf("дочерняя"), log) } @Test fun `UnconfinedTestDispatcher запускает немедленно`() = runTest(UnconfinedTestDispatcher()) { val log = mutableListOf() launch { log += "дочерняя" } assertEquals(listOf("дочерняя"), log) } @Test fun `общий scheduler связывает диспетчеры`() = runTest { val other = StandardTestDispatcher(testScheduler) val log = mutableListOf() launch(other) { delay(100); log += "другой" } advanceUntilIdle() assertEquals(listOf("другой"), log) } } ``` | Диспетчер | Поведение | |---|---| | `StandardTestDispatcher` | корутина ставится в очередь, нужен `runCurrent` или `advanceUntilIdle` | | `UnconfinedTestDispatcher` | корутина выполняется до первой приостановки немедленно | `runTest` без аргумента использует `StandardTestDispatcher` - это осознанный выбор: отложенный запуск приближает тест к реальному многопоточному исполнению и находит гонки, которые `Unconfined` скрывает. `UnconfinedTestDispatcher` удобен, когда проверяется результат, а не порядок - особенно при сборе `StateFlow`, где конфлюирование съедает промежуточные значения. Главное про `scheduler`: виртуальные часы живут в нём, а не в диспетчере. Диспетчер, созданный **без** передачи `testScheduler`, получает свой собственный планировщик, и `advanceUntilIdle` теста на него не действует. ## backgroundScope ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class BackgroundScopeTest { @Test fun `бесконечная корутина не подвешивает тест`() = runTest { var ticks = 0 backgroundScope.launch { while (true) { delay(100) ticks++ } } advanceTimeBy(350) assertEquals(3, ticks) } } ``` `runTest` ждёт дочерние корутины своего scope, поэтому бесконечный цикл в `launch { }` приведёт к `UncompletedCoroutinesError` по таймауту. `backgroundScope` отменяется автоматически по завершении теста - это единственно верное место для долгоживущих коллекторов, подписок и имитации фоновой работы. ## Внедрение диспетчера Виртуальное время работает только для диспетчеров, связанных с `testScheduler`. Жёстко прописанный `Dispatchers.IO` в продакшн-коде делает тест реально медленным: ```kotlin import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class Hardcoded { suspend fun work(): String = withContext(Dispatchers.IO) { delay(1000) "результат" } } class Injected(private val io: CoroutineDispatcher) { suspend fun work(): String = withContext(io) { delay(1000) "результат" } } class DispatcherInjectionTest { @Test fun `внедрённый диспетчер виртуализирует delay`() = runTest { val wall = System.currentTimeMillis() val service = Injected(StandardTestDispatcher(testScheduler)) assertEquals("результат", service.work()) assertTrue(System.currentTimeMillis() - wall < 500) } @Test fun `жёстко заданный диспетчер ждёт реальную секунду`() = runTest { val wall = System.currentTimeMillis() assertEquals("результат", Hardcoded().work()) assertTrue(System.currentTimeMillis() - wall >= 900) } } ``` Оба теста проходят, но второй занимает реальную секунду. В продакшн-коде диспетчер стоит передавать параметром с значением по умолчанию: ```kotlin import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext class Service( private val io: CoroutineDispatcher = Dispatchers.IO, ) { suspend fun load(): String = withContext(io) { delay(1000) "данные" } } ``` Вызывающий код ничего не меняет, а тест подставляет `StandardTestDispatcher(testScheduler)`. Подробнее: [Диспетчеры и контекст](../Корутины/Диспетчеры%20и%20контекст.md) ## Dispatchers.Main `Dispatchers.Main` на JVM без UI-фреймворка не существует, поэтому его подменяют: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import kotlin.test.Test import kotlin.test.assertEquals class ViewModelLike(private val scope: CoroutineScope) { private val _state = MutableStateFlow("начало") val state: StateFlow = _state fun load() { scope.launch { _state.value = "загрузка" delay(1000) _state.value = "готово" } } } class MainDispatcherTest { @Test fun `setMain подменяет Main на тестовый диспетчер`() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) try { val vm = ViewModelLike(CoroutineScope(Dispatchers.Main)) vm.load() runCurrent() assertEquals("загрузка", vm.state.value) advanceUntilIdle() assertEquals("готово", vm.state.value) } finally { Dispatchers.resetMain() } } } ``` `setMain` и `resetMain` - глобальное состояние, поэтому `try`/`finally` обязателен. Повторять его в каждом тесте не нужно - проще вынести в расширение JUnit: ```kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtensionContext import kotlin.test.Test import kotlin.test.assertEquals class MainDispatcherExtension( private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext) = Dispatchers.setMain(dispatcher) override fun afterEach(context: ExtensionContext) = Dispatchers.resetMain() } class ViewModel { private val scope = CoroutineScope(Dispatchers.Main) private val _state = MutableStateFlow("начало") val state: StateFlow = _state fun load() { scope.launch { _state.value = "загрузка" delay(1000) _state.value = "готово" } } } @ExtendWith(MainDispatcherExtension::class) class ViewModelTest { @Test fun `Main подменён для всех тестов класса`() = runTest { val vm = ViewModel() vm.load() assertEquals("загрузка", vm.state.value) advanceUntilIdle() assertEquals("готово", vm.state.value) } } ``` Подробнее про расширения: [kotlin.test и JUnit 5](kotlin.test%20и%20JUnit%205.md) ## Тестирование Flow Конечный поток проверяется сборкой в список: ```kotlin import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class FlowCollectTest { @Test fun `toList собирает конечный поток`() = runTest { val source = flow { emit(1); delay(100) emit(2); delay(100) emit(3) } assertEquals(listOf(1, 2, 3), source.toList()) } @Test fun `take ограничивает бесконечный поток`() = runTest { val infinite = flow { var i = 0 while (true) { emit(i++) delay(10) } } assertEquals(listOf(0, 1, 2), infinite.take(3).toList()) } } ``` `toList()` на бесконечном потоке подвесит тест до таймаута - ограничение через `take` обязательно. ### StateFlow и конфлюирование ```kotlin import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class StateFlowTest { @Test fun `Unconfined видит каждое значение`() = runTest { val state = MutableStateFlow(0) val seen = mutableListOf() backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { state.collect { seen += it } } state.value = 1 state.value = 2 runCurrent() assertEquals(listOf(0, 1, 2), seen) } @Test fun `Standard конфлюирует промежуточные значения`() = runTest { val state = MutableStateFlow(0) val seen = mutableListOf() backgroundScope.launch { state.collect { seen += it } } state.value = 1 state.value = 2 runCurrent() // коллектор не успел запуститься и получил только последнее assertEquals(listOf(2), seen) } } ``` Разница принципиальная: со `StandardTestDispatcher` коллектор стартует только при `runCurrent`, к тому моменту `value` уже равно `2`, а `StateFlow` не хранит историю. Для проверки последовательности значений нужен `UnconfinedTestDispatcher(testScheduler)` - и обязательно с передачей `testScheduler`. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md) Если проверяется только итог, достаточно `state.value` без коллектора вообще. ## Turbine Ручной сбор в список быстро становится громоздким: нужно управлять scope, продвигать время и помнить про конфлюирование. Turbine даёт императивный API над потоком: ```kotlin import app.cash.turbine.test import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class TurbineTest { @Test fun `awaitItem забирает элементы по одному`() = runTest { flow { emit(1); delay(100); emit(2) }.test { assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) awaitComplete() } } @Test fun `StateFlow отдаёт начальное значение первым`() = runTest { val state = MutableStateFlow(0) state.test { assertEquals(0, awaitItem()) state.value = 1 assertEquals(1, awaitItem()) cancelAndIgnoreRemainingEvents() } } @Test fun `awaitError ловит исключение потока`() = runTest { flow { emit(1); throw IllegalStateException("сбой") }.test { assertEquals(1, awaitItem()) assertEquals("сбой", awaitError().message) } } @Test fun `skipItems и expectNoEvents`() = runTest { flowOf(1, 2, 3, 4).test { skipItems(2) assertEquals(3, awaitItem()) assertEquals(4, awaitItem()) awaitComplete() } MutableSharedFlow().test { expectNoEvents() cancelAndIgnoreRemainingEvents() } } } ``` | Функция | Что делает | |---|---| | `awaitItem()` | ждёт следующий элемент | | `awaitComplete()` | ждёт нормального завершения | | `awaitError()` | ждёт исключения и возвращает его | | `skipItems(n)` | пропускает `n` элементов | | `expectNoEvents()` | проверяет, что событий не было | | `expectMostRecentItem()` | берёт последний, отбрасывая предыдущие | | `cancelAndIgnoreRemainingEvents()` | завершает сбор, не требуя разбора остатка | | `cancelAndConsumeRemainingEvents()` | то же, но возвращает остаток | Ключевое свойство: **незабранные события проваливают тест**. Если поток выдал три элемента, а проверены два, Turbine бросит `TurbineAssertionError` при выходе из блока. Это ловит лишние эмиссии, которые ручной `toList()` просто не замечает. Несколько потоков сразу - через `turbineScope`: ```kotlin import app.cash.turbine.turbineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class TurbineScopeTest { @Test fun `два потока проверяются вперемешку`() = runTest { turbineScope { val numbers = flowOf(1, 2).testIn(backgroundScope) val letters = flowOf("x").testIn(backgroundScope) assertEquals(1, numbers.awaitItem()) assertEquals("x", letters.awaitItem()) assertEquals(2, numbers.awaitItem()) numbers.awaitComplete() letters.awaitComplete() } } } ``` ## Исключения и отмена ```kotlin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds class ErrorsTest { @Test fun `исключение дочерней корутины доходит до теста`() = runTest { val e = runCatching { coroutineScope { launch { throw IllegalStateException("из дочерней") } } }.exceptionOrNull() assertEquals("из дочерней", e?.message) } @Test fun `незавершённая корутина в основном scope даёт UncompletedCoroutinesError`() { val e = runCatching { runTest(timeout = 1.seconds) { launch { while (true) { delay(100) } } } }.exceptionOrNull() // UncompletedCoroutinesError внутренний, поэтому проверяем по имени assertEquals("UncompletedCoroutinesError", e!!::class.simpleName) } @Test fun `TestScope создаётся вручную вне runTest`() { val scope = TestScope() var done = false scope.launch { delay(1000); done = true } scope.advanceUntilIdle() assertTrue(done) } } ``` Необработанное исключение дочерней корутины проваливает `runTest` - специально ловить его не нужно. `TestScope()` вручную пригодится, когда тестируемый класс сам принимает `CoroutineScope`: в этом случае scope передаётся в конструктор, а тест управляет временем через `advanceUntilIdle`. Подробнее: [Исключения в корутинах](../Корутины/Исключения%20в%20корутинах.md) ## Моки suspend-функций ```kotlin import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.delay import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals interface Api { suspend fun fetch(id: Int): String } class Repository(private val api: Api) { suspend fun load(id: Int): String = "данные: ${api.fetch(id)}" } class MockSuspendTest { @Test fun `coEvery с виртуальной задержкой`() = runTest { val api = mockk() coEvery { api.fetch(1) } coAnswers { delay(5000) "ответ" } assertEquals("данные: ответ", Repository(api).load(1)) assertEquals(5000, currentTime) coVerify(exactly = 1) { api.fetch(1) } } } ``` `delay` внутри `coAnswers` под `runTest` виртуализируется - имитация медленного сетевого вызова не замедляет тест. Подробнее: [MockK](MockK.md) ## Корутины в Kotest ```kotlin import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest class KotestCoroutines : StringSpec({ "тело теста уже suspend" { withContext(Dispatchers.Default) { 42 } shouldBe 42 } "runTest нужен для виртуального времени" { runTest { delay(10_000) currentTime shouldBe 10_000 } } }) ``` Тело теста в Kotest - уже `suspend`-функция, поэтому `suspend`-вызовы работают без обёртки. Но виртуального времени там нет: `delay(100)` займёт реальные 100 мс, пока тест не завёрнут в `runTest`. Подробнее: [Kotest](Kotest.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `Thread.sleep` в тесте - реальная пауза | `delay` в `runTest` виртуален | > | `CountDownLatch` для синхронизации | `advanceUntilIdle()` | > | `Awaitility.await().until { }` | детерминированный планировщик | > | `future.get(5, SECONDS)` | `runTest { }` ждёт сам | > | `ExecutorService` + `shutdownNow` | `backgroundScope` отменяется сам | > | флаки из-за таймингов | порядок задан планировщиком | > > **Виртуальное время заменяет ожидание реального.** В Java тест асинхронного кода либо спит (`Thread.sleep(100)` - медленно и ненадёжно), либо синхронизируется через `CountDownLatch` и `Awaitility` - это работает, но требует явных точек синхронизации в продакшн-коде или угадывания таймаутов. `TestCoroutineScheduler` вместо этого контролирует время: `delay` регистрирует задачу на виртуальный момент, а `advanceTimeBy` исполняет её немедленно. Тест становится детерминированным и мгновенным одновременно. Подробнее: [Взаимодейтсвие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md) > > **Порядок выполнения задан, а не случаен.** Java-тесты на `ExecutorService` зависят от решений планировщика ОС: один и тот же тест проходит локально и падает в CI. `StandardTestDispatcher` исполняет корутины в строго определённом порядке - по очереди задач. Это убирает флаки, но меняет требования к коду: тест, рассчитывающий на немедленный запуск `launch`, нужно писать с `runCurrent()`. > > **Структурированная конкурентность делает «забытую задачу» ошибкой компилятора времени выполнения.** В Java поток, оставшийся после теста, просто живёт дальше и может испортить следующий тест или задержать завершение JVM. `runTest` ждёт все дочерние корутины своего scope и бросает `UncompletedCoroutinesError`, если какая-то не завершилась. Для осознанно долгоживущих задач предусмотрен `backgroundScope` - забыть отменить подписку в тесте просто нельзя. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) > > **`Dispatchers.Main` приходится подменять, потому что это не настоящий диспетчер.** В Java нет понятия «главного потока» на уровне стандартной библиотеки - есть Swing EDT и Android main looper, каждый со своим API. Kotlin вводит `Dispatchers.Main` как общую абстракцию, и на JVM без UI-фреймворка он отсутствует: обращение без `setMain` даёт `IllegalStateException`. Это цена единого API для всех платформ. > > Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) > [!warning] Подводные камни > **Диспетчер без `testScheduler` подвешивает тест.** `UnconfinedTestDispatcher()` вне `runTest` создаёт **свой** планировщик, поэтому `advanceUntilIdle()` на него не действует: корутина остаётся активной, и тест падает с `UncompletedCoroutinesError` через 60 секунд. Всегда передавайте планировщик: `UnconfinedTestDispatcher(testScheduler)`. > > **`Dispatchers.Main` без `setMain` роняет не только тест, а весь прогон.** Исключение возникает внутри потока корутины, поэтому может не попасть в отчёт конкретного теста и проявиться как необъяснимое падение JVM. Подменяйте `Main` в `@BeforeEach` или через расширение. > > **`Dispatchers.setMain` - глобальное состояние.** Забытый `resetMain` ломает тесты, которым нужен настоящий `Main`, причём порядок тестов не гарантирован. Используйте `try`/`finally` или расширение JUnit. > > **`StateFlow` конфлюирует, и промежуточные значения теряются.** Со `StandardTestDispatcher` коллектор запускается позже эмиссий и увидит только последнее значение - проверено: из `0 → 1 → 2` в список попадёт `[2]`. Для последовательности нужен `UnconfinedTestDispatcher(testScheduler)`. Подробнее: [StateFlow и SharedFlow](../Корутины/StateFlow%20и%20SharedFlow.md) > > **`MutableSharedFlow` без буфера приостанавливает `emit` до появления подписчика.** Это не потеря данных, как часто пишут, а приостановка: `launch { shared.emit(1) }` до подписки дождётся коллектора и доставит значение. Но если `emit` вызван напрямую в теле теста, тест повиснет. Подписывайтесь до эмиссии и вызывайте `runCurrent()`. > > **Жёстко прописанный `Dispatchers.IO` или `Default` делает тест реально медленным.** `withContext(Dispatchers.IO) { delay(1000) }` займёт настоящую секунду даже внутри `runTest` - планировщик теста такой диспетчер не контролирует. Внедряйте диспетчер параметром. > > **Бесконечная корутина в основном scope `runTest` даёт `UncompletedCoroutinesError`.** `launch { while (true) { delay(100) } }` ждёт до таймаута (60 с по умолчанию). Для долгоживущих задач используйте `backgroundScope`. > > **`toList()` на бесконечном потоке висит до таймаута.** Ограничивайте через `take(n)`, `timeout` или проверяйте через Turbine с `cancelAndIgnoreRemainingEvents()`. > > **Незабранные события в Turbine проваливают тест - и это правильно.** `TurbineAssertionError` при выходе из `test { }` означает, что поток выдал больше элементов, чем проверено. Если остаток не важен, завершайте явным `cancelAndIgnoreRemainingEvents()`. > > **`advanceUntilIdle()` не исполняет задачи чужих планировщиков.** Если тестируемый класс создаёт корутины на `Dispatchers.Default`, они продолжат работать в реальном времени параллельно тесту - результат непредсказуем. > > **`runTest` ждёт дочерние корутины, но не сами задачи `Dispatchers.Default`.** Тест может завершиться раньше реальной фоновой работы, а исключение из неё всплывёт в следующем тесте. > > **`currentTime` требует отдельного импорта.** Это свойство-расширение `TestScope`, а не член класса, поэтому `import kotlinx.coroutines.test.currentTime` обязателен - без него IDE покажет `unresolved reference` внутри `runTest`. В обычной `suspend`-функции вне `TestScope` его нет вовсе. > > **`runTest(timeout = ...)` - реальный таймаут, а не виртуальный.** Он ограничивает физическое время прогона и не связан с `currentTime`; виртуально тест может «прожить» часы. > > **Версии `kotlinx-coroutines-core` и `-test` должны совпадать.** Расхождение даёт `NoSuchMethodError` в рантайме, причём сообщение не указывает на причину. Держите версию в `libs.versions.toml`. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md) > > **В Kotest тело теста `suspend`, но времени это не виртуализирует.** `delay(100)` в спецификации занимает реальные 100 мс. Для виртуального времени оборачивайте в `runTest` явно. Далее: [Рефлексия](../Рефлексия/Рефлексия.md)