Core assertion and matcher library for Kotest testing framework
—
Support for testing asynchronous and eventually consistent systems with configurable retry strategies and timing controls.
Test assertions that may initially fail but should eventually succeed within a time limit.
/**
* Repeatedly execute test until it passes or timeout is reached
* Uses default configuration (5 seconds duration, 25ms interval)
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> eventually(test: suspend () -> T): T
/**
* Repeatedly execute test until it passes or specified duration elapses
* @param duration Maximum time to keep retrying
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> eventually(duration: Duration, test: suspend () -> T): T
/**
* Repeatedly execute test with custom configuration
* @param config Configuration for retry behavior
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> eventually(config: EventuallyConfiguration, test: suspend () -> T): TUsage Examples:
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.matchers.shouldBe
import kotlin.time.Duration.Companion.seconds
// Basic eventually with defaults (5 second timeout)
eventually {
remoteService.isHealthy() shouldBe true
}
// Custom timeout
eventually(10.seconds) {
database.countRecords() shouldBe 100
}
// With custom configuration
val config = eventuallyConfig {
duration = 30.seconds
interval = 100.milliseconds
initialDelay = 1.seconds
}
eventually(config) {
queue.isEmpty() shouldBe true
}Test assertions that should remain true for a specified duration.
/**
* Repeatedly execute test and verify it continues to pass
* @param test The test code to execute repeatedly
* @return The result of the test execution
*/
suspend fun <T> continually(test: suspend () -> T): T
/**
* Repeatedly execute test for specified duration
* @param duration How long to keep testing
* @param test The test code to execute repeatedly
* @return The result of the test execution
*/
suspend fun <T> continually(duration: Duration, test: suspend () -> T): T
/**
* Repeatedly execute test with custom configuration
* @param config Configuration for continual testing
* @param test The test code to execute repeatedly
* @return The result of the test execution
*/
suspend fun <T> continually(config: ContinuallyConfiguration, test: suspend () -> T): TUsage Examples:
import io.kotest.assertions.nondeterministic.continually
import io.kotest.matchers.shouldBe
// Verify service stays healthy for 30 seconds
continually(30.seconds) {
healthCheck.status shouldBe "OK"
}
// Verify memory usage stays under limit
continually {
Runtime.getRuntime().freeMemory() should beGreaterThan(1000000)
}Execute test repeatedly until it returns without throwing an exception.
/**
* Execute test repeatedly until it succeeds (doesn't throw)
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> until(test: suspend () -> T): T
/**
* Execute test repeatedly until it succeeds or timeout
* @param duration Maximum time to keep trying
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> until(duration: Duration, test: suspend () -> T): T
/**
* Execute test repeatedly with custom configuration
* @param config Configuration for retry behavior
* @param test The test code to execute repeatedly
* @return The result of the successful test execution
*/
suspend fun <T> until(config: UntilConfiguration, test: suspend () -> T): TConfiguration objects for customizing retry behavior and intervals.
/**
* Configuration for eventually testing
*/
data class EventuallyConfiguration(
val duration: Duration = 5.seconds,
val interval: Duration = 25.milliseconds,
val initialDelay: Duration = Duration.ZERO,
val listener: EventuallyListener = NoopEventuallyListener
)
/**
* Builder for creating EventuallyConfiguration
*/
class EventuallyConfigurationBuilder {
var duration: Duration = 5.seconds
var interval: Duration = 25.milliseconds
var initialDelay: Duration = Duration.ZERO
var listener: EventuallyListener = NoopEventuallyListener
}
/**
* Configuration for continually testing
*/
data class ContinuallyConfiguration<T>(
val duration: Duration = 1.seconds,
val interval: Duration = 25.milliseconds,
val listener: ContinuallyListener<T> = NoopContinuallyListener
)
/**
* Configuration for until testing
*/
data class UntilConfiguration(
val duration: Duration = 5.seconds,
val interval: Duration = 25.milliseconds,
val listener: UntilListener = NoopUntilListener
)Configuration Usage:
import io.kotest.assertions.nondeterministic.*
// Create configuration using builder
val config = eventuallyConfig {
duration = 60.seconds // Total timeout
interval = 500.milliseconds // Time between attempts
initialDelay = 2.seconds // Wait before first attempt
listener = { attempt, error ->
println("Attempt $attempt failed: ${error.message}")
}
}
eventually(config) {
externalApi.getData() shouldBe expectedData
}Monitor retry attempts and failures during nondeterministic testing.
/**
* Listener for eventually test attempts and failures
*/
typealias EventuallyListener = suspend (Int, Throwable) -> Unit
/**
* Listener for continually test executions
*/
typealias ContinuallyListener<T> = suspend (Int, T) -> Unit
/**
* Listener for until test attempts and failures
*/
typealias UntilListener = suspend (Int, Throwable) -> Unit
/**
* No-op implementation that ignores all events
*/
object NoopEventuallyListener : EventuallyListener {
override suspend fun invoke(attempt: Int, error: Throwable) {}
}Advanced retry strategies with customizable backoff patterns.
/**
* Interface for generating intervals between retry attempts
*/
interface DurationFn {
fun next(iteration: Int): Duration
}
/**
* Fibonacci backoff strategy with maximum duration limit
*/
class FibonacciIntervalFn(private val max: Duration) : DurationFn {
override fun next(iteration: Int): Duration
}
/**
* Exponential backoff strategy
*/
class ExponentialIntervalFn(
private val base: Duration = 25.milliseconds,
private val factor: Double = 2.0,
private val max: Duration = 1.seconds
) : DurationFn {
override fun next(iteration: Int): Duration
}
/**
* Create a fibonacci backoff function with maximum duration
* @receiver Base duration for calculations
* @param max Maximum duration to cap the backoff
* @return FibonacciIntervalFn instance
*/
fun Duration.fibonacci(max: Duration): FibonacciIntervalFnAdvanced Usage Examples:
import io.kotest.assertions.nondeterministic.*
// Using fibonacci backoff
val fibConfig = eventuallyConfig {
duration = 2.minutes
interval = FibonacciIntervalFn(10.seconds)
}
// Using exponential backoff
val expConfig = eventuallyConfig {
duration = 1.minutes
interval = ExponentialIntervalFn(
base = 100.milliseconds,
factor = 1.5,
max = 5.seconds
)
}
// Monitor retry attempts
val monitoredConfig = eventuallyConfig {
duration = 30.seconds
listener = { attempt, error ->
logger.warn("Retry attempt $attempt failed", error)
if (attempt > 10) {
metrics.increment("high_retry_count")
}
}
}Nondeterministic testing functions provide detailed error information:
All functions throw AssertionError when the final attempt fails, preserving the original assertion failure message.
Install with Tessl CLI
npx tessl i tessl/maven-io-kotest--kotest-assertions-core-jvm