Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expectThrows messes up the coroutine dispatcher #262

Open
jensbaitingerbosch opened this issue Feb 28, 2022 · 1 comment
Open

expectThrows messes up the coroutine dispatcher #262

jensbaitingerbosch opened this issue Feb 28, 2022 · 1 comment

Comments

@jensbaitingerbosch
Copy link

jensbaitingerbosch commented Feb 28, 2022

For Unit tests, there is the library org.jetbrains.kotlinx:kotlinx-coroutines-test that provides the function runTest(). It provides a couroutines dispatcher that makes delay() return immediately, so unit tests testing e.g. timeout are still fast.

When expectThrows() is used in such test, it does not 'pass' the coroutine scope. That means that delay() delay for "real" time.

Example (this test fails because the junit timeout fails the test after 100ms, while the delay is only returning after 1sec):

@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
fun `delays should return immediately`() = runTest {
 expectThrows<IllegalArgumentException> {
      delay(1000)
      throw IllegalArgumentException()
  }
}

a more 'classical' approach works:

@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
fun `delays should return immediately`() = runTest {
 try {
      delay(1000)
      throw IllegalArgumentException()
      fail("this should not be reached, we expect the previous line to throw an exception")
  } catch(e: IllegalArgumentException) {
     // ok
  }
}

Suggestion to fix that issue: All functions like expectThrows() that call code passed as a suspend function should do this using the coroutineScope() function. This would pass the coroutine scheduler.

@Antimonit
Copy link

Can confirm. Any Strikt API that accepts suspend lambda will not work as expected when used with dispatchers such as StandardTestDispatcher().

This applies to all expectCatching, expectThrows but also the plain expect.


I understand that using suspend lambda inside of these functions might be convenient, but it really should not do runBlocking on the user's behalf.

For example, the following will block the test forever.

@Test
fun `strikt fails`() {
    val dispatcher = StandardTestDispatcher()
    runTest {
        expectThrows<Exception> {
            withContext(dispatcher) {
                error("fail")
            }
        }
    }
}

Converting these two functions:

inline fun <reified E : Throwable> expectThrows(
  noinline action: suspend () -> Any?
): Builder<E> =
  expectCatching(action).failedWith()

fun <T : Any?> expectCatching(action: suspend () -> T): DescribeableBuilder<Result<T>> =
  expectThat(
    runCatching {
      runBlocking { action() }
    }
  )

to suspend:

suspend inline fun <reified E : Throwable> expectThrows(
    noinline action: suspend () -> Any?
): Assertion.Builder<E> =
    expectCatching(action).failedWith()

suspend fun <T : Any?> expectCatching(action: suspend () -> T): DescribeableBuilder<Result<T>> =
    expectThat(
        runCatching {
            action()
        }
    )

indeed fixes the issue and the test succeeds.


We can also utilize the fact that inline functions are achromatic in the sense that they don't care whether they are invoked from suspending or non-suspending context. This will work as well:

inline fun <reified E : Throwable> expectThrows(
    action: () -> Any?
): Assertion.Builder<E> =
    expectCatching(action).failedWith()

inline fun <T : Any?> expectCatching(action: () -> T): DescribeableBuilder<Result<T>> =
    expectThat(
        runCatching {
            action()
        }
    )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants