Ejecución de pruebas JUnit en paralelo con Maven

1. Introducción

Aunque la ejecución de pruebas en serie funciona bien la mayor parte del tiempo, es posible que deseemos paralelizarlas para acelerar las cosas.

En este tutorial, cubriremos cómo paralelizar las pruebas usando JUnit y el complemento Surefire de Maven. Primero, ejecutaremos todas las pruebas en un solo proceso JVM, luego lo probaremos con un proyecto de varios módulos.

2. Dependencias de Maven

Comencemos por importar las dependencias requeridas. Necesitaremos usar JUnit 4.7 o posterior junto con Surefire 2.16 o posterior:

 junit junit 4.12 test 
 org.apache.maven.plugins maven-surefire-plugin 2.22.0 

En pocas palabras, Surefire proporciona dos formas de ejecutar pruebas en paralelo:

  • Multithreading dentro de un solo proceso JVM
  • Bifurcación de múltiples procesos JVM

3. Ejecución de pruebas en paralelo

Para ejecutar una prueba en paralelo, debemos usar un ejecutor de pruebas que amplíe org.junit.runners.ParentRunner .

Sin embargo, incluso las pruebas que no declaran un corredor de prueba explícito funcionan, ya que el corredor predeterminado extiende esta clase.

A continuación, para demostrar la ejecución de pruebas en paralelo, usaremos un conjunto de pruebas con dos clases de prueba, cada una con algunos métodos. De hecho, cualquier implementación estándar de un conjunto de pruebas JUnit funcionaría.

3.1. Usando el parámetro paralelo

Primero, habilitemos el comportamiento paralelo en Surefire usando el parámetro paralelo . Indica el nivel de granularidad al que nos gustaría aplicar el paralelismo.

Los posibles valores son:

  • métodos: ejecuta métodos de prueba en subprocesos separados
  • clases: ejecuta clases de prueba en subprocesos separados
  • classesAndMethods: ejecuta clases y métodos en subprocesos separados
  • suites: ejecuta suites en paralelo
  • suitesAndClasses: ejecuta suites y clases en subprocesos separados
  • suitesAndMethods: crea subprocesos separados para clases y métodos
  • all: ejecuta suites, clases y métodos en subprocesos separados

En nuestro ejemplo, usamos todos :

 all 

En segundo lugar, definamos el número total de subprocesos que queremos que Surefire cree. Podemos hacerlo de dos formas:

Usando threadCount que define el número máximo de subprocesos que Surefire creará:

10

O usando el parámetro useUnlimitedThreads donde se crea un hilo por núcleo de CPU:

true

De forma predeterminada, threadCount es por núcleo de CPU. Podemos usar el parámetro perCoreThreadCount para habilitar o deshabilitar este comportamiento:

true

3.2. Uso de limitaciones de número de subprocesos

Ahora, digamos que queremos definir el número de subprocesos a crear a nivel de método, clase y suite. Podemos hacer esto con los parámetros threadCountMethods , threadCountClasses y threadCountSuites .

Combinemos estos parámetros con threadCount de la configuración anterior:

2 2 6

Dado que usamos todo en paralelo, hemos definido los recuentos de subprocesos para métodos, suites y clases. Sin embargo, no es obligatorio definir el parámetro hoja. Surefire deduce el número de subprocesos a utilizar en caso de que se omitan los parámetros de hoja.

Por ejemplo, si se omite threadCountMethods , entonces solo necesitamos asegurarnos de que threadCount > threadCountClasses + threadCountSuites.

A veces, es posible que deseemos limitar el número de subprocesos creados para clases, suites o métodos, incluso cuando estamos usando un número ilimitado de subprocesos.

También podemos aplicar limitaciones de conteo de subprocesos en tales casos:

true 2

3.3. Establecer tiempos de espera

A veces, es posible que debamos asegurarnos de que la ejecución de la prueba tenga un límite de tiempo.

Para hacer eso, podemos usar el parámetro paraleloTestTimeoutForcedInSeconds . Esto interrumpirá los subprocesos actualmente en ejecución y no ejecutará ninguno de los subprocesos en cola una vez transcurrido el tiempo de espera:

5

Otra opción es utilizar paralelosTestTimeoutInSeconds .

En este caso, solo se detendrá la ejecución de los subprocesos en cola:

