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

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

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):

第一种方法:

  1. // Extension Function
  2. fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
  3. val result = async { block }
  4. advanceTimeBy(5000L)
  5. val isBlockComplete = result.isCompleted
  6. assertThat(isBlockComplete, equalTo(false))
  7. }
  8. // Usage
  9. @Test
  10. fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
  11. runTest {
  12. assertNotCompleted {
  13. someClass.waitForValue()
  14. }
  15. }

在这种情况下,然而,result.isCompleted 总是返回 true,即使它应该是 false。如果我删除 advanceTimeBy(5000L),那么 result.isCompleted 总是返回 false,即使我修改测试以实际返回某些内容。

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

第二种方法:

  1. // Extension Function
  2. @OptIn(ExperimentalCoroutinesApi::class)
  3. fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
  4. async(block = block).run {
  5. this@assertNotCompleted.advanceTimeBy(5000L)
  6. getCompletionExceptionOrNull()
  7. }
  8. }
  9. // Usage - wanting to avoid the need for expected property
  10. @Test(expected = IllegalStateException::class)
  11. fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
  12. runTest {
  13. assertNotCompleted {
  14. someClass.waitForValue()
  15. }
  16. }

我尝试捕获此异常,但测试无论何时都会通过或失败,这取决于 advanceTimeBy(5000L) 的位置 - 无论挂起是否完成。

第三种方法:

  1. // Extension Function
  2. @OptIn(ExperimentalCoroutinesApi::class)
  3. fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
  4. runCatching {
  5. val result = async {
  6. block
  7. // advanceTimeBy(5000L) 测试总是通过
  8. }
  9. // advanceTimeBy(5000L) 测试总是失败
  10. result.getCompletionExceptionOrNull()
  11. }.also {
  12. assertThat(it.isFailure, equalTo(true))
  13. }
  14. }
  15. // 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:

  1. // Extension Function
  2. fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; T) {
  3. val result = async { block }
  4. advanceTimeBy(5000L)
  5. val isBlockComplete = result.isCompleted
  6. assertThat(isBlockComplete, equalTo(false))
  7. }
  8. // Usage
  9. @Test
  10. fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
  11. runTest {
  12. assertNotCompleted {
  13. someClass.waitForValue()
  14. }
  15. }

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:

  1. // Extension Function
  2. @OptIn(ExperimentalCoroutinesApi::class)
  3. fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; T) {
  4. async(block = block).run {
  5. this@assertNotCompleted.advanceTimeBy(5000L)
  6. getCompletionExceptionOrNull()
  7. }
  8. }
  9. // Usage - wanting to avoid the need for expected property
  10. @Test(expected = IllegalStateException::class)
  11. fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
  12. runTest {
  13. assertNotCompleted {
  14. someClass.waitForValue()
  15. }
  16. }

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:

  1. // Extension Function
  2. @OptIn(ExperimentalCoroutinesApi::class)
  3. fun &lt;T&gt; TestScope.assertNotCompleted(block: suspend CoroutineScope.() -&gt; T) {
  4. runCatching {
  5. val result = async {
  6. block
  7. // advanceTimeBy(5000L) Test ALWAYS passes
  8. }
  9. // advanceTimeBy(5000L) Test ALWAYS fails
  10. result.getCompletionExceptionOrNull()
  11. }.also {
  12. assertThat(it.isFailure, equalTo(true))
  13. }
  14. }
  15. // 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

  1. suspend fun alwaysCompletes(): Int {
  2. delay(100)
  3. return 42
  4. }
  5. suspend fun neverCompletes() {
  6. while(true) {
  7. delay(1000)
  8. }
  9. }

These tests pass:

  1. @Test
  2. fun testAlwaysCompletes() {
  3. runBlocking {
  4. val job = launch {
  5. someClass.alwaysCompletes()
  6. }
  7. delay(5000) // wait for 5 seconds
  8. assertThat(true, equalTo(job.isCompleted ))
  9. }
  10. }
  11. @Test
  12. fun testNeverCompletes() {
  13. runBlocking {
  14. val job = launch {
  15. someClass.neverCompletes()
  16. }
  17. delay(5000) // wait for 5 seconds
  18. assertThat(false, equalTo(job.isCompleted ))
  19. job.cancelAndJoin() // cancel the job to avoid leaking resources
  20. }
  21. }


or simulating the time with advanceTimeBy:



  1. @Test
  2. fun testAlwaysCompletes() = runTest {
  3. val job = launch {
  4. someClass.alwaysCompletes()
  5. }
  6. advanceTimeBy(10_000) // advance time by 10,000ms
  7. assertFalse(job.isActive) // job should not be active
  8. assertTrue(job.isCompleted) // job should now be completed
  9. }
  10. @Test
  11. fun testNeverCompletes() = runTest {
  12. val job = launch {
  13. someClass.neverCompletes()
  14. }
  15. advanceTimeBy(10_000) // advance time by 10,000ms
  16. assertTrue(job.isActive) // job should still be active
  17. job.cancelAndJoin() // cancel the job to avoid leaking resources
  18. assertFalse(job.isActive) // job should now be cancelled and not active
  19. }

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:

确定