Saltar al contenido principal

Ejecutando pruebas de Serenity con JUnit 5 en paralelo

Ejecutar pruebas de JUnit 5 en paralelo puede reducir significativamente el tiempo de ejecucion de las pruebas. Esta guia explica como configurar y optimizar la ejecucion paralela para pruebas de Serenity BDD usando JUnit 5.

Descripcion general

JUnit 5 (Jupiter) tiene soporte integrado para la ejecucion paralela de pruebas. Cuando se combina con Serenity BDD, puedes ejecutar metodos y clases de prueba concurrentemente, aprovechando los procesadores multinucleo.

Beneficios de la ejecucion paralela

En una maquina con 8 nucleos, la ejecucion paralela puede reducir el tiempo de prueba de 30 minutos a 5-8 minutos o menos.

Prerrequisitos

  • JUnit 5 (Jupiter)
  • Serenity BDD 3.6.0 o superior
  • Codigo de prueba seguro para hilos (thread-safe)
  • Recursos de sistema suficientes (CPU, RAM)

Configuracion basica

Paso 1: Habilitar la ejecucion paralela

Crea o actualiza junit-platform.properties en src/test/resources:

# Habilitar ejecucion paralela
junit.jupiter.execution.parallel.enabled=true

Paso 2: Configurar el modo de ejecucion

# Ejecutar clases de prueba en paralelo
junit.jupiter.execution.parallel.mode.default=concurrent

# Ejecutar clases de prueba en paralelo
junit.jupiter.execution.parallel.mode.classes.default=concurrent

Paso 3: Configurar la estrategia de ejecucion

# Usar estrategia dinamica (recomendado)
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Modos de ejecucion

Modo concurrente

Las pruebas se ejecutan en paralelo:

junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

Modo mismo hilo

Las pruebas se ejecutan secuencialmente:

junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=same_thread

Modo mixto

Ejecuta clases en paralelo pero los metodos dentro de una clase secuencialmente:

# Clases en paralelo
junit.jupiter.execution.parallel.mode.classes.default=concurrent

# Metodos en la misma clase se ejecutan secuencialmente
junit.jupiter.execution.parallel.mode.default=same_thread

Estrategias de ejecucion

Estrategia dinamica (Recomendada)

Determina automaticamente el numero de hilos basandose en los procesadores disponibles:

junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Calculo del factor: hilos = procesadores x factor

Ejemplos (en una maquina de 8 nucleos):

  • Factor 1.0 = 8 hilos (recomendado)
  • Factor 0.5 = 4 hilos
  • Factor 2.0 = 16 hilos

Estrategia fija

Usa un numero especificado de hilos:

junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4

Usar cuando:

  • Necesitas conteos de hilos consistentes
  • Ejecutas en infraestructura CI/CD compartida
  • Depuras problemas de ejecucion paralela

Estrategia personalizada

Implementa tu propia estrategia:

junit.jupiter.execution.parallel.config.strategy=custom
junit.jupiter.execution.parallel.config.custom.class=com.example.MyParallelStrategy

Controlando el paralelismo a nivel de clase/metodo

Configuracion por clase

Usa la anotacion @Execution para controlar el paralelismo:

import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.CONCURRENT) // Ejecutar los metodos de esta clase en paralelo
class FastTests {

@Test
void test1() { }

@Test
void test2() { }

@Test
void test3() { }
}

Forzar ejecucion secuencial

@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.SAME_THREAD) // Ejecutar secuencialmente
class DatabaseMigrationTests {

@Test
void migration1() { }

@Test
void migration2() { } // Debe ejecutarse despues de migration1
}

Control a nivel de metodo

@ExtendWith(SerenityJUnit5Extension.class)
class MixedTests {

@Test
@Execution(ExecutionMode.CONCURRENT)
void canRunInParallel() { }

@Test
@Execution(ExecutionMode.SAME_THREAD)
void mustRunSequentially() { }
}

Bloqueos de recursos

Previene el acceso concurrente a recursos compartidos:

import org.junit.jupiter.api.parallel.ResourceLock;

@ExtendWith(SerenityJUnit5Extension.class)
class SharedResourceTests {

@Test
@ResourceLock("database")
void accessDatabase1() {
// Acceso exclusivo a la base de datos
}

@Test
@ResourceLock("database")
void accessDatabase2() {
// Esperara a que accessDatabase1 complete
}

@Test
@ResourceLock(value = "database", mode = READ)
void readFromDatabase() {
// Acceso de solo lectura - puede ejecutarse concurrentemente con otras lecturas
}
}

Bloqueos de recursos comunes

import static org.junit.jupiter.api.parallel.Resources.*;

@Test
@ResourceLock(SYSTEM_PROPERTIES)
void modifiesSystemProperties() { }

@Test
@ResourceLock(SYSTEM_OUT)
void writesToSystemOut() { }

