German (Deutsch) translation by Tatsiana Bochkareva (you can also view the original English article)
PHPUnit deutet seit 2007 auf Parallelität hin, aber in der Zwischenzeit laufen unsere Tests weiterhin langsam. Zeit ist Geld, richtig? ParaTest ist ein Tool, das auf PHPUnit basiert und es Ihnen ermöglicht, Tests ohne Verwendung von Erweiterungen parallel auszuführen. Dies ist ein idealer Kandidat für Funktionstests (d.h. Selenium) und andere lang laufende Prozesse.
ParaTest zu Ihren Diensten
ParaTest ist ein robustes Befehlszeilentool zum parallelen Ausführen von PHPUnit-Tests. Inspiriert von den feinen Leuten von Sauce Labs, wurde es ursprünglich als vollständigere Lösung zur Verbesserung der Geschwindigkeit von Funktionstests entwickelt.
Seit seiner Gründung - und dank einiger brillanter Mitarbeiter (einschließlich Giorgio Sironi, dem Betreuer der PHPUnit Selenium-Erweiterung) - ist ParaTest ein wertvolles Werkzeug zur Beschleunigung von Funktionstests sowie von Integrationstests mit Datenbanken, Webdiensten und Dateisystemen .
ParaTest hat auch die Ehre, mit dem Test-Framework Sausage von Sauce Labs gebündelt zu werden, und wurde zum Zeitpunkt dieses Schreibens in fast 7000 Projekten verwendet.
ParaTest installieren
Derzeit ist die einzige offizielle Möglichkeit, ParaTest zu installieren, Composer. Für diejenigen unter Ihnen, die Composer noch nicht kennen, haben wir einen großartigen Artikel zu diesem Thema. Fügen Sie zum Abrufen der neuesten Entwicklungsversion Folgendes in Ihre Datei composer.json ein:
1 |
"require": { |
2 |
"brianium/paratest": "dev-master" |
3 |
}
|
Alternativ für die neueste stabile Version:
1 |
"require": { |
2 |
"brianium/paratest": "0.4.4" |
3 |
}
|
Führen Sie als Nächstes die composer install über die Befehlszeile aus. Die ParaTest-Binärdatei wird im Verzeichnis vendor/bin erstellt.
Die ParaTest-Befehlszeilenschnittstelle
ParaTest enthält eine Befehlszeilenschnittstelle, die den meisten PHPUnit-Benutzern vertraut sein sollte - mit einigen zusätzlichen Boni für parallele Tests.



Ihr erster paralleler Test
Die Verwendung von ParaTest ist genauso einfach wie PHPUnit. Um dies schnell in Aktion zu demonstrieren, erstellen Sie ein Verzeichnis, paratest-sample, mit der folgenden Struktur:



Lassen Sie uns ParaTest wie oben erwähnt installieren. Angenommen, Sie haben eine Bash-Shell und eine global installierte Composer-Binärdatei, können Sie dies in einer Zeile aus dem paratest-sample-Verzeichnis ausführen:
1 |
echo '{"require": { "brianium/paratest": "0.4.4" }}' > composer.json && composer install |
Erstellen Sie für jede Datei im Verzeichnis eine Testfallklasse mit demselben Namen wie folgt:
1 |
class SlowOneTest extends PHPUnit_Framework_TestCase |
2 |
{
|
3 |
public function test_long_running_condition() |
4 |
{
|
5 |
sleep(5); |
6 |
$this->assertTrue(true); |
7 |
}
|
8 |
}
|
Beachten Sie die Verwendung von sleep(5), um einen Test zu simulieren, dessen Ausführung fünf Sekunden dauert. Wir sollten also fünf Testfälle haben, deren Ausführung jeweils fünf Sekunden dauert. Mit Vanilla PHPUnit werden diese Tests seriell ausgeführt und dauern insgesamt 25 Sekunden. ParaTest führt diese Tests gleichzeitig in fünf separaten Prozessen aus und sollte nur fünf Sekunden dauern, nicht fünfundzwanzig!



