Velocizzare i test in progetti Symfony con Paratest

paratest 2

I test sono fondamentali. I test fanno emergere il design. I test ti rendono coraggioso. I test sono la documentazione. I test rendono sicuro il refactoring. I test sono… lenti.

Nel mondo dello sviluppo web, dove spesso la parte di logica è inferiore alla parte di rappresentazione, la percentuale dei test funzionali o di integrazione è superiore alla percentuale dei test unitari. Mentre i test unitari sono eccezionalmente veloci, gli altri sono spesso lenti dovendo accedere a risorse come i database, filesystem o servizi di rete. Se consideriamo i test con browser per verificare comportamenti Javascript, tocchiamo l’apice della lentezza. Anche utilizzando mock per i servizi e isolando le porzioni di codice particolarmente inefficienti, la durata totale di build corpose rimane molto spesso troppo elevata per un sereno ciclo di sviluppo.

Colli di bottiglia

Quando lanciamo i test che utilizzano quasi esclusivamente i processori, come i test unitari, si ha l’utilizzo di un solo core alla volta. Se questi test sono lanciati in sequenza, anche se veloci, sprecano un altissimo numero di risorse di calcolo.

Un problema diverso, ma ancora più evidente vista la natura dei test, si ha con i test che utilizzano i browser. Il problema di questi test è che la loro velocità non è determinata principalmente dall’esecuzione del codice quanto dal tempo di caricamento delle risorse necessarie per il funzionamento della pagina: css, javascript, immagini, etc. Il problema è amplificato nel caso di risorse non locali (CDN per framework Javascript, sistemi di sharing, font, etc). Il passaggio dai ‘veri browser’ come Chrome o Firefox a browser headless come PhantomJs porta da subito ad un miglioramento in termini di velocità di esecuzione ma i tempi di attesa delle risorse (tempi morti) incidono ancora molto sull’esecuzione dei test.

I computer di uso comune hanno processori che permetto di eseguire diverse pipeline di operazioni contemporaneamente. “Ma io lancio test che fanno uso contemporaneo di webserver/php/db/filesystem/altro e sfrutto tutta la potenza del mio computer!”. Caro amico non è così: la ‘contemporaneità’ in molti casi è finta: il test php chiede al webserver una risorsa che chiede a php di eseguire uno script che viene letto dal filesystem e poi eseguito, ad un certo punto chiede al db dei dati che rielabora e… tutte azioni in sequenza! E’ vero che si utilizzano più processi ma la le operazioni vengono fatte quasi totalmente in sequenza.

PHPUnit non permette di lanciare test in parallelo ma esistono tool che ti permettono di farlo, uno di questi è Paratest.

Paratest in due frasi

Paratest legge la configurazione di PHPUnit, prende le informazioni principali, colleziona i test da eseguire e poi li esegue in parallelo. Ogni collezione di test scrive il suo report che Paratest unirà agli altri per avere i risultati aggregati.

Installazione e configurazione in un progetto Symfony

Partiamo da un caso reale, progetto già avviato, con test già scritti. Il problema più comune che ci troviamo a risolvere è quello che i test non sono stati scritti per essere lanciati in concorrenza. Quindi andremo a creare tanti ambienti di test quanti processi vogliamo lanciare.

Iniziamo con l’installazione della libreria richiesta, è sufficiente aggiungere in composer.json il require

e lanciare

Ora passiamo alla configurazione degli ambienti di test. Partendo dal presupposto che i test girano correttamente e che il file di configurazione app/config/config_test.yml è corretto, creiamo tanti file config_testX.yml con X che va da 1 a quanti processi volete far girare. Compiliamo i file config_testX.yml con:

Modificate il vostro app/phpunit.xml.dist (o il file di configurazione phpunit che utilizzate) sostituendo nei tag directory le voci con gli asterischi con le relative directory. Es. sostituiamo

con

Aggiungente in app/AppKernel.php, nel metodo registerBundles gli environment di test (test1, test2, …) per il caricamento dei bundle di sviluppo:

Se non l’avete già, create una classe di testcase base per i vostri test e aggiungete/modificate i seguenti metodi

In questo modo potremo utilizzare sia Paratest con gli X environment dedicati, sia PHPUnit “classico” con l’ambiente di test di default.
Create tanti db quante sono le vostre nuove configurazioni di test:

Ora potete lanciare (con X = 4)

Se paratest non lo trovate nella directory bin del progetto, potete fare voi un link simbolico con  vendor/brianium/paratest/bin/paratest

Il miglioramento dei tempi di esecuzione varia in funzione del numero di processi con cui si lancia paratest e dal numero core disponibili sulla macchina.

Per una build estremamente lenta e con molti test con PhantomJs, siamo passati da 45 minuti a 15 utilizzando 4 processi su una macchina con i7.

Se utilizzate Travis, verificate il numero di core messi a disposizione dalla macchina virtuale. Ad oggi sono 1,5 quindi potreste avere un calo di performance superando i 3-4 processi di paratest.

Approcci differenti di parallelizzazione: isolamento vs concorrenza

Negli esempi di codice sopra riportati si è preso in considerazione un caso d’uso su un progetto già avviato, con test scritti per non essere eseguiti sullo stesso set di dati contemporaneamente. Tutti questi test partono dal presupposto di avere un prefissato set di dati. Per questo motivo abbiamo isolato i database per ogni “sequenza” di test.

Nel caso di progetti ex-novo si potrebbe pensare di scrivere test che verifichino le funzionalità senza tenere conto dei cambiamenti al database che non siano strettamente legati al singolo test eseguito. Se scriviamo un test per la creazione di una entità, dovremmo verificare direttamente l’esistenza dell’oggetto sul db. Un errore sarebbe quello di controllare il numero totale di entità sul db o contare la variazione del numero di righe su una pagina html perché qualche altro test potrebbe aver eliminato o creato altre entità nello stesso lasso di tempo. Isolando i test seguendo queste osservazioni, possiamo raggiungere un altro obiettivo: testare l’applicazione su reali connessioni concorrenti. Sicuramente avremo come pro quello di caricare poche volte (o una sola) le fixture e avremo il test sulla concorrenza, come contro avremo test leggermente più difficili da scrivere.