Утверждение сопрограммы не завершено в модульном тесте

У меня есть приостанавливаемая функция, которую я хочу утверждать, что она НЕ завершается с результатом при определенных условиях. Я попытался написать следующее расширение, которое стремится подождать 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

Заранее спасибо.

Просто смотрю, я не думаю, что вы на самом деле вызываете блок с первой попытки, просто возвращаете функцию «блок» из асинхронного блока, поэтому она возвращается сразу же, когда диспетчер ее запускает. Вместо этого, вероятно, должно быть val result = async { block() }.

FatalCatharsis 20.04.2023 05:35
4
1
111
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Дана функция, которая всегда завершается, и функция, которая никогда не завершается.

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
}

Чтобы убедиться, что эти тесты утверждают, что функция приостановки завершена или не завершена, вы можете переключить функции, и оба теста не пройдут.

Другие вопросы по теме