Telegram Group & Telegram Channel
Ошибки при тестировании корутин, которые вижу чаще всего на #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
🔥15👍1



group-telegram.com/rareilly/167
Create:
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

View MORE
Open in Telegram


Telegram | DID YOU KNOW?

Date: |

"We as Ukrainians believe that the truth is on our side, whether it's truth that you're proclaiming about the war and everything else, why would you want to hide it?," he said. On February 27th, Durov posted that Channels were becoming a source of unverified information and that the company lacks the ability to check on their veracity. He urged users to be mistrustful of the things shared on Channels, and initially threatened to block the feature in the countries involved for the length of the war, saying that he didn’t want Telegram to be used to aggravate conflict or incite ethnic hatred. He did, however, walk back this plan when it became clear that they had also become a vital communications tool for Ukrainian officials and citizens to help coordinate their resistance and evacuations. There was another possible development: Reuters also reported that Ukraine said that Belarus could soon join the invasion of Ukraine. However, the AFP, citing a Pentagon official, said the U.S. hasn’t yet seen evidence that Belarusian troops are in Ukraine. Telegram has become more interventionist over time, and has steadily increased its efforts to shut down these accounts. But this has also meant that the company has also engaged with lawmakers more generally, although it maintains that it doesn’t do so willingly. For instance, in September 2021, Telegram reportedly blocked a chat bot in support of (Putin critic) Alexei Navalny during Russia’s most recent parliamentary elections. Pavel Durov was quoted at the time saying that the company was obliged to follow a “legitimate” law of the land. He added that as Apple and Google both follow the law, to violate it would give both platforms a reason to boot the messenger from its stores. This ability to mix the public and the private, as well as the ability to use bots to engage with users has proved to be problematic. In early 2021, a database selling phone numbers pulled from Facebook was selling numbers for $20 per lookup. Similarly, security researchers found a network of deepfake bots on the platform that were generating images of people submitted by users to create non-consensual imagery, some of which involved children.
from it


Telegram Ra'Reilly - Заметки про Android и не только
FROM American