Nachdem wir nun verstanden haben, was ParaTest ist, wollen wir uns etwas eingehender mit den Problemen befassen, die mit der parallelen Ausführung von PHPUnit-Tests verbunden sind.
Das Problem zur Hand
Das Testen kann ein langsamer Prozess sein, insbesondere wenn wir über das Aufrufen einer Datenbank oder das Automatisieren eines Browsers sprechen. Um schneller und effizienter testen zu können, müssen wir in der Lage sein, unsere Tests gleichzeitig (gleichzeitig) und nicht seriell (nacheinander) auszuführen.
Die allgemeine Methode, um dies zu erreichen, ist keine neue Idee: Führen Sie verschiedene Testgruppen in mehreren PHPUnit-Prozessen aus. Dies kann leicht mit der nativen PHP-Funktion proc_open erreicht werden. Das Folgende wäre ein Beispiel dafür in Aktion:
1 |
/**
|
2 |
* $runningTests - currently open processes
|
3 |
* $loadedTests - an array of test paths
|
4 |
* $maxProcs - the total number of processes we want running
|
5 |
*/
|
6 |
while(sizeof($runningTests) || sizeof($loadedTests)) { |
7 |
while(sizeof($loadedTests) && sizeof($runningTests) < $maxProcs) |
8 |
$runningTests[] = proc_open("phpunit " . array_shift($loadedTests), $descriptorspec, $pipes); |
9 |
//log results and remove any processes that have finished ....
|
10 |
}
|
Da in PHP keine nativen Threads vorhanden sind, ist dies eine typische Methode, um ein gewisses Maß an Parallelität zu erreichen. Die besonderen Herausforderungen beim Testen von Tools, die diese Methode verwenden, lassen sich auf drei Kernprobleme reduzieren:
- Wie laden wir Tests?
- Wie aggregieren und melden wir Ergebnisse aus den verschiedenen PHPUnit-Prozessen?
- Wie können wir Konsistenz mit dem ursprünglichen Tool (d. H. PHPUnit) herstellen?
Schauen wir uns einige Techniken an, die in der Vergangenheit angewendet wurden, und überprüfen wir dann ParaTest und wie es sich vom Rest der Masse unterscheidet.
Diejenigen, die vorher kamen
Wie bereits erwähnt, ist die Idee, PHPUnit in mehreren Prozessen auszuführen, nicht neu. Das typische Verfahren ist wie folgt:
- Suchen Sie nach Testmethoden oder laden Sie ein Verzeichnis mit Dateien, die Testsuiten enthalten.
- Öffnen Sie einen Prozess für jede Testmethode oder Suite.
- Analysiert die Ausgabe der STDOUT-Pipe.
Schauen wir uns ein Tool an, das diese Methode verwendet.
Hallo Paraunit
Paraunit war der ursprüngliche Parallelläufer, der mit dem Sausage werkzeug von Sauce Labs gebündelt wurde, und diente als Ausgangspunkt für ParaTest. Schauen wir uns an, wie es die drei oben genannten Hauptprobleme angeht.
Belastungtest
Paraunit wurde entwickelt, um Funktionstests zu vereinfachen. Es führt jede Testmethode und nicht eine gesamte Testsuite in einem eigenen PHPUnit-Prozess aus. Angesichts des Pfades zu einer Sammlung von Tests sucht Paraunit nach einzelnen Testmethoden über den Mustervergleich mit dem Dateiinhalt.
1 |
preg_match_all("/function (test[^\(]+)\(/", $fileContents, $matches); |
Geladene Testmethoden können dann folgendermaßen ausgeführt werden:
1 |
proc_open("phpunit --filter=$testName $testFile", $descriptorspec, $pipes); |
In einem Test, in dem jede Methode einen Browser einrichtet und herunterfährt, kann dies die Dinge erheblich beschleunigen, wenn jede dieser Methoden in einem separaten Prozess ausgeführt wird. Bei dieser Methode gibt es jedoch einige Probleme.
Während Methoden, die mit dem Wort "test" beginnen, eine starke Konvention unter PHPUnit-Benutzern sind, sind Anmerkungen eine weitere Option. Die von Paraunit verwendete Lademethode würde diesen vollkommen gültigen Test überspringen:
1 |
/**
|
2 |
* @test
|
3 |
*/
|
4 |
public function twoTodosCheckedShowsCorrectClearButtonText() |
5 |
{
|
6 |
$this->todos->addTodos(array('one', 'two')); |
7 |
$this->todos->getToggleAll()->click(); |
8 |
$this->assertEquals('Clear 2 completed items', $this->todos->getClearButton()->text()); |
9 |
}
|
Die Vererbung unterstützt nicht nur Testanmerkungen, sondern ist auch begrenzt. Wir könnten über die Vorzüge eines solchen Vorgangs streiten, aber betrachten wir das folgende Setup:
1 |
abstract class TodoTest extends PHPUnit_Extensions_Selenium2TestCase |
2 |
{
|
3 |
protected $browser = null; |
4 |
|
5 |
public function setUp() |
6 |
{
|
7 |
//configure browser
|
8 |
}
|
9 |
|
10 |
public function testTypingIntoFieldAndHittingEnterAddsTodo() |
11 |
{
|
12 |
//selenium magic
|
13 |
}
|
14 |
}
|
15 |
|
16 |
/**
|
17 |
* ChromeTodoTest.php
|
18 |
* No test methods to read!
|
19 |
*/
|
20 |
class ChromeTodoTest extends TodoTest |
21 |
{
|
22 |
protected $browser = 'chrome'; |
23 |
}
|
24 |
|
25 |
/**
|
26 |
* FirefoxTodoTest.php
|
27 |
* No test methods to read!
|
28 |
*/
|
29 |
class FirefoxTodoTest extends TodoTest |
30 |
{
|
31 |
protected $browser = 'firefox'; |
32 |
}
|
Die geerbten Methoden sind nicht in der Datei enthalten und werden daher niemals geladen.
Ergebnisse anzeigen
Paraunit aggregiert die Ergebnisse jedes Prozesses, indem es die von jedem Prozess generierte Ausgabe analysiert. Mit dieser Methode kann Paraunit die gesamte Bandbreite der von PHPUnit bereitgestellten Funktionscodes und Rückmeldungen erfassen.
Der Nachteil beim Aggregieren von Ergebnissen auf diese Weise ist, dass es ziemlich unhandlich und leicht zu brechen ist. Es gibt viele verschiedene Ergebnisse, die berücksichtigt werden müssen, und viele reguläre Ausdrücke bei der Arbeit, um auf diese Weise aussagekräftige Ergebnisse anzuzeigen.
Konsistenz mit PHPUnit
Aufgrund der Dateifassung ist Paraunit in seinen unterstützten PHPUnit-Funktionen ziemlich eingeschränkt. Es ist ein hervorragendes Tool zum Ausführen einer einfachen Struktur von Funktionstests, aber zusätzlich zu einigen der bereits erwähnten Schwierigkeiten werden einige nützliche PHPUnit-Funktionen nicht unterstützt. Einige dieser Beispiele umfassen Testsuiten, das Angeben von Konfigurations- und Bootstrap-Dateien, das Protokollieren von Ergebnissen und das Ausführen bestimmter Testgruppen.
Viele der vorhandenen Tools folgen diesem Muster. Durchsuchen Sie ein Verzeichnis mit Testdateien und führen Sie entweder die gesamte Datei in einem neuen Prozess oder jede Methode aus - niemals beides.
ParaTest bei Bat
Das Ziel von ParaTest ist es, parallele Tests für eine Vielzahl von Szenarien zu unterstützen. Ursprünglich entwickelt, um die Lücken in Paraunit zu schließen, wurde es zu einem robusten Befehlszeilenprogramm, mit dem sowohl Testsuiten als auch Testmethoden parallel ausgeführt werden können. Dies macht ParaTest zu einem idealen Kandidaten für Langzeittests in verschiedenen Formen und Größen.
Wie ParaTest mit parallelen Tests umgeht
ParaTest weicht von der etablierten Norm ab, um mehr von PHPUnit zu unterstützen, und fungiert als wirklich praktikabler Kandidat für parallele Tests.
Belastungtest
ParaTest lädt Tests auf ähnliche Weise wie PHPUnit. Es lädt alle Tests in ein angegebenes Verzeichnis, die mit dem Suffix *Test.php enden, oder lädt Tests basierend auf der Standard-PHPUnit-XML-Konfigurationsdatei. Das Laden erfolgt über Reflektion, sodass @test-Methoden, Vererbung, Testsuiten und einzelne Testmethoden problemlos unterstützt werden können. Durch die Reflexion wird das Hinzufügen von Unterstützung für andere Anmerkungen zum Kinderspiel.
Da ParaTest durch Reflektion Klassen und Methoden erfassen kann, können sowohl Testsuiten als auch Testmethoden parallel ausgeführt werden, was es zu einem vielseitigeren Tool macht.
ParaTest unterwirft einige Einschränkungen, aber fundierte in der PHP-Community. Tests müssen dem PSR-0-Standard folgen, und das Standard-Dateisuffix von *Test.php ist nicht konfigurierbar, wie es in PHPUnit der Fall ist. Derzeit wird ein Zweig zur Unterstützung derselben Suffix-Konfiguration ausgeführt, die in PHPUnit zulässig ist.
Ergebnisse anzeigen
ParaTest weicht auch vom Pfad zum Parsen von STDOUT-Pipes ab. Anstatt Ausgabestreams zu analysieren, protokolliert ParaTest die Ergebnisse jedes PHPUnit-Prozesses im JUnit-Format und aggregiert die Ergebnisse aus diesen Protokollen. Es ist viel einfacher, Testergebnisse aus einem festgelegten Format zu lesen als aus einem Ausgabestream.
1 |
<?xml version="1.0" encoding="UTF-8"?>
|
2 |
<testsuites>
|
3 |
<testsuite name="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" tests="3" assertions="3" failures="0" errors="0" time="0.005295"> |
4 |
<testcase name="testTruth" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="7" assertions="1" time="0.001739"/> |
5 |
<testcase name="testFalsehood" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="15" assertions="1" time="0.000477"/> |
6 |
<testcase name="testArrayLength" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="23" assertions="1" time="0.003079"/> |
7 |
</testsuite>
|
8 |
</testsuites>
|
Das Parsen von JUnit-Protokollen weist einige kleinere Nachteile auf. Übersprungene und ignorierte Tests werden nicht in der unmittelbaren Rückmeldung gemeldet, sondern in den nach einem Testlauf angezeigten Gesamtwerten wiedergegeben.
Konsistenz mit PHPUnit
Durch Reflection kann ParaTest mehr PHPUnit-Konventionen unterstützen. Die ParaTest-Konsole unterstützt mehr PHPUnit-Funktionen als jedes andere ähnliche Tool, z. B. die Möglichkeit, Gruppen auszuführen, Konfigurations- und Bootstrap-Dateien bereitzustellen und Ergebnisse im JUnit-Format zu protokollieren.
ParaTest-Beispiele
ParaTest kann verwendet werden, um in mehreren Testszenarien an Geschwindigkeit zu gewinnen.
Funktionsprüfung mit Selen
ParaTest zeichnet sich durch Funktionstests aus. Es unterstützt einen -f-Schalter in seiner Konsole, um den Funktionsmodus zu aktivieren. Der Funktionsmodus weist ParaTest an, jede Testmethode in einem separaten Prozess auszuführen, anstatt standardmäßig jede Testsuite in einem separaten Prozess auszuführen.
Es ist häufig der Fall, dass jede Funktionstestmethode viel Arbeit leistet, z. B. das Öffnen eines Browsers, das Navigieren auf der Seite und das anschließende Schließen des Browsers.
Das Beispielprojekt paratest-selenium demonstriert das Testen einer Backbone.js-ToDo-Anwendung mit Selenium und ParaTest. Jede Testmethode öffnet einen Browser und testet eine bestimmte Funktion:
1 |
public function setUp() |
2 |
{
|
3 |
$this->setBrowserUrl('https://backbonejs.org/examples/todos/'); |
4 |
$this->todos = new Todos($this->prepareSession()); |
5 |
}
|
6 |
|
7 |
public function testTypingIntoFieldAndHittingEnterAddsTodo() |
8 |
{
|
9 |
$this->todos->addTodo("parallelize phpunit tests\n"); |
10 |
$this->assertEquals(1, sizeof($this->todos->getItems())); |
11 |
}
|
12 |
|
13 |
public function testClickingTodoCheckboxMarksTodoDone() |
14 |
{
|
15 |
$this->todos->addTodo("make sure you can complete todos"); |
16 |
$items = $this->todos->getItems(); |
17 |
$item = array_shift($items); |
18 |
$this->todos->getItemCheckbox($item)->click(); |
19 |
$this->assertEquals('done', $item->attribute('class')); |
20 |
}
|
21 |
|
22 |
//....more tests
|
Dieser Testfall kann eine heiße Sekunde dauern, wenn er seriell über Vanilla PHPUnit ausgeführt wird. Warum nicht mehrere Methoden gleichzeitig ausführen?






