1. Code
  2. PHP

Paralleles Testen für PHPUnit mit ParaTest

Scroll to top
10 min read

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.

ParaTest CLIParaTest CLIParaTest CLI

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:

sample directory structuresample directory structuresample directory structure

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!

ParaTest vs Vanilla PHPUnitParaTest vs Vanilla PHPUnitParaTest vs Vanilla PHPUnit

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?

running selenium tests with ParaTestrunning selenium tests with ParaTestrunning selenium tests with ParaTest
Many chrome instances running functional testsMany chrome instances running functional testsMany chrome instances running functional tests

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_TOKEN versucht 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: