Test paralleli per PHPUnit con ParaTest

PHPUnit ha accennato al parallelismo dal 2007, ma, nel frattempo, i nostri test continuano a funzionare lentamente. Il tempo è denaro, giusto? ParaTest è uno strumento che si trova su PHPUnit e ti permette di eseguire test in parallelo senza l'uso di estensioni. Questo è un candidato ideale per i test funzionali (cioè il selenio) e altri processi di lunga durata.


ParaTest al tuo servizio

ParaTest è un robusto strumento a linea di comando per l'esecuzione di test PHPUnit in parallelo. Ispirato dalla brava gente di Sauce Labs, è stato originariamente sviluppato per essere una soluzione più completa per migliorare la velocità dei test funzionali.

Fin dalla sua nascita - e grazie ad alcuni brillanti contributori (tra cui Giorgio Sironi, il manutentore dell'estensione PHPUnit Selenium) - ParaTest è diventato un prezioso strumento per accelerare i test funzionali, oltre a test di integrazione che coinvolgono database, servizi Web e file system.

ParaTest ha anche l'onore di essere in bundle con Saus Labs, il framework di test di Sauce Labs, ed è stato utilizzato in quasi 7000 progetti, al momento della stesura di questo articolo.

Installazione di ParaTest

Attualmente, l'unico modo ufficiale per installare ParaTest è tramite Composer. Per quelli di voi che sono nuovi a Composer, abbiamo un grande articolo sull'argomento. Per scaricare l'ultima versione di sviluppo, includi quanto segue all'interno del tuo composer.json file:

 "require": "brianium / paratest": "dev-master"

In alternativa, per l'ultima versione stabile:

 "require": "brianium / paratest": "0.4.4"

Quindi, corri installazione di compositore dalla riga di comando. Il binario ParaTest verrà creato nel vendor / bin elenco.

L'interfaccia della riga di comando ParaTest

ParaTest include un'interfaccia a riga di comando che dovrebbe essere familiare alla maggior parte degli utenti di PHPUnit - con alcuni bonus aggiuntivi per i test paralleli.

Il tuo primo test parallelo

Usare ParaTest è semplice come PHPUnit. Per dimostrare rapidamente questo in azione, creare una directory, paratest-campione, con la seguente struttura:

Installiamo ParaTest come menzionato sopra. Supponendo di avere una shell Bash e un binario Composer installato a livello globale, è possibile farlo in una riga dal paratest-campione directory:

 echo '"require": "brianium / paratest": "0.4.4"'> compositore.json && compositore install

Per ciascuno dei file nella directory, creare una classe case test con lo stesso nome, in questo modo:

 class SlowOneTest estende PHPUnit_Framework_TestCase public function test_long_running_condition () sleep (5); $ This-> assertTrue (true); 

Prendi nota dell'uso di dormire (5) per simulare un test che richiederà cinque secondi per essere eseguito. Quindi dovremmo avere cinque casi di test che ciascuno impiega cinque secondi per essere eseguito. Utilizzando vanilla PHPUnit, questi test verranno eseguiti in serie e dureranno venticinque secondi, in totale. ParaTest eseguirà questi test contemporaneamente in cinque processi separati e dovrebbe richiedere solo cinque secondi, non venticinque!

Ora che abbiamo una comprensione di cosa è ParaTest, approfondiamo un po 'i problemi associati all'esecuzione di test PHPUnit in parallelo.


Il problema a portata di mano

Il test può essere un processo lento, soprattutto quando si inizia a parlare di colpire un database o di automatizzare un browser. Per testare più rapidamente ed efficacemente, dobbiamo essere in grado di eseguire i nostri test contemporaneamente (contemporaneamente), anziché in serie (uno dopo l'altro).

Il metodo generale per realizzare questo non è una nuova idea: eseguire diversi gruppi di test in più processi PHPUnit. Questo può essere facilmente realizzato usando la funzione nativa di PHP proc_open. Il seguente sarebbe un esempio di questo in azione:

 / ** * $ runningTests - processi attualmente aperti * $ loadedTests - una serie di percorsi di test * $ maxProcs - il numero totale di processi che vogliamo eseguire * / while (sizeof ($ runningTests) || sizeof ($ loadedTests)) while (sizeof ($ loadedTests) && sizeof ($ runningTests) < $maxProcs) $runningTests[] = proc_open("phpunit " . array_shift($loadedTests), $descriptorspec, $pipes); //log results and remove any processes that have finished… 

Poiché in PHP mancano i thread nativi, questo è un metodo tipico per ottenere un certo livello di concorrenza. Le sfide particolari degli strumenti di test che utilizzano questo metodo possono essere ridotte a tre problemi principali:

  • Come carichiamo i test?
  • Come possiamo aggregare e riportare i risultati dei diversi processi PHPUnit?
  • Come possiamo fornire coerenza con lo strumento originale (cioè PHPUnit)?

Diamo un'occhiata ad alcune tecniche che sono state impiegate in passato e poi rivediamo ParaTest e come si differenzia dal resto della folla.


Coloro che sono venuti prima

Come notato in precedenza, l'idea di eseguire PHPUnit in più processi non è nuova. La procedura tipica utilizzata è qualcosa che segue le seguenti linee:

  • Grep per i metodi di test o carica una directory di file contenenti test suite.
  • Aprire un processo per ogni metodo o suite di test.
  • Parse output dal tubo STDOUT.

Diamo un'occhiata a uno strumento che impiega questo metodo.

Ciao, Paraunit

Paraunit era l'originale parallelo runner in bundle con Sauce Labs 'Sausage tool, ed è servito come punto di partenza per ParaTest. Diamo un'occhiata a come affronta i tre problemi principali sopra menzionati.

Caricamento del test

Paraunit è stato progettato per facilitare i test funzionali. Esegue ogni metodo di test piuttosto che un'intera suite di test in un processo PHPUnit di sua proprietà. Dato il percorso verso una raccolta di test, Paraunit cerca i singoli metodi di test, attraverso la corrispondenza dei pattern con i contenuti dei file.

 preg_match_all ("/ function (test [^ \ (] +) \ (/", $ fileContents, $ matches);

I metodi di test caricati possono quindi essere eseguiti in questo modo:

 proc_open ("phpunit --filter = $ testName $ testFile", $ descriptorspec, $ pipes);

In un test in cui ogni metodo sta configurando e abbattendo un browser, questo può rendere le cose un po 'più veloci, se ognuno di questi metodi viene eseguito in un processo separato. Tuttavia, ci sono un paio di problemi con questo metodo.

Mentre i metodi iniziano con la parola "test,"è una convenzione forte tra gli utenti di PHPUnit, le annotazioni sono un'altra opzione: il metodo di caricamento utilizzato da Paraunit salta questo test perfettamente valido:

 / ** * @test * / public function twoTodosCheckedShowsCorrectClearButtonText () $ this-> todos-> addTodos (array ('one', 'two')); $ This-> todos-> getToggleAll () -> click (); $ this-> assertEquals ('Cancella 2 elementi completati', $ this-> todos-> getClearButton () -> text ()); 

Oltre a non supportare le annotazioni di test, anche l'ereditarietà è limitata. Potremmo argomentare sul merito di fare qualcosa del genere, ma consideriamo la seguente configurazione:

 classe astratta TodoTest estende PHPUnit_Extensions_Selenium2TestCase protected $ browser = null; funzione pubblica setUp () // configure browser public function testTypingIntoFieldAndHittingEnterAddsTodo () // selenio magico / ** * ChromeTodoTest.php * Nessun metodo di prova da leggere! * / class ChromeTodoTest estende TodoTest protected $ browser = 'chrome';  / ** * FirefoxTodoTest.php * Nessun metodo di prova da leggere! * / class FirefoxTodoTest estende TodoTest protected $ browser = 'firefox'; 

I metodi ereditati non sono nel file, quindi non verranno mai caricati.

Visualizzazione dei risultati

Paraunit aggrega i risultati di ogni processo analizzando l'output generato da ciascun processo. Questo metodo consente a Paraunit di catturare l'intera gamma di codici brevi e feedback presentati da PHPUnit.

Lo svantaggio di aggregare i risultati in questo modo è che è piuttosto ingombrante e facile da rompere. Ci sono molti risultati diversi da tenere in considerazione e molte espressioni regolari sul lavoro per mostrare risultati significativi in ​​questo modo.

Coerenza con PHPUnit

A causa del grepping dei file, Paraunit è abbastanza limitato in quali funzionalità di PHPUnit è in grado di supportare. È uno strumento eccellente per eseguire una semplice struttura di test funzionali, ma, oltre ad alcune delle difficoltà menzionate, manca del supporto per alcune utili funzionalità di PHPUnit. Alcuni esempi includono le suite di test, che specificano i file di configurazione e di bootstrap, i risultati di registrazione e l'esecuzione di gruppi di test specifici.

Molti degli strumenti esistenti seguono questo schema. Grep una directory di file di test ed esegui l'intero file in un nuovo processo o in ogni metodo, mai entrambi.


ParaTest At Bat

L'obiettivo di ParaTest è supportare i test paralleli per una varietà di scenari. Creato originariamente per colmare le lacune di Paraunit, è diventato un solido strumento a riga di comando per l'esecuzione parallela di entrambe le suite di test e dei metodi di test. Questo rende ParaTest un candidato ideale per test di lunga durata di diverse forme e dimensioni.

Come ParaTest gestisce i test paralleli

ParaTest si discosta dalla norma stabilita per supportare più di PHPUnit e agisce come un candidato realmente valido per i test paralleli.

Caricamento del test

ParaTest carica i test in modo simile a PHPUnit. Carica tutti i test in una directory specificata che termina con * Test.php suffisso o caricherà i test in base al file di configurazione XML PHPUnit standard. Il caricamento è effettuato, tramite la riflessione, quindi è facile da supportare @test metodi, ereditarietà, test suite e metodi di test individuali. Reflection rende l'aggiunta di supporto per altre annotazioni in un attimo.

Poiché reflection consente a ParaTest di acquisire classi e metodi, può eseguire sia test suite che metodi di test in parallelo, rendendolo uno strumento più versatile.

ParaTest impone alcuni vincoli, ma quelli ben fondati nella comunità PHP. I test devono seguire lo standard PSR-0 e il suffisso del file predefinito di * Test.php non è configurabile, come in PHPUnit. È in corso una succursale per supportare la stessa configurazione di suffisso consentita in PHPUnit.

Visualizzazione dei risultati

ParaTest si discosta anche dal percorso di analisi dei tubi STDOUT. Anziché analizzare i flussi di output, ParaTest registra i risultati di ciascun processo PHPUnit nel formato JUnit e aggrega i risultati di questi registri. È molto più semplice leggere i risultati dei test da un formato stabilito rispetto a un flusso di output.

        

L'analisi dei registri JUnit presenta alcuni inconvenienti minori. I test ignorati e ignorati non vengono riportati nel feedback immediato, ma si rifletteranno sui valori totali visualizzati dopo un'esecuzione di test.

Coerenza con PHPUnit

Reflection consente a ParaTest di supportare più convenzioni PHPUnit. La console ParaTest supporta più funzionalità PHPUnit out-of-box rispetto a qualsiasi altro strumento simile, come la possibilità di eseguire gruppi, fornire file di configurazione e bootstrap e registrare i risultati nel formato JUnit.


Esempi di ParaTest

ParaTest può essere utilizzato per guadagnare velocità in diversi scenari di test.

Test funzionali con selenio

ParaTest eccelle nei test funzionali. Supporta un -f passare nella sua console per abilitare la modalità funzionale. La modalità funzionale istruisce ParaTest ad eseguire ogni metodo di prova in un processo separato, invece del valore predefinito, che consiste nell'eseguire ciascuna suite di test in un processo separato.

Spesso accade che ogni metodo di test funzionale faccia un sacco di lavoro, come aprire un browser, navigare intorno alla pagina e chiudere il browser.

Il progetto di esempio, paratest-selenium, dimostra il test di un'applicazione Backbone.js con Selenium e ParaTest. Ogni metodo di test apre un browser e verifica una funzionalità specifica:

 funzione pubblica setUp () $ this-> setBrowserUrl ('http://backbonejs.org/examples/todos/'); $ this-> todos = new Todos ($ this-> prepareSession ());  public function testTypingIntoFieldAndHittingEnterAddsTodo () $ this-> todos-> addTodo ("parallelizza i test phpunit \ n"); $ this-> assertEquals (1, sizeof ($ this-> todos-> getItems ()));  public function testClickingTodoCheckboxMarksTodoDone () $ this-> todos-> addTodo ("assicurati di poter completare i todos"); $ articoli = $ this-> todos-> getItems (); $ item = array_shift ($ items); $ This-> todos-> getItemCheckbox ($ item) -> clicca (); $ this-> assertEquals ('done', $ item-> attribute ('class'));  // ... altri test

Questo test case potrebbe prendere un secondo caldo se fosse eseguito in serie, tramite vanilla PHPUnit. Perché non eseguire più metodi contemporaneamente?

Gestire le condizioni della gara

Come per qualsiasi test parallelo, dobbiamo essere consapevoli degli scenari che presenteranno condizioni di gara, come i processi che tentano di accedere a un database. La sezione dev-master di ParaTest offre una funzione di test token molto utile, scritta dal collaboratore Dimitris Baltas (dbaltas su Github), che semplifica notevolmente i database di test di integrazione.

Dimitris ha incluso un utile esempio che dimostra questa caratteristica su Github. Nelle parole di Dimitris:

TEST_TOKEN tenta di affrontare il problema delle risorse comuni in un modo molto semplice: clonare le risorse per garantire che nessun processo concorrente acceda alla stessa risorsa.

UN TEST_TOKEN la variabile di ambiente viene fornita per i test da consumare e viene riciclata al termine del processo. Può essere usato per modificare condizionalmente i test, in questo modo:

 funzione pubblica setUp () parent :: setUp (); $ this -> _ filename = sprintf ('out% s.txt', getenv ('TEST_TOKEN')); 

ParaTest e Sauce Labs

Sauce Labs è l'Excalibur dei test funzionali. Sauce Labs fornisce un servizio che ti consente di testare facilmente le tue applicazioni in una varietà di browser e piattaforme. Se non li hai mai controllati prima, ti incoraggio vivamente a farlo.

Provare con Sauce potrebbe essere un tutorial in sé, ma quei maghi hanno già fatto un ottimo lavoro nel fornire tutorial per l'utilizzo di PHP e ParaTest per scrivere test funzionali usando il loro servizio.


Il futuro di ParaTest

ParaTest è un ottimo strumento per riempire alcune lacune di PHPUnit, ma, in definitiva, è solo una spina nella diga. Uno scenario molto migliore sarebbe il supporto nativo in PHPUnit!

Nel frattempo, ParaTest continuerà ad aumentare il supporto per un maggior numero di comportamenti nativi di PHPUnit. Continuerà a offrire funzionalità utili ai test paralleli, in particolare nei reami funzionali e di integrazione.

ParaTest ha molte grandi cose in cantiere per rafforzare la trasparenza tra PHPUnit e se stesso, principalmente in quali opzioni di configurazione sono supportate.

L'ultima versione stabile di ParaTest (v0.4.4) supporta comodamente Mac, Linux e Windows, ma ci sono alcune preziose richieste di pull e funzionalità in dev-master che sicuramente soddisfa le folle Mac e Linux. Questa sarà una conversazione interessante che andrà avanti.

Lettura e risorse aggiuntive

Ci sono una manciata di articoli e risorse in tutto il web che presentano ParaTest. Date loro una lettura, se siete interessati:

  • ParaTest su Github
  • Parallel PHPUnit di ParaTest contributor e PHPUnit Selenium extension maintainer Giorgio Sironi
  • Contribuire a Paratest. Un eccellente articolo su WrapperRunner di Giorgio's sperimentale per ParaTest
  • Giorgio's WrapperRunner Source Code
  • tripsta / paratest-campione. Un esempio della funzione TEST_TOKEN del suo creatore Dimitris Baltas
  • brianium / paratest-selenio. Un esempio di utilizzo di ParaTest per scrivere test funzionali