Abschnitt 17 · Lektion 2
PHPUnit einrichten
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
{
"name": "hofladen/kasse",
"require-dev": {
"phpunit/phpunit": "^11"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
} <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="alle">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit> <?php
// Startet PHPUnit. Der Pruefknopf startet immer eine PHP-Datei, also
// uebernimmt diese Zeile den Aufruf. Im Terminal brauchst du sie nicht,
// dort tippst du vendor/bin/phpunit direkt, wie auf deinem Rechner.
require __DIR__ . "/vendor/bin/phpunit"; <?php
declare(strict_types=1);
namespace App;
final class Rechnung
{
public const MWST = 19.0;
public static function brutto(float $netto): float
{
return round($netto * (1 + self::MWST / 100), 2);
}
public static function netto(float $brutto): float
{
return round($brutto / (1 + self::MWST / 100), 2);
}
} <?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.jsonsagt, was installiert wird und wo deine Klassen liegen. - Die
phpunit.xmlsagt PHPUnit, wo die Tests liegen. Mehr steht nicht darin, und mehr braucht ein kleines Projekt auch nicht. - Die Datei
tests-starten.phpist eine Eigenheit des Prüfknopfs. Der startet immer eine PHP-Datei, also steht der Aufruf in einer, und im Terminal siehst du ihn alsphp tests-starten.php. Sieh hinein: Unter den Kommentaren steht genau eine wirksame Zeile. Tippst du selbst, brauchst du den Umweg nicht:vendor/bin/phpunitim Terminal ist derselbe Lauf, so wie auf deinem eigenen Rechner. src/Rechnung.phpist 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.
{
"name": "hofladen/kasse",
"require-dev": {
"phpunit/phpunit": "^11"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
} <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="alle">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit> <?php
// Startet PHPUnit. Der Pruefknopf startet immer eine PHP-Datei, also
// uebernimmt diese Zeile den Aufruf. Im Terminal brauchst du sie nicht,
// dort tippst du vendor/bin/phpunit direkt, wie auf deinem Rechner.
require __DIR__ . "/vendor/bin/phpunit"; <?php
declare(strict_types=1);
namespace App;
final class Rechnung
{
public const MWST = 19.0;
public static function brutto(float $netto): float
{
return round($netto * (1 + self::MWST / 100), 2);
}
public static function netto(float $brutto): float
{
return round($brutto / (1 + self::MWST / 100), 2);
}
} <?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
{
"name": "hofladen/kasse",
"require-dev": {
"phpunit/phpunit": "^11"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
} <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="alle">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit> <?php
// Startet PHPUnit. Der Pruefknopf startet immer eine PHP-Datei, also
// uebernimmt diese Zeile den Aufruf. Im Terminal brauchst du sie nicht,
// dort tippst du vendor/bin/phpunit direkt, wie auf deinem Rechner.
require __DIR__ . "/vendor/bin/phpunit"; <?php
declare(strict_types=1);
namespace App;
final class Rechnung
{
public const MWST = 19.0;
public static function brutto(float $netto): float
{
return round($netto * (1 + self::MWST / 100), 2);
}
public static function netto(float $brutto): float
{
return round($brutto / (1 + self::MWST / 100), 2);
}
} <?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));
}
public function testNettoRechnetSieWiederHeraus(): void
{
$this->assertSame(100.0, Rechnung::netto(119.0));
}
} <?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, kostenlosZu 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.