3.5

Sin embargo, con ambas opciones, las pruebas terminarán con un mensaje de error cuando haya transcurrido el tiempo de espera.

3.4. Advertencias

Surefire llama a métodos estáticos anotados con @Parameters , @BeforeClass y @AfterClass en el hilo principal. Por lo tanto, asegúrese de verificar posibles inconsistencias de memoria o condiciones de carrera antes de ejecutar pruebas en paralelo.

Además, las pruebas que mutan el estado compartido definitivamente no son buenas candidatas para ejecutarse en paralelo.

4. Ejecución de pruebas en proyectos Maven de varios módulos

Hasta ahora, nos hemos centrado en ejecutar pruebas en paralelo dentro de un módulo de Maven.

Pero digamos que tenemos varios módulos en un proyecto de Maven. Dado que estos módulos se construyen secuencialmente, las pruebas para cada módulo también se ejecutan secuencialmente.

Podemos cambiar este comportamiento predeterminado usando el parámetro -T de Maven que construye módulos en paralelo . Esto se puede hacer de dos maneras.

Podemos especificar el número exacto de subprocesos que se utilizarán durante la construcción del proyecto:

mvn -T 4 surefire:test

O use la versión portátil y especifique la cantidad de subprocesos para crear por núcleo de CPU:

mvn -T 1C surefire:test

De cualquier manera, podemos acelerar las pruebas y construir tiempos de ejecución.

5. Bifurcación de JVM

Con la ejecución de prueba en paralelo a través de la opción en paralelo , la simultaneidad ocurre dentro del proceso de JVM usando subprocesos .

Dado que los subprocesos comparten el mismo espacio de memoria, esto puede ser eficiente en términos de memoria y velocidad. Sin embargo, podemos encontrarnos con condiciones de carrera inesperadas u otras fallas de prueba sutiles relacionadas con la concurrencia. Resulta que compartir el mismo espacio de memoria puede ser tanto una bendición como una maldición.

Para evitar problemas de simultaneidad a nivel de subprocesos, Surefire proporciona otro modo de ejecución de prueba en paralelo: bifurcación y simultaneidad a nivel de proceso . La idea de los procesos bifurcados es bastante simple. En lugar de generar varios subprocesos y distribuir los métodos de prueba entre ellos, surefire crea nuevos procesos y hace la misma distribución.

Dado que no hay memoria compartida entre diferentes procesos, no sufriremos esos sutiles errores de concurrencia. Por supuesto, esto se produce a expensas de un mayor uso de memoria y un poco menos de velocidad.

De todos modos, para habilitar la bifurcación, solo tenemos que usar la propiedad forkCount y establecerla en cualquier valor positivo:

3

Aquí, surefire creará como máximo tres bifurcaciones desde la JVM y ejecutará las pruebas en ellas. El valor predeterminado de forkCount es uno, lo que significa que maven-surefire-plugin crea un nuevo proceso JVM para ejecutar todas las pruebas en un módulo Maven.

La propiedad forkCount admite la misma sintaxis que -T . Es decir, si agregamos la C al valor, ese valor se multiplicará por el número de núcleos de CPU disponibles en nuestro sistema. Por ejemplo:

2.5C

Luego, en una máquina de dos núcleos, Surefire puede crear como máximo cinco bifurcaciones para la ejecución de pruebas en paralelo.

De forma predeterminada, Surefire reutilizará las bifurcaciones creadas para otras pruebas . Sin embargo, si establecemos la propiedad reuseForks en false , destruirá cada bifurcación después de ejecutar una clase de prueba.

Además, para deshabilitar la bifurcación, podemos establecer forkCount en cero.

6. Conclusión

En resumen, comenzamos habilitando el comportamiento multiproceso y definiendo el grado de paralelismo utilizando el parámetro paralelo . Posteriormente, aplicamos limitaciones en la cantidad de subprocesos que Surefire debería crear. Posteriormente, configuramos los parámetros de tiempo de espera para controlar los tiempos de ejecución de las pruebas.

Finalmente, analizamos cómo podemos reducir los tiempos de ejecución de la compilación y, por lo tanto, probar los tiempos de ejecución en proyectos Maven de varios módulos.

Como siempre, el código presentado aquí está disponible en GitHub.