У меня есть приостанавливаемая функция, которую я хочу утверждать, что она НЕ завершается с результатом при определенных условиях. Я попытался написать следующее расширение, которое стремится подождать 5 секунд, прежде чем утверждать, завершено ли задание (я считаю, что этой проверки достаточно, чтобы знать, что приостановка все еще висит):
Первый подход:
// Extension Function
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
val result = async { block }
advanceTimeBy(5000L)
val isBlockComplete = result.isCompleted
assertThat(isBlockComplete, equalTo(false))
}
// Usage
@Test
fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
runTest {
assertNotCompleted {
someClass.waitForValue()
}
}
Однако в этом сценарии result.isCompleted
всегда возвращает true, когда должно быть false. И если я удалю advanceTimeBy(5000L)
, то result.isCompleted
всегда будет возвращать false
, даже если я изменю тест, чтобы он действительно что-то возвращал.
Я попробовал другой подход, который выдает IllegalStateException
с помощью getCompletionExceptionOrNull()
. Это действительно работает, но приводит к странному интерфейсу, в котором нам нужно аннотировать каждый тест, который его использует, с «ожидаемым» свойством. Я хотел бы избежать этого, если это возможно, поскольку возможно, что в другом месте теста возникнет исключение, и поэтому тест может пройти неправильно.
Второй подход:
// Extension Function
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
async(block = block).run {
[email protected](5000L)
getCompletionExceptionOrNull()
}
}
// Usage - wanting to avoid the need for expected property
@Test(expected = IllegalStateException::class)
fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
runTest {
assertNotCompleted {
someClass.waitForValue()
}
}
Я пытался поймать это исключение, однако тесты либо всегда проходят, либо всегда терпят неудачу, в зависимости от местоположения advanceTimeBy(5000L)
- не имеет значения, может ли подвеска завершиться или нет.
Третий подход:
// Extension Function
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
runCatching {
val result = async {
block
// advanceTimeBy(5000L) Test ALWAYS passes
}
// advanceTimeBy(5000L) Test ALWAYS fails
result.getCompletionExceptionOrNull()
}.also {
assertThat(it.isFailure, equalTo(true))
}
}
// Usage is same as first approach
Заранее спасибо.
Дана функция, которая всегда завершается, и функция, которая никогда не завершается.
suspend fun alwaysCompletes(): Int {
delay(100)
return 42
}
suspend fun neverCompletes() {
while(true) {
delay(1000)
}
}
Эти тесты проходят:
@Test
fun testAlwaysCompletes() {
runBlocking {
val job = launch {
someClass.alwaysCompletes()
}
delay(5000) // wait for 5 seconds
assertThat(true, equalTo(job.isCompleted ))
}
}
@Test
fun testNeverCompletes() {
runBlocking {
val job = launch {
someClass.neverCompletes()
}
delay(5000) // wait for 5 seconds
assertThat(false, equalTo(job.isCompleted ))
job.cancelAndJoin() // cancel the job to avoid leaking resources
}
}
или моделирование времени с помощью advanceTimeBy:
@Test
fun testAlwaysCompletes() = runTest {
val job = launch {
someClass.alwaysCompletes()
}
advanceTimeBy(10_000) // advance time by 10,000ms
assertFalse(job.isActive) // job should not be active
assertTrue(job.isCompleted) // job should now be completed
}
@Test
fun testNeverCompletes() = runTest {
val job = launch {
someClass.neverCompletes()
}
advanceTimeBy(10_000) // advance time by 10,000ms
assertTrue(job.isActive) // job should still be active
job.cancelAndJoin() // cancel the job to avoid leaking resources
assertFalse(job.isActive) // job should now be cancelled and not active
}
Чтобы убедиться, что эти тесты утверждают, что функция приостановки завершена или не завершена, вы можете переключить функции, и оба теста не пройдут.
Просто смотрю, я не думаю, что вы на самом деле вызываете блок с первой попытки, просто возвращаете функцию «блок» из асинхронного блока, поэтому она возвращается сразу же, когда диспетчер ее запускает. Вместо этого, вероятно, должно быть
val result = async { block() }
.