Esecuzione di test JUnit in parallelo con Maven

1. Introduzione

Sebbene l'esecuzione di test in serie funzioni bene per la maggior parte del tempo, potremmo volerli parallelizzare per velocizzare le cose.

In questo tutorial, tratteremo come parallelizzare i test utilizzando JUnit e il plug-in Surefire di Maven. Innanzitutto, eseguiremo tutti i test in un unico processo JVM, quindi lo proveremo con un progetto multimodulo.

2. Dipendenze di Maven

Cominciamo importando le dipendenze richieste. Dovremo usare JUnit 4.7 o successivo insieme a Surefire 2.16 o successivo:

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

In poche parole, Surefire offre due modi per eseguire i test in parallelo:

  • Multithreading all'interno di un singolo processo JVM
  • Forking di più processi JVM

3. Esecuzione di test paralleli

Per eseguire un test in parallelo, dovremmo usare un test runner che estende org.junit.runners.ParentRunner .

Tuttavia, anche i test che non dichiarano un test runner esplicito funzionano, poiché il runner predefinito estende questa classe.

Successivamente, per dimostrare l'esecuzione di test in parallelo, utilizzeremo una suite di test con due classi di test ciascuna con alcuni metodi. In effetti, qualsiasi implementazione standard di una suite di test JUnit andrebbe bene.

3.1. Utilizzo del parametro parallelo

Innanzitutto, abilitiamo il comportamento parallelo in Surefire utilizzando il parametro parallel . Indica il livello di granularità a cui vorremmo applicare il parallelismo.

I valori possibili sono:

  • metodi: esegue i metodi di test in thread separati
  • classi: esegue le classi di test in thread separati
  • classesAndMethods: esegue classi e metodi in thread separati
  • suites - gestisce le suite in parallelo
  • suitesAndClasses: esegue suite e classi in thread separati
  • suitesAndMethods: crea thread separati per classi e metodi
  • all - esegue suite, classi e metodi in thread separati

Nel nostro esempio, usiamo tutto :

 all 

In secondo luogo, definiamo il numero totale di thread che vogliamo che Surefire crei. Possiamo farlo in due modi:

Utilizzando threadCount che definisce il numero massimo di thread che Surefire creerà:

10

Oppure utilizzando il parametro useUnlimitedThreads in cui viene creato un thread per core della CPU:

true

Per impostazione predefinita, threadCount è per core della CPU. Possiamo utilizzare il parametro perCoreThreadCount per abilitare o disabilitare questo comportamento:

true

3.2. Utilizzo delle limitazioni del numero di thread

Supponiamo ora di voler definire il numero di thread da creare a livello di metodo, classe e suite. Possiamo farlo con i parametri threadCountMethods , threadCountClasses e threadCountSuites .

Combiniamo questi parametri con threadCount dalla configurazione precedente:

2 2 6

Poiché abbiamo utilizzato tutto in parallelo, abbiamo definito i conteggi dei thread per metodi, suite e classi. Tuttavia, non è obbligatorio definire il parametro foglia. Surefire deduce il numero di thread da utilizzare nel caso in cui i parametri foglia vengano omessi.

Ad esempio, se threadCountMethods viene omesso, dobbiamo solo assicurarci che threadCount > threadCountClasses + threadCountSuites.

A volte potremmo voler limitare il numero di thread creati per classi o suite o metodi anche mentre stiamo utilizzando un numero illimitato di thread.

Possiamo applicare limitazioni al numero di thread anche in questi casi:

true 2

3.3. Impostazione dei timeout

A volte potrebbe essere necessario garantire che l'esecuzione del test sia limitata nel tempo.

Per farlo possiamo usare il parametro parallelTestTimeoutForcedInSeconds . Questo interromperà i thread attualmente in esecuzione e non eseguirà nessuno dei thread in coda dopo che è trascorso il timeout:

5

Un'altra opzione consiste nell'usare parallelTestTimeoutInSeconds .

In questo caso, verrà interrotta l'esecuzione solo dei thread in coda:

3.5