@Test
@ResourceLock("my-custom-resource")
void accessesCustomResource() { }

Seguridad de hilos con Serenity

Gestion de WebDriver

El WebDriver @Managed de Serenity es automaticamente seguro para hilos:

@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.CONCURRENT)
class WebTests {

@Managed(driver = "chrome", options = "headless")
WebDriver driver; // Cada hilo obtiene su propia instancia

@Test
void test1() {
driver.get("https://example.com");
// Seguro para hilos
}

@Test
void test2() {
driver.get("https://example.com/other");
// Instancia de WebDriver independiente
}
}

Bibliotecas de pasos

Las bibliotecas de pasos son seguras para hilos cuando se usan correctamente:

@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.CONCURRENT)
class CheckoutTests {

@Steps
CartSteps cart; // Seguro para hilos - instancia por hilo

@Test
void addToCart() {
cart.addProduct("Product1");
}

@Test
void removeFromCart() {
cart.addProduct("Product2");
cart.removeProduct("Product2");
}
}

Evitar estado mutable compartido

// MAL - No es seguro para hilos
@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.CONCURRENT)
class BadTests {

private String sharedData; // Estado mutable compartido!

@Test
void test1() {
sharedData = "test1"; // Condicion de carrera!
}

@Test
void test2() {
sharedData = "test2"; // Condicion de carrera!
}
}

// BIEN - Seguro para hilos
@ExtendWith(SerenityJUnit5Extension.class)
@Execution(ExecutionMode.CONCURRENT)
class GoodTests {

@Test
void test1() {
String localData = "test1"; // Variable local - segura para hilos
// Usar localData
}

@Test
void test2() {
String localData = "test2"; // Independiente
// Usar localData
}
}

Ejemplo de configuracion completa

junit-platform.properties completo

# ==========================================
# Ejecucion paralela
# ==========================================
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

# ==========================================
# Estrategia de ejecucion
# ==========================================
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

# Alternativa: Estrategia fija
# junit.jupiter.execution.parallel.config.strategy=fixed
# junit.jupiter.execution.parallel.config.fixed.parallelism=4
# junit.jupiter.execution.parallel.config.fixed.max-pool-size=8

# ==========================================
# Ciclo de vida de instancia de prueba
# ==========================================
junit.jupiter.testinstance.lifecycle.default=per_method

# ==========================================
# Descubrimiento de pruebas
# ==========================================
junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$Random

# ==========================================
# Nombres de visualizacion
# ==========================================
junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

Configuracion de Maven

Plugin Failsafe

Configura el plugin Maven Failsafe para la ejecucion paralela:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<!-- Habilitar JUnit 5 -->
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>

<!-- Propiedades del sistema -->
<systemPropertyVariables>
<webdriver.driver>${webdriver.driver}</webdriver.driver>
<environment>${environment}</environment>
</systemPropertyVariables>

<!-- Incrementar memoria para ejecucion paralela -->
<argLine>-Xmx2g</argLine>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>

Ejecutar desde la linea de comandos

# Ejecutar todas las pruebas en paralelo
mvn clean verify

# Sobrescribir paralelismo
mvn clean verify -Djunit.jupiter.execution.parallel.config.fixed.parallelism=8

# Deshabilitar ejecucion paralela
mvn clean verify -Djunit.jupiter.execution.parallel.enabled=false

Configuracion de Gradle

test {
useJUnitPlatform()

// Habilitar ejecucion paralela
maxParallelForks = Runtime.runtime.availableProcessors()

// Incrementar memoria
minHeapSize = "512m"
maxHeapSize = "2g"

// Propiedades del sistema
systemProperty 'junit.jupiter.execution.parallel.enabled', 'true'
systemProperty 'junit.jupiter.execution.parallel.config.strategy', 'dynamic'
systemProperty 'junit.jupiter.execution.parallel.config.dynamic.factor', '1.0'
}

Optimizacion del rendimiento

1. Elegir la estrategia correcta

Suites de pruebas pequenas (<30 pruebas):

junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=2

Suites de pruebas medianas (30-100 pruebas):

junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Suites de pruebas grandes (>100 pruebas):

junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=0.75

2. Monitorear el uso de recursos

# Monitorear mientras se ejecutan las pruebas
top
htop
jconsole

Observa:

  • CPU: Deberia estar cerca del 100% de utilizacion
  • Memoria: Cada instancia del navegador usa ~200-500MB de RAM
  • Hilos: Deberian coincidir con tu configuracion de paralelismo

3. Optimizar el diseno de pruebas

// BIEN - Pruebas rapidas y enfocadas
@Test
void shouldCalculateTotal() {
// Sin dependencias externas, se ejecuta rapidamente
assertThat(calculator.add(2, 3)).isEqualTo(5);
}

