Parallel testen voor PHPUnit met ParaTest

PHPUnit heeft laten doorschemeren bij parallellisme sinds 2007, maar in de tussentijd blijven onze tests langzaam lopen. Tijd is geld, toch? ParaTest is een tool die bovenop PHPUnit staat en waarmee u tests parallel kunt uitvoeren zonder het gebruik van extensies. Dit is een ideale kandidaat voor functionele (d.w.z. Selenium) tests en andere langlopende processen.


ParaTest tot uw dienst

ParaTest is een robuuste opdrachtregelhulpprogramma voor het parallel uitvoeren van PHPUnit-tests. Geïnspireerd door de fijne mensen van Sauce Labs, werd het oorspronkelijk ontwikkeld als een completere oplossing voor het verbeteren van de snelheid van functionele tests.

Sinds haar oprichting - en dankzij een aantal briljante bijdragers (waaronder Giorgio Sironi, de beheerder van de PHPUnit Selenium-extensie) - is ParaTest een waardevol hulpmiddel geworden voor het versnellen van functionele tests, evenals integratietests met databases, webservices en bestandssystemen.

ParaTest heeft ook de eer om gebundeld te worden met Sauce Labs 'testraamwerk Sausage, en is gebruikt in bijna 7000 projecten, op het moment van schrijven.

ParaTest installeren

Momenteel is de enige officiële manier om ParaTest te installeren via Composer. Voor degenen onder u die nieuw zijn voor Composer, hebben we een geweldig artikel over dit onderwerp. Als u de nieuwste ontwikkelversie wilt ophalen, neemt u het volgende op in uw composer.json het dossier:

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

Als alternatief voor de nieuwste stabiele versie:

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

Voer vervolgens uit componist installeren vanaf de opdrachtregel. Het binaire bestand van ParaTest wordt gemaakt in de vendor / bin directory.

De ParaTest-opdrachtregelinterface

ParaTest bevat een opdrachtregelinterface die bekend moet zijn bij de meeste PHPUnit-gebruikers - met enkele toegevoegde bonussen voor parallelle tests.

Uw eerste parallelle test

Het gebruik van ParaTest is net zo eenvoudig als PHPUnit. Om snel dit in actie aan te tonen, maakt u een map, paratest-sample, met de volgende structuur:

Laten we ParaTest installeren zoals hierboven vermeld. Ervan uitgaande dat u een Bash-shell en een wereldwijd geïnstalleerde Composer-binary hebt, kunt u dit op één regel uit de paratest-sample directory:

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

Maak voor elk van de bestanden in de directory een testcase-klasse met dezelfde naam, zoals:

 klasse SlowOneTest breidt PHPUnit_Framework_TestCase uit public function test_long_running_condition () sleep (5); $ This-> assertTrue (true); 

Let op het gebruik van slapen (5) om een ​​test te simuleren die vijf seconden nodig heeft om uit te voeren. We zouden dus vijf testgevallen moeten hebben die elk vijf seconden in beslag nemen. Met behulp van vanilla PHPUnit zullen deze tests in serie worden uitgevoerd en in totaal vijfentwintig seconden duren. ParaTest voert deze tests gelijktijdig uit in vijf afzonderlijke processen en duurt slechts vijf seconden, niet vijfentwintig!

Nu we begrijpen wat ParaTest is, gaan we dieper in op de problemen die samenhangen met PHPUnit-tests parallel uitvoeren.


Het probleem bij de hand

Testen kan een langzaam proces zijn, vooral wanneer we beginnen te praten over het raken van een database of het automatiseren van een browser. Om sneller en efficiënter te kunnen testen, moeten we onze tests tegelijkertijd (op hetzelfde moment) kunnen uitvoeren, in plaats van serieel (de een na de ander).

De algemene methode om dit te bereiken is geen nieuw idee: voer verschillende testgroepen uit in meerdere PHPUnit-processen. Dit kan eenvoudig worden bereikt met behulp van de native PHP-functie proc_open. Het volgende zou hiervan een voorbeeld in actie zijn:

 / ** * $ runningTests - open processen * $ loadedTests - een reeks testpaden * $ maxProcs - het totale aantal processen dat we willen uitvoeren * / 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… 

Omdat PHP native threads mist, is dit een standaardmethode om een ​​bepaald niveau van concurrency te bereiken. De specifieke uitdagingen van testtools die deze methode gebruiken, zijn te wijten aan drie kernproblemen:

  • Hoe laden we tests?
  • Hoe aggregeren en rapporteren we resultaten van de verschillende PHPUnit-processen?
  • Hoe kunnen we consistentie bieden met het originele hulpmiddel (d.w.z. PHPUnit)?

Laten we een paar technieken bekijken die in het verleden zijn toegepast, en dan ParaTest bekijken en hoe het verschilt van de rest van de menigte.


Degenen die eerder kwamen

Zoals eerder opgemerkt, is het idee om PHPUnit in meerdere processen uit te voeren niet nieuw. De typische procedure die wordt toegepast, is iets in de volgende zin:

  • Grep voor testmethoden of laad een map met bestanden die testsuites bevatten.
  • Open een proces voor elke testmethode of suite.
  • Ontleed de uitvoer van de STDOUT-pijp.

Laten we eens kijken naar een tool die deze methode gebruikt.

Hallo, Paraunit

Paraunit was de originele parallelle loper gebundeld met Sauce Labs 'Sausage-tool en diende als startpunt voor ParaTest. Laten we eens kijken naar hoe het de drie belangrijkste problemen behandelt die hierboven zijn genoemd.

Test laden

Paraunit is ontworpen om functioneel testen te vergemakkelijken. Het voert elke testmethode uit in plaats van een volledige testsuite in een eigen PHPUnit-proces. Gezien het pad naar een verzameling testen, zoekt Paraunit naar individuele testmethoden, via patroonvergelijking met bestandsinhoud.

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

Geladen testmethoden kunnen dan als volgt worden uitgevoerd:

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

In een test waarbij elke methode een browser opzet en afbreekt, kan dit dingen een stuk sneller maken, als elk van die methoden in een afzonderlijk proces wordt uitgevoerd. Er zijn echter een aantal problemen met deze methode.

Terwijl methoden die met het woord beginnen, "test,"is een sterke overeenkomst tussen PHPUnit-gebruikers, annotaties zijn een andere optie. De laadmethode die door Paraunit wordt gebruikt, zou deze perfect geldige test overslaan:

 / ** * @test * / public function twoTodoscheckedShowsCorrectClearButtonText () $ this-> todos-> addTodos (array ('one', 'two')); $ This-> todos-> getToggleAll () -> klik (); $ this-> assertEquals ('Wis 2 voltooide items', $ this-> todos-> getClearButton () -> text ()); 

Naast het niet ondersteunen van testannotaties, is de erfenis ook beperkt. We kunnen de voordelen van het doen van iets als dit beargumenteren, maar laten we de volgende opstelling overwegen:

 abstracte klasse TodoTest breidt uit PHPUnit_Extensions_Selenium2TestCase protected $ browser = null; openbare functie setUp () // configureer browser public function testTypingIntoFieldAndHittingEnterAddsTodo () // selenium magic / ** * ChromeTodoTest.php * Geen testmethoden om te lezen! * / class ChromeTodoTest breidt TodoTest uit protected $ browser = 'chrome';  / ** * FirefoxTodoTest.php * Geen testmethoden om te lezen! * / class FirefoxTodoTest breidt TodoTest uit protected $ browser = 'firefox'; 

De overgenomen methoden staan ​​niet in het bestand, dus ze zullen nooit worden geladen.

Resultaten weergeven

Paraunit verzamelt de resultaten van elk proces door de uitvoer van elk proces te ontleden. Met deze methode kan Paraunit het volledige scala aan korte codes en feedback van PHPUnit vastleggen.

Het nadeel van het samenvoegen van resultaten op deze manier is dat het vrij onpraktisch en gemakkelijk te breken is. Er zijn veel verschillende uitkomsten om rekening mee te houden en veel reguliere expressies op het werk om op deze manier betekenisvolle resultaten weer te geven.

Consistentie met PHPUnit

Vanwege de grepping van bestanden is Paraunit vrij beperkt in wat PHPUnit-functies kunnen ondersteunen. Het is een uitstekende tool voor het uitvoeren van een eenvoudige structuur van functionele tests, maar in aanvulling op enkele van de genoemde problemen, mist het ondersteuning voor enkele nuttige PHPUnit-functies. Enkele voorbeelden hiervan zijn testsuites, configuratie- en bootstrap-bestanden opgeven, resultaten vastleggen en specifieke testgroepen uitvoeren.

