Skip to main content
Version: 5.8

BlockHound

The Kotest BlockHound extension activates BlockHound support for coroutines. It helps to detect blocking code on non-blocking coroutine threads, e.g. when accidentally calling a blocking I/O library function on a UI thread.

note

To use this extension add the io.kotest.extensions:kotest-extensions-blockhound module to your test compile path.

Getting Started​

Register the BlockHound extension in your test class:

class BlockHoundSpecTest : FunSpec({
extension(BlockHound())

test("detects for spec") {
blockInNonBlockingContext()
}
})

The BlockHound extension can also be registered per test case or at the project level.

If BlockHound is enabled project-wide or spec-wide, you can disable it for an individual test:

   test("allow blocking").config(extensions = listOf(BlockHound(BlockHoundMode.DISABLED))) {
blockInNonBlockingContext()
}

You can also change BlockHoundMode for a section of code:

   test("allow blocking section") {
// ...
withBlockHoundMode(BlockHoundMode.DISABLED) {
blockInNonBlockingContext()
}
// ...
}

Detection​

Blocking calls will be detected in coroutine threads which are expected not to block. Such threads are created by the default dispatcher as this example demonstrates:

private suspend fun blockInNonBlockingContext() {
withContext(Dispatchers.Default) {
@Suppress("BlockingMethodInNonBlockingContext")
Thread.sleep(2)
}
}

The BlockHound extension will by default produce an exception like this whenever it detects a blocking call:

reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
at io.kotest.extensions.blockhound.KotestBlockHoundIntegration.applyTo$lambda-2$lambda-1(KotestBlockHoundIntegration.kt:27)
at reactor.blockhound.BlockHound$Builder.lambda$install$8(BlockHound.java:427)
at reactor.blockhound.BlockHoundRuntime.checkBlocking(BlockHoundRuntime.java:89)
at java.base/java.lang.Thread.sleep(Thread.java)
at io.kotest.extensions.blockhound.BlockHoundTestKt$blockInNonBlockingContext$2.invokeSuspend(BlockHoundTest.kt:17)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
note

By invoking it as BlockHound(BlockHoundMode.PRINT), it will print detected calls and continue the test without interruption.

Whenever a blocking call is detected, you can

  • replace the call with a non-blocking one (using a coroutine-aware library), or
  • schedule the calling coroutine to run on a separate I/O thread (e.g. via Dispatchers.IO), or
  • add an exception if the blocking is harmless (see below).

Customization​

To customize BlockHound, familiarize yourself with the BlockHound documentation.

Exceptions for blocking calls considered harmless can be added via a separate BlockHoundIntegration class like this:

import reactor.blockhound.BlockHound
import reactor.blockhound.integration.BlockHoundIntegration

class MyBlockHoundIntegration : BlockHoundIntegration {
override fun applyTo(builder: BlockHound.Builder): Unit = with(builder) {
allowBlockingCallsInside("org.slf4j.LoggerFactory", "performInitialization")
}
}

In order to allow BlockHound to auto-detect and load the integration, add its fully qualified class name to a service provider configuration file resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration.