mitmario.dev

PHPUnit einrichten

PHP Sandbox 5 Min Lesezeit 3 BeispieleLektion 2 von 6

In der letzten Lektion hast du dir eine Prüffunktion selbst geschrieben. Sie war ein Dutzend Zeilen lang und hat funktioniert. PHPUnit ist dasselbe in ausgewachsen: das Testwerkzeug für PHP, seit über zwanzig Jahren, und in jedem Projekt zu finden, in dem überhaupt getestet wird.

Installieren, und zwar als Werkzeug

Installiert wird es mit Composer, genau wie jedes andere Paket aus Abschnitt 9, nur mit einem Zusatz: composer require --dev phpunit/phpunit ^11.

Der Schalter --dev schreibt das Paket in der composer.json unter require-dev statt unter require, und das ist keine Formsache. Unter require stehen die Pakete, ohne die dein Programm nicht läuft. PHPUnit gehört nicht dazu: Auf dem Server, der deine Seite ausliefert, wird nie ein Test laufen. Dort ruft man composer install --no-dev auf, und dann bleiben diese Pakete einfach weg. Weniger fremder Code auf der Maschine, die im Netz steht, ist ein Sicherheitsgewinn und kein Aufräumtick.

Sieh dir in den Beispielen die composer.json an: PHPUnit steht dort unter require-dev, genau so, wie Lektion 2 des Composer-Abschnitts es angekündigt hat.

Was neben deinem Test liegen muss

Der erste Test, ganz klein
<?php

declare(strict_types=1);

use App\Rechnung;
use PHPUnit\Framework\TestCase;

final class RechnungTest extends TestCase
{
    public function testBruttoSchlaegtDieMehrwertsteuerAuf(): void
    {
        $this->assertSame(119.0, Rechnung::brutto(100.0));
    }
}

Vier Dateien, und die ersten drei schreibst du in einem Projekt genau einmal:

  • Die composer.json sagt, was installiert wird und wo deine Klassen liegen.
  • Die phpunit.xml sagt PHPUnit, wo die Tests liegen. Mehr steht nicht darin, und mehr braucht ein kleines Projekt auch nicht.
  • Die Datei tests-starten.php ist eine Eigenheit des Prüfknopfs. Der startet immer eine PHP-Datei, also steht der Aufruf in einer, und im Terminal siehst du ihn als php tests-starten.php. Sieh hinein: Unter den Kommentaren steht genau eine wirksame Zeile. Tippst du selbst, brauchst du den Umweg nicht: vendor/bin/phpunit im Terminal ist derselbe Lauf, so wie auf deinem eigenen Rechner.
  • src/Rechnung.php ist der Code, um den es geht.

Und dann tests/RechnungTest.php, die eigentliche Arbeit. Sie hält sich an drei Regeln, die PHPUnit nicht vorschlägt, sondern voraussetzt: Die Datei endet auf Test.php, die Klasse heißt wie die Datei, und sie erbt von TestCase. Jede Methode, deren Name mit test beginnt, ist ein eigener Testfall. Ein Test ist also nichts weiter als eine Methode mit einem langen Namen.

Der Name ist ernst gemeint. testBruttoSchlaegtDieMehrwertsteuerAuf liest sich ungelenk und ist trotzdem richtig so, denn er ist das Einzige, was im Fehlerfall auf dem Bildschirm steht. test1 sagt dir dann gar nichts.

Was $this->assertSame(119.0, ...) genau bedeutet und welche Behauptungen es sonst noch gibt, ist die nächste Lektion. Für den Moment reicht: erwarteter Wert links, tatsächlicher rechts.

Die Ausgabe lesen

Bei einem erfolgreichen Lauf steht am Ende OK und in Klammern, wie viele Tests und wie viele Behauptungen gelaufen sind. Die Punkte darüber sind der Fortschritt, ein Punkt je Test.

Zwei Schalter lohnen sich von Anfang an. vendor/bin/phpunit --testdox im Terminal schreibt die Tests als Sätze untereinander, je einer mit Haken, aus testBruttoSchlaegtDieMehrwertsteuerAuf wird Brutto schlaegt die mehrwertsteuer auf; spätestens bei zwanzig Tests liest sich das besser als zwanzig Punkte. Und --colors=always färbt das OK grün und ein FAILURES! rot. Von selbst tut PHPUnit das hier nicht, denn das Terminal dieses Kurses ist für ein Programm keine Sitzung mit Bildschirm, sondern eine Leitung, und ohne den Schalter hält es Farbe für fehl am Platz.

Wenn ein Test fehlschlägt
<?php

declare(strict_types=1);

use App\Rechnung;
use PHPUnit\Framework\TestCase;

final class RechnungTest extends TestCase
{
    public function testBruttoSchlaegtDieMehrwertsteuerAuf(): void
    {
        // 100 Euro netto sind 119 Euro brutto. Hier steht mit Absicht
        // eine falsche Erwartung, damit du den Bericht einmal siehst.
        $this->assertSame(119.5, Rechnung::brutto(100.0));
    }
}

Interessanter ist der Fehlerfall, und den solltest du einmal in Ruhe ansehen. Der Bericht nennt nacheinander: die Nummer, dann Klasse und Methode, dann den Satz Failed asserting that ... is identical to ..., dann die Datei und Zeile, in der die Behauptung steht. Am Ende die Zeile Tests: 1, Assertions: 1, Failures: 1.

Aus dem Punkt oben ist ein F geworden. Das ist die Kurzschrift, an die du dich gewöhnen wirst: . bestanden, F Behauptung nicht erfüllt, E Fehler im Code, R verdächtig, S übersprungen.

Zwei Kleinigkeiten noch. Erstens steht in der Ortsangabe eine zweite Zeile, die auf vendor/bin/phpunit zeigt: Das ist der Umweg über tests-starten.php, den der Prüfknopf nimmt, und nie die interessante Zeile. Sieh immer auf die, die in deinen Ordner zeigt; tippst du vendor/bin/phpunit selbst, fehlt die zweite Zeile ganz. Zweitens endet ein fehlgeschlagener Lauf mit einem Exit-Code ungleich null. vendor/bin/phpunit; echo $? im Terminal zeigt die 1 unter dem Bericht, beim grünen Lauf eine 0. Genau daran erkennt später eine Maschine, ob sie deinen Stand ausliefern darf, und genau das konnte die selbstgebaute Prüffunktion aus der letzten Lektion nicht.

Wo PHPUnit sucht und wo nicht

Wo PHPUnit die Tests sucht
<?php

declare(strict_types=1);

use App\Rechnung;
use PHPUnit\Framework\TestCase;

// Diese Datei heisst Zweiter.php und nicht ZweiterTest.php. PHPUnit sieht
// deshalb gar nicht hinein, egal was darin steht.

final class ZweiterTest extends TestCase
{
    public function testWirdNieAufgerufen(): void
    {
        $this->assertSame(0.0, Rechnung::brutto(0.0));
    }
}

Im Ordner tests/ liegen hier zwei Dateien mit zusammen drei Testmethoden. Gelaufen sind zwei. Der Grund ist der Dateiname: PHPUnit durchsucht das Verzeichnis aus der phpunit.xml, nimmt aber nur Dateien, die auf Test.php enden. Zweiter.php wird gar nicht erst geöffnet. vendor/bin/phpunit --list-tests im Terminal zeigt, was PHPUnit gefunden hat: zwei Zeilen, beide aus RechnungTest. Und wer die Datei von Hand angibt, vendor/bin/phpunit tests/Zweiter.php, bekommt den Grund gesagt: Class Zweiter cannot be found. PHPUnit sucht die Klasse, die so heißt wie die Datei, und die gibt es nicht.

Das ist die Falle, die am häufigsten zuschlägt, und sie ist besonders unangenehm, weil sie grün aussieht. Ein Test, der nicht gefunden wird, meldet sich nicht. Er fehlt einfach. Wenn du dir sicher bist, dass du etwas geprüft hast, und die Zahl in der Klammer passt nicht dazu, sieh zuerst auf die Dateinamen.

Der Gegenfall macht mehr Lärm, und das ist gut: Heißt die Datei richtig, enthält aber keine Klasse mit passendem Namen, sagt PHPUnit Class ... cannot be found und beendet den Lauf mit No tests executed!. Diese Meldung wirst du in der Aufgabe gleich sehen.

Und die eine Zeile in der Konfiguration?

In der phpunit.xml steht bootstrap="vendor/autoload.php". Sie sorgt dafür, dass deine Klassen gefunden werden, bevor der erste Test läuft. In dieser Aufstellung könntest du sie sogar weglassen, weil PHPUnit den Autolader von Composer selbst mitbringt, wenn es über Composer installiert wurde. Probier es: Nimm das Attribut im Editor heraus und tipp vendor/bin/phpunit, es läuft trotzdem.

Stehen lassen solltest du sie trotzdem. Sobald ein Projekt einmal etwas vor den Tests erledigen muss, eine Zeitzone setzen zum Beispiel, ist bootstrap die Stelle dafür, und dann zeigt sie auf eine eigene Datei statt direkt auf den Autolader.

Zum Mitnehmen

Drei Regeln entscheiden, ob PHPUnit deinen Test überhaupt findet: die Datei endet auf Test.php, die Klasse heißt wie die Datei, die Methode fängt mit test an. Wer eine davon verletzt, bekommt keinen Fehler, sondern eine Zahl in der Klammer, die zu klein ist.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.