// LENTO - Considerar optimizacion
@Test
void shouldProcessLargeDataset() {
// Procesa 10000 registros - considerar usar un conjunto de datos mas pequeno
}

4. Balancear el numero de hilos

Muy pocos hilos:

# Subutilizando CPU
junit.jupiter.execution.parallel.config.dynamic.factor=0.25

Demasiados hilos:

# Puede causar agotamiento de recursos
junit.jupiter.execution.parallel.config.dynamic.factor=4.0

Optimo:

# Coincide con los nucleos de CPU
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Depurando pruebas paralelas

Deshabilitar temporalmente el paralelismo

junit.jupiter.execution.parallel.enabled=false

O via linea de comandos:

mvn verify -Djunit.jupiter.execution.parallel.enabled=false

Ejecutar una sola clase de prueba

mvn test -Dtest=MyTestClass

Habilitar registro detallado

Agregar a junit-platform.properties:

# Mostrar que hilo ejecuta cada prueba
junit.platform.output.capture.stdout=true
junit.platform.output.capture.stderr=true

En tu prueba:

@BeforeEach
void logThread() {
System.out.println("Running on: " + Thread.currentThread().getName());
}

Problemas comunes y soluciones

Problema 1: Pruebas inestables (flaky)

Sintomas: Las pruebas pasan individualmente pero fallan en paralelo

Soluciones:

  • Verifica si hay estado mutable compartido
  • Verifica el aislamiento de datos de prueba
  • Revisa los bloqueos de recursos
  • Agrega sincronizacion adecuada
// Problema: Estado compartido
private static int counter = 0; // No es seguro para hilos

@Test
void incrementCounter() {
counter++; // Condicion de carrera
}

// Solucion: Usar tipos atomicos o variables locales
private static AtomicInteger counter = new AtomicInteger(0); // Seguro para hilos

@Test
void incrementCounter() {
counter.incrementAndGet();
}

Problema 2: Agotamiento de recursos

Sintomas: Las pruebas se ralentizan o fallan despues de algun tiempo

Soluciones:

  • Reducir el numero de hilos
  • Incrementar el tamano del heap de la JVM
  • Verificar fugas de recursos
  • Monitorear los recursos del sistema
<!-- Incrementar tamano del heap -->
<argLine>-Xmx4g</argLine>

Problema 3: Problemas con WebDriver

Sintomas: Las instancias del navegador se multiplican o no se cierran

Soluciones:

  • Usar WebDriver @Managed (Serenity gestiona el ciclo de vida)
  • Asegurar limpieza adecuada en @AfterEach
  • Limitar las instancias concurrentes del navegador
// Serenity gestiona el ciclo de vida
@Managed
WebDriver driver;

// No gestionar manualmente en pruebas paralelas
WebDriver driver = new ChromeDriver(); // Riesgo de fuga de memoria

Problema 4: Bloqueos de base de datos

Sintomas: Las pruebas agotan el tiempo de espera esperando la base de datos

Soluciones:

  • Usar bloqueos de recursos
  • Aislar los datos de prueba
  • Usar base de datos en memoria para pruebas
@Test
@ResourceLock("database")
void modifiesDatabase() {
// Acceso exclusivo a la base de datos
}

Mejores practicas

1. Empezar pequeno

Comienza con 2-4 hilos e incrementa gradualmente:

junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=2

2. Aislar datos de prueba

@ExtendWith(SerenityJUnit5Extension.class)
class UserTests {

@Test
void createUser() {
String uniqueId = UUID.randomUUID().toString();
User user = createTestUser("user_" + uniqueId);
// Prueba con usuario unico
}
}

3. Usar etiquetas para paralelismo selectivo

@Test
@Tag("parallel")
void canRunInParallel() { }

@Test
@Tag("serial")
@Execution(ExecutionMode.SAME_THREAD)
void mustRunSequentially() { }
# Ejecutar solo pruebas paralelas
junit.jupiter.includeTags=parallel

4. Limpiar recursos

@AfterEach
void cleanup() {
// Limpiar datos de prueba
testDataService.deleteTestData(testId);
}

5. Documentar la seguridad de hilos

/**
* Las pruebas en esta clase modifican el estado global de la aplicacion
* y deben ejecutarse secuencialmente.
*/
@Execution(ExecutionMode.SAME_THREAD)
class GlobalStateTests {
// ...
}

Midiendo el rendimiento

Antes de la ejecucion paralela

Total de pruebas: 150
Tiempo de ejecucion: 25 minutos

Despues de la ejecucion paralela (8 hilos)

Total de pruebas: 150
Tiempo de ejecucion: 4 minutos
Aceleracion: 6.25x
Eficiencia: 78%

Calcular metricas:

Aceleracion = Tiempo secuencial / Tiempo paralelo
Eficiencia = Aceleracion / Numero de hilos x 100%

Proximos pasos

Recursos adicionales