Ir al contenido principal
Versión: 6.2 🚧

Concurrencia

[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 →

nota

Este documento describe las nuevas funcionalidades de concurrencia introducidas en Kotest 6.0. Si estás utilizando una versión anterior de Kotest, consulta la documentación previa sobre concurrencia.

La concurrencia es fundamental en Kotlin, con soporte del compilador para continuaciones (funciones suspendidas), lo que permite la potente biblioteca de corrutinas, además de las herramientas estándar de concurrencia de Java.

Por tanto, es lógico que un framework de pruebas para Kotlin ofrezca soporte completo para ejecutar tests concurrentemente, ya sea mediante llamadas bloqueantes tradicionales o funciones suspendibles.

Kotest ofrece las siguientes características:

  • Capacidad para lanzar especificaciones (specs) y pruebas concurrentemente.

  • Capacidad para especificar el despachador de corrutinas utilizado para ejecutar pruebas.

  • Capacidad para ejecutar pruebas que usan APIs bloqueantes en un hilo separado exclusivo para esa prueba.

Estas funcionalidades son ortogonales pero complementarias.

Por defecto, Kotest ejecuta cada caso de prueba secuencialmente usando Dispatchers.Default. Esto significa que si una prueba se suspende o bloquea, todo el conjunto de pruebas se detendrá hasta que esa prueba se reanude.

Esta configuración predeterminada es la más segura, ya que no exige al usuario escribir pruebas seguras para hilos (thread-safe). Por ejemplo, las pruebas pueden compartir estado o usar campos de instancia que no son seguros para hilos. Evita condiciones de carrera y elimina la necesidad de conocer el modelo de memoria de Java. Las especificaciones pueden usar métodos before/after con la certeza de que no interferirán entre sí.

Sin embargo, algunos usuarios querrán ejecutar pruebas concurrentemente para reducir el tiempo total de ejecución de su suite de pruebas. Esto es especialmente útil al probar código que se suspende o bloquea: las mejoras de rendimiento al permitir ejecución concurrente pueden ser significativas.

Modos de Concurrencia

nota

Los modos de concurrencia descritos a continuación solo están disponibles en la plataforma JVM. En otras plataformas, las pruebas siempre se ejecutarán secuencialmente.

Kotest ofrece dos tipos de modos de concurrencia:

  1. Modo de Concurrencia de Especificaciones (Spec) - Controla cómo se ejecutan las especificaciones (clases de prueba) entre sí.

  2. Modo de Concurrencia de Pruebas (Test) - Controla cómo se ejecutan las pruebas raíz dentro de una especificación entre sí.

Modo de Concurrencia de Especificaciones

Este modo determina si múltiples especificaciones pueden ejecutarse simultáneamente. Hay tres opciones:

  • Sequential (Secuencial) - Todas las especificaciones se ejecutan secuencialmente (modo predeterminado).

  • Concurrent (Concurrente) - Todas las especificaciones se ejecutan concurrentemente.

  • LimitedConcurrency(max: Int) (Concurrencia Limitada) - Las especificaciones se ejecutan concurrentemente hasta un número máximo dado.

Puedes configurar este modo en tu configuración de proyecto:

class MyProjectConfig : AbstractProjectConfig() {
override val specExecutionMode = SpecExecutionMode.Concurrent
}

O para concurrencia limitada:

class MyProjectConfig : AbstractProjectConfig() {
override val specExecutionMode = SpecExecutionMode.LimitedConcurrency(4) // Run up to 4 specs concurrently
}

Modo de Concurrencia de Pruebas

Este modo determina si múltiples pruebas raíz dentro de una especificación pueden ejecutarse simultáneamente. Nota: las pruebas anidadas (definidas dentro de otras pruebas) no se ven afectadas por esta configuración; siempre se ejecutarán secuencialmente.

Hay tres opciones:

  • Sequential (Secuencial) - Todas las pruebas se ejecutan secuencialmente (modo predeterminado).

  • Concurrent (Concurrente) - Todas las pruebas se ejecutan concurrentemente.

  • LimitedConcurrency(max: Int) (Concurrencia Limitada) - Las pruebas se ejecutan concurrentemente hasta un número máximo dado.

Puedes configurar este modo en diferentes niveles:

Configuración global del proyecto

Se aplicará a todas las especificaciones y pruebas del proyecto, a menos que se sobrescriba en un nivel inferior.

class MyProjectConfig : AbstractProjectConfig() {
override val testExecutionMode = TestExecutionMode.Concurrent
}

Configuración por paquete

Esta configuración permite establecer el modo de ejecución para todas las especificaciones en un paquete específico, y solo está disponible en la plataforma JVM.

class MyPackageConfig : AbstractPackageConfig() {
override val testExecutionMode = TestExecutionMode.Concurrent
}

Configuración a nivel de especificación

Puedes configurar el modo de concurrencia para tests en una especificación concreta de dos formas:

  1. Sobrescribiendo la función testExecutionMode():
class MySpec : FreeSpec() {
override fun testExecutionMode() = TestExecutionMode.Concurrent

// tests...
}
  1. Estableciendo la propiedad testExecutionMode:
class MySpec : FreeSpec() {
init {
testExecutionMode = TestExecutionMode.Concurrent

// tests...
}
}

Aislamiento

A veces puedes tener pruebas que no son seguras para ejecutar concurrentemente con otras (quizás porque mutan algún estado externo o algo similar), incluso si el resto de la suite de pruebas sigue siendo seguro para ejecutar concurrentemente.

Para dar soporte a esto, Kotest permite bifurcar las especificaciones en dos contextos: aquellas que pueden ejecutarse concurrentemente con otras especificaciones y aquellas que deben ejecutarse secuencialmente, en aislamiento.

Para marcar una especificación como aislada, añade la anotación @Isolate a la clase de la especificación:

@Isolate
class SomeIsolationSpec : FreeSpec() {
// tests
}

Por defecto, todas las especificaciones no están aisladas y son aptas para ejecutarse concurrentemente si los modos de concurrencia están habilitados según la documentación anterior. Las especificaciones aisladas se ejecutarán primero de forma secuencial, antes de que las no aisladas se ejecuten juntas.

consejo

En Kotest 6.1, el objeto de configuración del proyecto tiene un ajuste concurrencyOrder que se puede usar para controlar si las especificaciones aisladas deben ejecutarse primero o al final.

Ejemplos

Ejemplo: Ejecución concurrente de tests dentro de una especificación

class ConcurrentTestsSpec : FreeSpec({

// Configure this spec to run tests concurrently
testExecutionMode = TestExecutionMode.Concurrent

"test 1" {
// This test will run concurrently with other tests
delay(1000)
// assertions...
}

"test 2" {
// This test will run concurrently with other tests
delay(500)
// assertions...
}

"test 3" {
// This test will run concurrently with other tests
delay(200)
// assertions...
}
})

Ejemplo: Concurrencia limitada para tests

class LimitedConcurrencySpec : FreeSpec({
// Configure this spec to run up to 2 tests concurrently
testExecutionMode = TestExecutionMode.LimitedConcurrency(2)

// tests...
})

Ejemplo: Combinación de modos de concurrencia para especificaciones y tests

class MyProjectConfig : AbstractProjectConfig() {
// Run up to 3 specs concurrently
override val specExecutionMode = SpecExecutionMode.LimitedConcurrency(3)

// By default, run tests sequentially within each spec
override val testExecutionMode = TestExecutionMode.Sequential
}

// Override the test execution mode for a specific spec
class ConcurrentTestsSpec : FreeSpec({
// This spec will run its tests concurrently
testExecutionMode = TestExecutionMode.Concurrent

// tests...
})

Fábrica de Despachadores de Corrutinas

Kotest te permite personalizar el despachador de corrutinas usado para ejecutar especificaciones y tests mediante la característica CoroutineDispatcherFactory. Esto te ofrece un control granular sobre el contexto de ejecución de tus pruebas.

La interfaz CoroutineDispatcherFactory proporciona métodos para cambiar el CoroutineDispatcher utilizado en:

  1. Callbacks de especificación (como beforeSpec y afterSpec)

  2. Ejecución de casos de prueba

Funcionamiento

La interfaz CoroutineDispatcherFactory tiene dos métodos principales:

interface CoroutineDispatcherFactory {
// For spec callbacks
suspend fun <T> withDispatcher(spec: Spec, f: suspend () -> T): T

// For test case execution
suspend fun <T> withDispatcher(testCase: TestCase, f: suspend () -> T): T

// Closes resources when the test engine completes
fun close() {}
}

Cuando se configura una CoroutineDispatcherFactory, Kotest la utilizará para determinar qué despachador emplear durante la ejecución de especificaciones y tests.

Opciones de configuración

Puedes configurar una CoroutineDispatcherFactory en diferentes niveles:

Configuración global del proyecto

class MyProjectConfig : AbstractProjectConfig() {
override val coroutineDispatcherFactory = ThreadPerSpecCoroutineContextFactory
}

Configuración a nivel de especificación

class MySpec : FreeSpec() {
// Option 1: Using property
init {
coroutineDispatcherFactory = ThreadPerSpecCoroutineContextFactory

// tests...
}

// Option 2: Using function
override fun coroutineDispatcherFactory() = ThreadPerSpecCoroutineContextFactory
}

Implementaciones integradas

Kotest proporciona una implementación integrada llamada ThreadPerSpecCoroutineContextFactory que crea un hilo dedicado por especificación.

Esta implementación:

  • Crea un hilo dedicado para cada especificación

  • Utiliza ese hilo como despachador de corrutinas para la especificación y todos sus tests

  • Finaliza el hilo cuando la especificación se completa

Ejemplo de implementación personalizada

Puedes crear tu propia implementación personalizada para adaptarte a necesidades específicas:

object CustomDispatcherFactory : CoroutineDispatcherFactory {

// A fixed thread pool with 4 threads
private val dispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

override suspend fun <T> withDispatcher(spec: Spec, f: suspend () -> T): T {
return withContext(dispatcher) {
f()
}
}

override suspend fun <T> withDispatcher(testCase: TestCase, f: suspend () -> T): T {
return withContext(dispatcher) {
f()
}
}

override fun close() {
dispatcher.close()
}
}

Casos de uso

La característica coroutineDispatcherFactory es útil para:

  1. Optimización de rendimiento: Usar un hilo dedicado por especificación puede mejorar el rendimiento reduciendo cambios de contexto

  2. Aislamiento de recursos: Garantizar que cada especificación se ejecute en su propio hilo ayuda a aislar tests entre sí

  3. Modelos de threading personalizados: Implementar estrategias específicas de gestión de hilos para tu suite de pruebas

  4. Pruebas con despachadores específicos: Probar código que se comporta de manera diferente en distintos despachadores

Modo de Pruebas Bloqueantes

Al trabajar con código bloqueante en tests, puedes encontrarte con problemas donde los tiempos de espera no funcionan como se espera. Esto ocurre porque los timeouts en corrutinas son cooperativos por naturaleza, lo que significa que dependen de que la corrutina ceda el control al planificador.

Para abordar este problema, Kotest ofrece un modo blockingTest que puede activarse por prueba individual:

"test with blocking code" {
// Enable blocking test mode for this test
blockingTest = true

// Your test with blocking code...
Thread.sleep(1000) // Example of blocking code
}

Cuando blockingTest está activado:

  • La ejecución cambia a un hilo dedicado para el caso de prueba

  • Esto permite al motor de pruebas interrumpir tests de forma segura mediante Thread.interrupt cuando exceden su tiempo

  • Otros tests pueden continuar ejecutándose concurrentemente si así está configurado

Ejemplo: Uso del modo blockingTest con tiempos de espera

class BlockingTestSpec : FreeSpec({
"test with timeout and blocking code".config(blockingTest = true, timeout = 500.milliseconds) {
// This blocking call would normally prevent the timeout from working
// With blockingTest = true, the test will be interrupted after 500ms
Thread.sleep(1000)
}
})
consejo

El modo blockingTest solo es necesario cuando usas llamadas bloqueantes en tus tests. Para pruebas que usan funciones suspendidas, el mecanismo de timeout regular funciona correctamente sin necesidad de activar este modo.