Veel van de bestaande tools volgen dit patroon. Grep een map met testbestanden en voer het hele bestand uit in een nieuw proces of elke methode - nooit beide.


ParaTest At Bat

Het doel van ParaTest is parallelle tests te ondersteunen voor verschillende scenario's. Oorspronkelijk gemaakt om de hiaten in Paraunit te vullen, is het een krachtige tool voor de commandoregel geworden om zowel testsuites als testmethoden parallel uit te voeren. Dit maakt ParaTest een ideale kandidaat voor langdurige tests van verschillende vormen en afmetingen.

Hoe ParaTest parallelle tests verwerkt

ParaTest wijkt af van de vastgestelde norm om meer van PHPUnit te ondersteunen en fungeert als een echt levensvatbare kandidaat voor parallelle tests.

Test laden

ParaTest laadt tests op dezelfde manier als PHPUnit. Het laadt alle tests in een opgegeven map die eindigen op de * test.php achtervoegsel of laadt tests op basis van het standaard PHPUnit XML-configuratiebestand. Het laden gebeurt via reflectie, dus het is gemakkelijk te ondersteunen @test methoden, overerving, testsuites en individuele testmethoden. Reflectie maakt het toevoegen van ondersteuning voor andere annotaties in een handomdraai.

Omdat reflectie ParaTest in staat stelt klassen en methoden te grijpen, kan het zowel testsuites als testmethoden parallel uitvoeren, waardoor het een veelzijdiger hulpmiddel is.

ParaTest legt wel enkele beperkingen op, maar wel gefundeerde in de PHP-gemeenschap. Tests moeten wel voldoen aan de PSR-0 standaard en het standaard bestands achtervoegsel van * test.php is niet configureerbaar, zoals het is in PHPUnit. Er is een lopende tak actief om dezelfde suffixconfiguratie te ondersteunen die is toegestaan ​​in PHPUnit.

Resultaten weergeven

ParaTest wijkt ook af van het pad van het ontleden van STDOUT-buizen. In plaats van uitvoerstromen te parsen, logt ParaTest de resultaten van elk PHPUnit-proces in de JUnit-indeling in en verzamelt de resultaten van deze logboeken. Het is veel eenvoudiger om testresultaten van een vastgesteld formaat te lezen dan een uitvoerstroom.

        

Het parseren van JUnit-logboeken heeft enkele kleine nadelen. Overgeslagen en genegeerde tests worden niet gerapporteerd in de directe feedback, maar ze worden wel weerspiegeld in de opgetelde waarden die na een testrun worden weergegeven.

Consistentie met PHPUnit

Reflectie stelt ParaTest in staat om meer PHPUnit-conventies te ondersteunen. De ParaTest-console ondersteunt meer PHPUnit-functies uit de doos dan enig ander vergelijkbaar hulpmiddel, zoals de mogelijkheid om groepen uit te voeren, configuratie- en bootstrap-bestanden aan te leveren en resultaten in het JUnit-formaat te loggen.


ParaTest-voorbeelden

ParaTest kan worden gebruikt om snelheid te winnen in verschillende testscenario's.

Functioneel testen met selenium

ParaTest blinkt uit in functioneel testen. Het ondersteunt a -f schakel in de console om de functionele modus in te schakelen. Functionele modus instrueert ParaTest om elke testmethode in een afzonderlijk proces uit te voeren, in plaats van de standaardmethode, waarmee elke testreeks in een afzonderlijk proces wordt uitgevoerd.

Het is vaak zo dat elke functionele testmethode veel werk doet, zoals het openen van een browser, het navigeren door de pagina en het sluiten van de browser.