Tuttavia, con entrambe le opzioni, i test termineranno con un messaggio di errore quando il timeout sarà scaduto.

3.4. Avvertenze

Surefire chiama metodi statici annotati con @Parameters , @BeforeClass e @AfterClass nel thread padre. Quindi assicurati di controllare potenziali incongruenze di memoria o condizioni di competizione prima di eseguire i test in parallelo.

Inoltre, i test che modificano lo stato condiviso non sono sicuramente buoni candidati per l'esecuzione in parallelo.

4. Esecuzione del test in progetti Maven multimodulo

Fino ad ora, ci siamo concentrati sull'esecuzione di test in parallelo all'interno di un modulo Maven.

Ma diciamo di avere più moduli in un progetto Maven. Poiché questi moduli sono costruiti in sequenza, anche i test per ogni modulo vengono eseguiti in sequenza.

Possiamo cambiare questo comportamento predefinito usando il parametro -T di Maven che costruisce i moduli in parallelo . Questo può essere fatto in due modi.

Possiamo specificare il numero esatto di thread da utilizzare durante la creazione del progetto:

mvn -T 4 surefire:test

Oppure utilizza la versione portatile e specifica il numero di thread da creare per core della CPU:

mvn -T 1C surefire:test

In ogni caso, possiamo velocizzare i test così come i tempi di esecuzione della build.

5. Forking JVM

Con l'esecuzione del test parallelo tramite l' opzione parallela , la concorrenza avviene all'interno del processo JVM utilizzando i thread .

Poiché i thread condividono lo stesso spazio di memoria, ciò può essere efficiente in termini di memoria e velocità. Tuttavia, è possibile che si verifichino condizioni di gara impreviste o altri sottili errori di test correlati alla concorrenza. A quanto pare, condividere lo stesso spazio di memoria può essere sia una benedizione che una maledizione.

Per evitare problemi di concorrenza a livello di thread, Surefire fornisce un'altra modalità di esecuzione dei test paralleli: fork e concorrenza a livello di processo . L'idea dei processi biforcuti è in realtà abbastanza semplice. Invece di generare più thread e distribuire i metodi di test tra di loro, surefire crea nuovi processi e fa la stessa distribuzione.

Poiché non esiste memoria condivisa tra processi diversi, non soffriremo di quei sottili bug di concorrenza. Ovviamente, questo va a scapito di un maggiore utilizzo della memoria e un po 'meno velocità.

Ad ogni modo, per abilitare il fork, dobbiamo solo usare la proprietà forkCount e impostarla su un valore qualsiasi positivo:

3

Qui, surefire creerà al massimo tre fork dalla JVM ed eseguirà i test in essi. Il valore predefinito per forkCount è uno, il che significa che maven-surefire-plugin crea un nuovo processo JVM per eseguire tutti i test in un modulo Maven.

La proprietà forkCount supporta la stessa sintassi di -T . Cioè, se aggiungiamo la C al valore, quel valore verrà moltiplicato per il numero di core CPU disponibili nel nostro sistema. Per esempio:

2.5C

Quindi, in una macchina a due core, Surefire può creare al massimo cinque fork per l'esecuzione di test paralleli.

Per impostazione predefinita, Surefire riutilizzerà i fork creati per altri test . Tuttavia, se impostiamo la proprietà reuseForks su false , distruggerà ogni fork dopo aver eseguito una classe di test.

Inoltre, per disabilitare il fork, possiamo impostare il forkCount a zero.

6. Conclusione

Per riassumere, abbiamo iniziato abilitando il comportamento multi-thread e definendo il grado di parallelismo utilizzando il parametro parallel . Successivamente, abbiamo applicato limitazioni al numero di thread che Surefire dovrebbe creare. Successivamente, abbiamo impostato i parametri di timeout per controllare i tempi di esecuzione dei test.

Infine, abbiamo esaminato come ridurre i tempi di esecuzione della build e quindi i tempi di esecuzione dei test nei progetti Maven multi-modulo.

Come sempre, il codice qui presentato è disponibile su GitHub.