Почему запускаются исключения глотания в сопрограммах kotlin? - PullRequest
0 голосов
/ 05 июня 2018

Следующий тест завершается успешно с Process finished with exit code 0.Обратите внимание, что этот тест выводит исключение в журналы, но не завершает тест (такое поведение я хочу).

@Test
fun why_does_this_test_pass() {
    val job = launch(Unconfined) {
        throw IllegalStateException("why does this exception not fail the test?")
    }

    // because of `Unconfined` dispatcher, exception is thrown before test function completes
}

Как и ожидалось, этот тест завершается неудачно с Process finished with exit code 255

@Test
fun as_expected_this_test_fails() {
    throw IllegalStateException("this exception fails the test")
}

Почему эти тесты не ведут себя одинаково?

Ответы [ 3 ]

0 голосов
/ 06 июня 2018

Мне удалось создать исключение, выдавающее CoroutineContext для тестов.

val coroutineContext = Unconfined + CoroutineExceptionHandler { _, throwable ->
        throw throwable
    }

Хотя это, вероятно, не подходит для производства.Может быть, нужно отловить исключения отмены или что-то, я не уверен

0 голосов
/ 12 сентября 2018

Пользовательское правило тестирования пока кажется лучшим решением.

/**
 * Coroutines can throw exceptions that can go unnoticed by the JUnit Test Runner which will pass
 * a test that should have failed. This rule will ensure the test fails, provided that you use the
 * [CoroutineContext] provided by [dispatcher].
 */
class CoroutineExceptionRule : TestWatcher(), TestRule {

    private val exceptions = Collections.synchronizedList(mutableListOf<Throwable>())

    val dispatcher: CoroutineContext
        get() = Unconfined + CoroutineExceptionHandler { _, throwable ->
            // I want to hook into test lifecycle and fail test immediately here
            exceptions.add(throwable)
            // this throw will not always fail the test. this does print the stacktrace at least
            throw throwable 
        }

    override fun starting(description: Description) {
        // exceptions from a previous test execution should not fail this test
        exceptions.clear()
    }

    override fun finished(description: Description) {
        // instead of waiting for test to finish to fail it
        exceptions.forEach { throw AssertionError(it) }
    }
}

Я надеюсь улучшить его с помощью этой публикации , хотя

0 голосов
/ 06 июня 2018

Сравните ваш тест со следующим, в котором не используются сопрограммы, но вместо этого начинается новый поток:

@Test
fun why_does_this_test_pass() {
    val job = thread { // <-- NOTE: Changed here
        throw IllegalStateException("why does this exception not fail the test?")
    }
    // NOTE: No need for runBlocking any more
    job.join() // ensures exception is thrown before test function completes
}

Что здесь происходит?Как и тест с launch, этот тест проходит , если вы его запустите, но исключение будет напечатано на консоли.

Таким образом, использование launch для запуска новой сопрограммы очень похоже на использование thread для запуска нового потока.Если это не удается, ошибка обрабатывается необработанным обработчиком исключений в thread и CoroutineExceptionHandler (см. Его в документации) launch.Исключения при запуске не проглатываются , а обрабатываются обработчиком исключений сопрограммы.

Если вы хотите, чтобы исключение распространялось на тест, вы должны заменить launchс async и замените join на await в вашем коде.См. Также этот вопрос: В чем разница между запуском / соединением и асинхронностью / ожиданием в сопрограммах Kotlin

ОБНОВЛЕНИЕ : сопрограммы Kotlin недавно представили концепцию "структурированногоПараллелизм », чтобы избежать потери такого рода исключений.Код в этом вопросе больше не компилируется.Чтобы скомпилировать его, вы должны либо явно сказать GlobalScope.launch (как в «Я подтверждаю, что все нормально, чтобы потерять мои исключения, вот моя подпись»), либо заключить тест в runBlocking { ... }, в этом случае исключение не являетсяпотерял.

...