Het voorbeeldproject, paratest-selenium, demonstreert het testen van een Backbone.js-todo-toepassing met Selenium en ParaTest. Elke testmethode opent een browser en test een specifieke functie:

 openbare functie setUp () $ this-> setBrowserUrl ('http://backbonejs.org/examples/todos/'); $ this-> todos = new Todos ($ this-> prepareSession ());  public function testTypingIntoFieldAndHittingEnterAddsTodo () $ this-> todos-> addTodo ("parallelize phpunit tests \ n"); $ this-> assertEquals (1, sizeof ($ this-> todos-> getItems ()));  public function testClickingTodoCheckboxMarksTodoDone () $ this-> todos-> addTodo ("zorg ervoor dat u de todos kunt voltooien"); $ items = $ this-> todos-> getItems (); $ item = array_shift ($ items); $ This-> todos-> getItemCheckbox ($ item) -> klik (); $ this-> assertEquals ('done', $ item-> attribute ('class'));  // ... meer tests

Deze testcase kan een hete seconde duren als het serieel zou lopen, via vanilla PHPUnit. Waarom niet meerdere methoden tegelijkertijd gebruiken?

Omgaan met raceomstandigheden

Net als bij elke parallelle test, moeten we rekening houden met scenario's die de raceomstandigheden weergeven - zoals meerdere processen die proberen toegang te krijgen tot een database. De dev-master tak van ParaTest heeft een echt handige test token functie, geschreven door medewerker Dimitris Baltas (dbaltas op Github), die het testen van integratietests veel gemakkelijker maakt.

Dimitris heeft een handig voorbeeld toegevoegd dat deze functie op Github demonstreert. In de woorden van Dimitris:

TEST_TOKEN pogingen om het probleem van algemene hulpbronnen op een zeer eenvoudige manier aan te pakken: de bronnen klonen om ervoor te zorgen dat geen gelijktijdige processen toegang tot dezelfde bron hebben.

EEN TEST_TOKEN omgevingsvariabele wordt verstrekt voor tests die moeten worden geconsumeerd en wordt gerecycled wanneer het proces is voltooid. Het kan worden gebruikt om uw tests voorwaardelijk te wijzigen, zoals:

 openbare functie setUp () parent :: setUp (); $ this -> _ bestandsnaam = sprintf ('uit% s.txt', getenv ('TEST_TOKEN')); 

ParaTest en Sauce Labs

Sauce Labs is de Excalibur van functioneel testen. Sauce Labs biedt een service waarmee u eenvoudig uw toepassingen in verschillende browsers en platforms kunt testen. Als u ze nog niet eerder hebt gecontroleerd, raad ik u ten zeerste aan dit te doen.

Testen met saus kan op zichzelf een zelfstudie zijn, maar die wizards hebben het al goed gedaan met tutorials voor het gebruik van PHP en ParaTest om functionele tests te schrijven met behulp van hun service.


De toekomst van ParaTest

ParaTest is een geweldig hulpmiddel om enkele gaten in PHPUnit in te vullen, maar uiteindelijk is het gewoon een plug in de dam. Een veel beter scenario zou native support in PHPUnit zijn!

In de tussentijd zal ParaTest doorgaan met het vergroten van de ondersteuning voor meer van het native gedrag van PHPUnit. Het blijft functies bieden die nuttig zijn voor parallelle tests, met name in de functionele en integratiesystemen.

ParaTest heeft veel geweldige dingen in de maak om de transparantie tussen PHPUnit en zichzelf te vergroten, vooral in welke configuratie-opties worden ondersteund.

De nieuwste stabiele versie van ParaTest (v0.4.4) ondersteunt op comfortabele wijze Mac, Linux en Windows, maar er zijn enkele waardevolle pull-verzoeken en -functies in dev-meester die zeker tegemoetkomen aan de Mac- en Linux-drukte. Dus dat zal een interessant gesprek zijn dat vooruitgaat.

Aanvullende lectuur en bronnen

Er zijn een handvol artikelen en bronnen op internet die ParaTest bevatten. Geef ze een bericht, als je geïnteresseerd bent:

  • ParaTest op Github
  • Parallelle PHPUnit door ParaTest-bijdrager en PHPUnit Selenium-extensiebehouder Giorgio Sironi
  • Bijdragen aan Paratest. Een uitstekend artikel over Giorgio's experimentele WrapperRunner voor ParaTest
  • Giorgio's WrapperRunner-broncode
  • Tripsta / paratest-monster. Een voorbeeld van de TEST_TOKEN-functie van zijn maker Dimitris Baltas
  • brianium / paratest-selenium. Een voorbeeld van het gebruik van ParaTest om functionele tests te schrijven