Umgang mit Rennbedingungen
Wie bei jedem parallelen Test müssen wir Szenarien berücksichtigen, die Rennbedingungen darstellen - beispielsweise mehrere Prozesse, die versuchen, auf eine Datenbank zuzugreifen. Der Dev-Master-Zweig von ParaTest bietet eine sehr praktische Test-Token-Funktion, die von Mitarbeiter Dimitris Baltas (dbaltas on Github) geschrieben wurde und die Integrationstestdatenbanken erheblich vereinfacht.
Dimitris hat ein hilfreiches Beispiel beigefügt, das diese Funktion auf Github demonstriert. In Dimitris' eigenen Worten:
TEST_TOKENversucht auf sehr einfache Weise, das Problem der allgemeinen Ressourcen zu lösen: Klonen Sie die Ressourcen, um sicherzustellen, dass keine gleichzeitigen Prozesse auf dieselbe Ressource zugreifen.
Eine Umgebungsvariable TEST_TOKEN wird für Tests bereitgestellt und nach Abschluss des Prozesses wiederverwendet. Es kann verwendet werden, um Ihre Tests bedingt zu ändern:
1 |
public function setUp() |
2 |
{
|
3 |
parent::setUp(); |
4 |
$this->_filename = sprintf('out%s.txt', getenv('TEST_TOKEN')); |
5 |
}
|
ParaTest und Sauce Labs
Sauce Labs ist der Excalibur für Funktionstests. Sauce Labs bietet einen Service, mit dem Sie Ihre Anwendungen auf einfache Weise in einer Vielzahl von Browsern und Plattformen testen können. Wenn Sie sie noch nicht ausgecheckt haben, empfehle ich Ihnen dringend, dies zu tun.
Das Testen mit Sauce könnte ein Tutorial für sich sein, aber diese Assistenten haben bereits großartige Arbeit geleistet, um Tutorials für die Verwendung von PHP und ParaTest zum Schreiben von Funktionstests mithilfe ihres Dienstes bereitzustellen.
Die Zukunft von ParaTest
ParaTest ist ein großartiges Tool, um einige der Lücken von PHPUnit zu schließen, aber letztendlich ist es nur ein Stecker im Damm. Ein viel besseres Szenario wäre die native Unterstützung in PHPUnit!
In der Zwischenzeit wird ParaTest die Unterstützung für mehr natives Verhalten von PHPUnit weiter erhöhen. Es wird weiterhin Funktionen bieten, die für parallele Tests hilfreich sind - insbesondere im Funktions- und Integrationsbereich.
ParaTest hat viele großartige Dinge in Arbeit, um die Transparenz zwischen PHPUnit und sich selbst zu verbessern, vor allem, welche Konfigurationsoptionen unterstützt werden.
Die neueste stabile Version von ParaTest (v0.4.4) unterstützt bequem Mac, Linux und Windows, aber es gibt einige wertvolle Pull-Anfragen und Funktionen in dev-master, die definitiv die Mac- und Linux-Massen bedienen. Das wird also ein interessantes Gespräch für die Zukunft.
Zusätzliche Lektüre und Ressourcen
Es gibt eine Handvoll Artikel und Ressourcen im Internet, die ParaTest enthalten. Lesen Sie sie, wenn Sie interessiert sind:
- ParaTest auf Github
- Parallele PHPUnit von ParaTest-Mitarbeiter und PHPUnit Selenium-Erweiterungsbetreuer Giorgio Sironi
- Beitrag zu Paratest. Ein ausgezeichneter Artikel über Giorgios experimentellen WrapperRunner für ParaTest
- Giorgios WrapperRunner-Quellcode
- tripsta/paratest-sample. Ein Beispiel für die TEST_TOKEN-Funktion des Erstellers Dimitris Baltas
- brianium/paratest-selenium. Ein Beispiel für die Verwendung von ParaTest zum Schreiben von Funktionstests



