Ir al contenido principal
Versión: 5.5.x

Eventually

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Al probar código no determinista, un caso de uso común es "espero que este código pase después de un breve período de tiempo".

Por ejemplo, al probar una operación de E/S, podrías necesitar esperar hasta que la operación haya completado su escritura.

A veces puedes usar Thread.sleep, pero esto no es ideal porque debes establecer un umbral de sueño lo suficientemente alto para que no expire prematuramente en máquinas lentas. Además, significa que tu prueba esperará inactiva el tiempo completo incluso si el código se completa rápidamente en máquinas rápidas.

O puedes implementar un bucle con reintentos y pausas, pero esto es solo código repetitivo que te ralentiza.

Otro enfoque común es usar contadores (countdown latches), lo cual funciona bien si puedes inyectarlos en los lugares apropiados, pero no siempre es posible hacer que el código bajo prueba active un contador.

Como alternativa, kotest proporciona la función eventually y la configuración Eventually que prueba periódicamente el código ignorando excepciones específicas y verificando que el resultado cumpla un predicado opcional, hasta alcanzar el timeout o superar el número máximo de iteraciones. Esta solución flexible es perfecta para probar código no determinista.

Ejemplos

Ejemplos básicos

Supongamos que enviamos un mensaje a un servicio asíncrono. Tras procesar el mensaje, se inserta una nueva fila en la tabla de usuarios.

Podemos verificar este comportamiento con nuestra función eventually.

class MyTests : ShouldSpec() {
init {
should("check if user repository has one row after message is sent") {
sendMessage()
eventually(5.seconds) {
userRepository.size() shouldBe 1
}
}
}
}

Excepciones

Por defecto, eventually ignorará cualquier AssertionError lanzado dentro de la función (nota: esto no incluye Error). Para ser más específico, puedes indicar a eventually que ignore excepciones concretas; cualquier otra fallará la prueba inmediatamente.

Supongamos que nuestro ejemplo anterior lanza UserNotFoundException mientras el usuario no se encuentra en la base de datos. Eventualmente devolverá el usuario cuando el sistema procese el mensaje.

En este escenario, podemos omitir explícitamente la excepción esperada hasta que la prueba tenga éxito, pero cualquier otra excepción no sería ignorada. Nota: este ejemplo es similar al anterior, pero si ocurriera otro error como ConnectionException, esto haría que el bloque eventually finalice inmediatamente con un mensaje de fallo.

class MyTests : ShouldSpec() {
init {
should("check if user repository has one row") {
eventually(5.seconds, UserNotFoundException::class.java) {
userRepository.findBy(1) shouldNotBe null
}
}
}
}

Predicados

Además de verificar que un caso de prueba eventualmente se ejecute sin excepciones, también podemos validar el resultado y tratar un resultado sin excepciones como fallido si no cumple ciertas condiciones.

class MyTests : StringSpec({
"check that predicate eventually succeeds in time" {
var i = 0
eventually<Int>(25.seconds, predicate = { it == 5 }) {
delay(1.seconds)
i++
}
}
})

Compartir configuración

Compartir la configuración para eventually es muy sencillo con la clase de datos Eventually. Imagina que has clasificado las operaciones de tu sistema en "lentas" y "rápidas". En lugar de recordar qué valores temporales corresponden a cada categoría, podemos crear objetos compartidos entre pruebas y personalizarlos por conjunto. ¡También es el momento perfecto para mostrar las capacidades de listeners de eventually, que proporcionan información sobre el valor actual del resultado y el estado de las iteraciones!

val slow = EventuallyConfig<ServerResponse, ServerException>(5.minutes, interval = 25.milliseconds.fibonacci(), exceptionClass = ServerException::class)
val fast = slow.copy(duration = 5.seconds)

class FooTests : StringSpec({
val logger = logger("FooTests")
val fSlow = slow.copy(listener = { i, t -> logger.info("Current $i after {${t.times} attempts")})

"server eventually provides a result for /foo" {
eventually(fSlow) {
fooApi()
}
}
})

class BarTests : StringSpec({
val logger = logger("BarTests")
val bFast = fast.copy(listener = { i, t -> logger.info("Current $i after {${t.times} attempts")})

"server eventually provides a result for /bar" {
eventually(bFast) {
barApi()
}
}
})

Aquí vemos cómo compartir configuración puede reducir código duplicado mientras permite flexibilidad para funcionalidades como registros personalizados por conjunto de pruebas, mejorando la claridad de los logs.