group-telegram.com/rareilly/167
Create:
Last Update:
Last Update:
Ошибки при тестировании корутин, которые вижу чаще всего на #review (и сам периодически допускаю, хе-хе).
1️⃣ Не используется runTest
Structured concurrency гарантирует нам, что родительская корутина дождётся завершения всех дочерних. И наоборот, если родительскую корутину принудительно завершили, дочерние тоже завершатся. runTest
как раз стартует корутину, срок жизни которой ограничен одним тестом, а значит ограничивает срок жизни всех дочерних корутин. При этом он не просто дожидается завершения всех корутин, но и проверяет, что все они завершились без ошибок.
Вот пример, где вместо runTest
используется самопальный скоуп. Мы ожидаем, что тест упадёт, но он будет зелёным:
val testScope = CoroutineScope(StandardTestDispatcher())
@Test
fun `false positive`() {
testScope.launch { fail("Opps...") }
}
Здесь код внутри
launch
даже не выполнится потому что мы просто забыли дать ей возможность выполниться (см. п. 4️⃣). Возможна и другая проблема. Если в корутине выполняется долгая операция, тестовая функция может просто не дождаться её выполнения. В итоге ошибка упадёт уже после того как тест окрасился в зелёный, а может вообще уронить тестовый фреймворк во время выполнения другого теста. Этих проблем не будет, если родительская корутина органичена одним тестом.2️⃣ Нет возможности подменить CoroutineScope
В реальности корутина может запускаться не напрямую из теста, а внутри тестируемой сущности с каким-то собственным скоупом и тогда structured concurrency не сработает и мы снова получаем все проблемы из примера выше. Чтобы это пофиксить, нужно давать возможность передавать
CoroutineScope
внутрь сущностей через конструктор или параметры функций.Например,
viewModelScope
работает на Main-диспатчере. До недавнего времени единственным вариантом было перед тестом ViewModel
вызывать Dispatchers.setMain(...)
, но теперь это не нужно. В lifecycle-viewmodel 2.8.0 появилась возможность переопределять viewModelScope
через параметр конструктора! Этот вариант более явный и даёт в тестах полный контроль над корутинами внутри ViewModel
.Если используете подход с переопределением Main-диспатчера, важно не забывать, что оборачивать тест в
runTest
всё ещё нужно. Забыть легко потому что все вызовы suspend-функций будут внутри ViewModel
и кажется, что runTest
бесполезен, но это не так. runTest
неявно подтянет переопределённый Dispatchers.Main
и после выполнения проверит, что все корутины на этом диспатчере завершились успешно.3️⃣ Неограниченные StateFlow и SharedFlow
Это горячие потоки, они никогда не завершаются, так что подписка на такой поток внутри теста неизбежно приведёт к падению теста по таймауту. Есть два варианта обхода проблемы:
1. Искусственно ограничить количество элементов, которые хочется поймать из потока. Самый простой способ — использовать операторы
take(n)
или first()
, а где-то будет удобнее использовать Turbine.2. Если мы не знаем ожидаемое количество элементов и просто хотим поймать всё, что прилетело во
Flow
за время теста, можно подписаться на Flow
используя backgroundScope
. Это специальный скоуп внутри TestScope
, который завершается вместе с тестовым скоупом, прерывая выполнение дочерних корутин.4️⃣ Корутине не даётся шанса выполниться
Вызова
launch
недостаточно, чтобы запустить корутину, нужно ещё дать ей шанс выполниться. Например, вот тест где корутине такого шанса я не дал:@Test
fun `not launched coroutine`() = runTest {
var result = 0
launch { result = 42 }
// ❌ Fails
assertEquals(expected = 42, actual = result)
}
Чтобы исправить проблему, нужно вызвать
runCurrent
или yield
после старта корутины. Тогда текущая корутина уступит поток другим корутинам.Другой вариант — использовать
UnconfinedTestScheduler
, тогда все корутины будут сразу же запускаться без необходимости пинать шедулер. Это удобно, когда нет необходимости строго контролировать последовательность выполнения корутин.Полезное:
- Документация к coroutines-test
- Примеры плохих тестов с возможностью запуска
Пишите в комменты с какими проблемами сталкивались при тестировании корутин!
#test #coroutines
BY Ra'Reilly - Заметки про Android и не только
Share with your friend now:
group-telegram.com/rareilly/167