在单元测试中断言协程尚未完成。

huangapple go评论59阅读模式
英文:

Asserting a coroutine is not complete under unit test

问题

I have a suspendible function that I want to assert does NOT complete with a result under certain conditions. I have tried to write the following extension, which aims to wait 5 seconds before asserting whether the Job is complete (I deem this check sufficient for knowing that the suspendible is still hanging):

第一种方法:

// 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,即使我修改测试以实际返回某些内容。

我尝试了另一种方法,通过使用 getCompletionExceptionOrNull() 抛出 IllegalStateException。这确实可以工作,但它会导致一个奇怪的接口,我们需要使用一个 'expected' 属性来注释使用它的每个测试。如果可能的话,我想避免这种情况,因为测试可能在其他地方抛出异常,从而导致测试可能错误地通过。

第二种方法:

// Extension Function
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
    async(block = block).run {
        this@assertNotCompleted.advanceTimeBy(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) 测试总是通过
        }
        // advanceTimeBy(5000L) 测试总是失败
        result.getCompletionExceptionOrNull()
    }.also { 
        assertThat(it.isFailure, equalTo(true))
    }
}

// Usage 与第一种方法相同

提前感谢您。

英文:

I have a suspendible function that I want to assert does NOT complete with a result under certain conditions. I have tried to write the following extension, which aims to wait 5 seconds before asserting whether the Job is complete (I deem this check sufficient for knowing that the suspendible is still hanging):

First Approach:

// Extension Function
fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; 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() 
        }
    }

In this scenario, however, result.isCompleted is always returning true whenever it should be false. And if I remove the advanceTimeBy(5000L), then result.isCompleted always returns false even if I modify the test to actually return something.

I have tried another approach, which throws an IllegalStateException by using getCompletionExceptionOrNull(). This does actually work, but it results in a strange interface whereby we need to annotate every test that uses it with an 'expected' property. I would like to avoid this if possible as it is possible that an exception is thrown elsewhere in the test and thus the test might pass incorrectly.

Second Approach:

// Extension Function
@OptIn(ExperimentalCoroutinesApi::class)
fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; T) {
    async(block = block).run {
        this@assertNotCompleted.advanceTimeBy(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() 
        }
    }

I did try to catch this exception, however the tests either always pass or always fail, depending on the location of advanceTimeBy(5000L) - irrelevant if the suspendible can complete or not.

Thrid Approach:

// Extension Function
@OptIn(ExperimentalCoroutinesApi::class)
fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; 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

Thanks in advance.

答案1

得分: 2

Here's the translated code portion without the code itself:

给定一个总是完成的函数和一个永远不会完成的函数。

这些测试通过:

要验证这些测试是否断言了挂起函数是否已经完成,您可以交换函数,这两个测试都会失败。

英文:

Given a function that alwaysCompletes and a function that neverCompletes

suspend fun alwaysCompletes(): Int {
    delay(100)
    return 42
}

suspend fun neverCompletes() {
    while(true) {
        delay(1000)
    }
}

These tests pass:

@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
    }
}


or simulating the time with 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
}

To verify these tests assert that the suspend function has or hasn't completed, you can switch the functions over and both tests will fail

huangapple
  • 本文由 发表于 2023年4月19日 16:37:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/76052388.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定