Abschnitt 17 · Lektion 3
Behauptungen
Ein Test besteht aus zwei Teilen: Du bringst den Code in eine Lage, und dann behauptest du etwas über das Ergebnis. Der zweite Teil heißt bei PHPUnit Assertion, und davon bringt es 160 mit. Du kommst mit einer Handvoll aus.
assertSame ist die Vorgabe
{
"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 Korb
{
/** @var array<string, int> */
private array $waren = [];
public function lege(string $ware, int $menge): void
{
if ($menge < 1) {
throw new \InvalidArgumentException("Die Menge muss mindestens 1 sein.");
}
$this->waren[$ware] = ($this->waren[$ware] ?? 0) + $menge;
}
public function anzahl(): int
{
return array_sum($this->waren);
}
/** @return list<string> */
public function sorten(): array
{
return array_keys($this->waren);
}
public function menge(string $ware): ?int
{
return $this->waren[$ware] ?? null;
}
public function istLeer(): bool
{
return $this->waren === [];
}
public function beleg(): string
{
$zeilen = [];
foreach ($this->waren as $ware => $menge) {
$zeilen[] = $menge . " x " . $ware;
}
return implode(", ", $zeilen);
}
} <?php
declare(strict_types=1);
use App\Korb;
use PHPUnit\Framework\TestCase;
final class VergleichTest extends TestCase
{
private function korbMitBrot(): Korb
{
$korb = new Korb();
$korb->lege("Brot", 2);
return $korb;
}
public function testEqualsVergleichtLocker(): void
{
// anzahl() gibt eine ganze Zahl zurueck, hier steht eine
// Zeichenkette. assertEquals vergleicht mit == und ist zufrieden.
$this->assertEquals("2", $this->korbMitBrot()->anzahl());
}
public function testEqualsAuchBeiKommazahlen(): void
{
$this->assertEquals(2.0, $this->korbMitBrot()->anzahl());
}
public function testSameSiehtDenTyp(): void
{
// Dieselbe Behauptung wie oben, nur mit assertSame. Sie reisst
// mit Absicht.
$this->assertSame("2", $this->korbMitBrot()->anzahl());
}
public function testSameMitPassendemTyp(): void
{
$this->assertSame(2, $this->korbMitBrot()->anzahl());
}
} assertSame($erwartet, $tatsaechlich) vergleicht mit ===, also Wert und Typ.
assertEquals() vergleicht mit == und macht vorher alles passend. Das ist genau der Unterschied
aus Lektion 6 des Abschnitts über Rechnen und Text, nur diesmal mit Folgen.
Im Beispiel siehst du ihn an derselben Behauptung: anzahl() liefert die ganze Zahl 2. Die
Erwartung "2" als Zeichenkette geht bei assertEquals durch, und 2.0 als Kommazahl auch. Bei
assertSame reißt sie, mit Failed asserting that 2 is identical to '2'. Willst du nur diese zwei
Tests sehen, tipp vendor/bin/phpunit --filter Same ins Terminal: Es läuft dann nur, was Same im
Namen trägt, F. statt ..F.. So sieht man sich einen einzelnen Fall an, ohne die anderen
mitzunehmen.
Die Folge ist unangenehmer, als sie klingt. Ein Test mit assertEquals hätte auch dann noch grün
gemeldet, wenn anzahl() irgendwann eine Zeichenkette zurückgibt statt einer Zahl. Der Test soll dir
aber sagen, dass sich etwas geändert hat. Nimm deshalb assertSame, und weiche nur ab, wenn du
weißt, warum.
Eine Ausnahme gibt es wirklich, und du wirst ihr begegnen: Kommazahlen mit Nachkommastellen kann man
nicht sinnvoll auf Gleichheit prüfen, weil 0.1 + 0.2 eben nicht genau 0.3 ist. Dafür ist
assertEqualsWithDelta($erwartet, $tatsaechlich, 0.001) da. Wo im Code schon gerundet wird, wie in
der letzten Lektion, geht assertSame weiterhin.
Die Handvoll, die neunzig Prozent abdeckt
{
"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 Korb
{
/** @var array<string, int> */
private array $waren = [];
public function lege(string $ware, int $menge): void
{
if ($menge < 1) {
throw new \InvalidArgumentException("Die Menge muss mindestens 1 sein.");
}
$this->waren[$ware] = ($this->waren[$ware] ?? 0) + $menge;
}
public function anzahl(): int
{
return array_sum($this->waren);
}
/** @return list<string> */
public function sorten(): array
{
return array_keys($this->waren);
}
public function menge(string $ware): ?int
{
return $this->waren[$ware] ?? null;
}
public function istLeer(): bool
{
return $this->waren === [];
}
public function beleg(): string
{
$zeilen = [];
foreach ($this->waren as $ware => $menge) {
$zeilen[] = $menge . " x " . $ware;
}
return implode(", ", $zeilen);
}
} <?php
declare(strict_types=1);
use App\Korb;
use PHPUnit\Framework\TestCase;
final class KorbTest extends TestCase
{
public function testEinFrischerKorbIstLeer(): void
{
$this->assertTrue((new Korb())->istLeer());
}
public function testMitWareIstErNichtMehrLeer(): void
{
$korb = new Korb();
$korb->lege("Brot", 1);
$this->assertFalse($korb->istLeer());
}
public function testZweiSortenSindZweiEintraege(): void
{
$korb = new Korb();
$korb->lege("Brot", 1);
$korb->lege("Milch", 3);
$this->assertCount(2, $korb->sorten());
}
public function testEineWareOhneEintragHatKeineMenge(): void
{
$this->assertNull((new Korb())->menge("Kaffee"));
}
public function testDerBelegNenntDieWare(): void
{
$korb = new Korb();
$korb->lege("Brot", 2);
$this->assertStringContainsString("Brot", $korb->beleg());
}
} Fünf Tests, fünf verschiedene Behauptungen, und damit kommst du sehr weit:
assertTrueundassertFalsefür alles, was ohnehin schonboolist. Was du damit nicht tun solltest, ist einen Vergleich hineinzuschreiben.assertTrue($korb->anzahl() === 5)meldet im FehlerfallFailed asserting that false is true, und damit weißt du genau nichts: Die Zahl, die wirklich herauskam, ist unterwegs verlorengegangen.assertSame(5, $korb->anzahl())nennt sie dir.assertNullfür den Fall „gibt es nicht”. Der Bericht nennt dabei den Wert, den er stattdessen vorgefunden hat.assertCountfür Arrays. Etwas besser alsassertSame(2, count(...)), weil die Meldung von Größen spricht statt von zwei nackten Zahlen:actual size 3 matches expected size 2ist im Vorbeilesen klarer als3 is identical to 2.assertStringContainsStringfür Text, bei dem dich nur ein Teil interessiert. Ein Beleg, eine Fehlermeldung, eine erzeugte Seite: Auf den vollständigen Wortlaut zu prüfen macht den Test kaputt, sobald jemand ein Komma verschiebt.
Der Rest ergibt sich, wenn du ihn brauchst. Die Namen sind so gebaut, dass man sie raten kann, und die Meldung im Fehlerfall ist immer die halbe Miete.
Eine Ausnahme prüfen
{
"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 Korb
{
/** @var array<string, int> */
private array $waren = [];
public function lege(string $ware, int $menge): void
{
if ($menge < 1) {
throw new \InvalidArgumentException("Die Menge muss mindestens 1 sein.");
}
$this->waren[$ware] = ($this->waren[$ware] ?? 0) + $menge;
}
public function anzahl(): int
{
return array_sum($this->waren);
}
/** @return list<string> */
public function sorten(): array
{
return array_keys($this->waren);
}
public function menge(string $ware): ?int
{
return $this->waren[$ware] ?? null;
}
public function istLeer(): bool
{
return $this->waren === [];
}
public function beleg(): string
{
$zeilen = [];
foreach ($this->waren as $ware => $menge) {
$zeilen[] = $menge . " x " . $ware;
}
return implode(", ", $zeilen);
}
} <?php
declare(strict_types=1);
use App\Korb;
use PHPUnit\Framework\TestCase;
final class MengeTest extends TestCase
{
public function testMengeNullWirdAbgelehnt(): void
{
$korb = new Korb();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("mindestens 1");
$korb->lege("Brot", 0);
}
public function testHierKommtKeineAusnahme(): void
{
// Reisst mit Absicht: 2 ist eine gueltige Menge, es wird also
// nichts geworfen, obwohl dieser Test es erwartet.
$korb = new Korb();
$this->expectException(InvalidArgumentException::class);
$korb->lege("Brot", 2);
}
} Bis hierher hast du Rückgabewerte geprüft. Der Fehlerfall aus dem Abschnitt über Ausnahmen gibt aber
nichts zurück, er wirft. Dafür gibt es keine assert-Methode, sondern eine Ankündigung:
$this->expectException(InvalidArgumentException::class) sagt „im weiteren Verlauf dieses Tests wird
eine solche Ausnahme geworfen”. Kommt sie, ist der Test bestanden. Kommt sie nicht, reißt er.
Die Reihenfolge ist dabei die Falle. Die Ankündigung steht vor dem Aufruf, nicht danach. Wer sie darunter schreibt, hat den Aufruf schon ohne Schutznetz gemacht, und die Ausnahme fliegt an PHPUnit vorbei.
Mit expectExceptionMessage() prüfst du zusätzlich einen Teil des Textes. Nimm dafür ein kurzes
Stück, das den Fall beschreibt, nicht den ganzen Satz. Sonst schreibst du den Test jedes Mal um, wenn
jemand eine Formulierung glättet.
Der zweite Test im Beispiel zeigt den Fehlerfall: Er erwartet eine Ausnahme, die nicht kommt, und die Meldung sagt genau das.
Ein Test, eine Behauptung
Als Faustregel und nicht als Gesetz: Ein Test prüft eine Sache. Das hat einen sehr praktischen Grund, und der hat nichts mit Reinheit zu tun. PHPUnit hört in einem Test bei der ersten gerissenen Behauptung auf. Stehen fünf in einer Methode, siehst du nur die erste und darfst danach nochmal laufen lassen. Stehen sie in fünf Methoden, siehst du auf einen Schlag, welche vier davon in Ordnung sind.
Zwei Behauptungen zusammenzunehmen, die dieselbe Sache aus zwei Blickwinkeln beschreiben, ist trotzdem in Ordnung. Die Regel heißt „ein Gedanke je Test”, nicht „eine Zeile”.
Namen, die im Fehlerfall etwas sagen
Der Methodenname ist das Erste, was im Bericht steht, und meistens auch das Einzige, das du liest.
testKorb1 zwingt dich, die Datei zu öffnen. testEineWareOhneEintragHatKeineMenge sagt dir schon
im Vorbeigehen, welche Regel gerade verletzt ist.
Ein brauchbares Muster: Was ist der Fall, und was soll dabei herauskommen. Lang ist dabei kein Problem. Diese Namen ruft niemand von Hand auf.
Zum Mitnehmen
PHPUnit bringt über hundert Behauptungen mit. Sieben davon decken fast alles ab, und die wichtigste Entscheidung ist die zwischen zwei Zeichen: assertSame vergleicht mit ===, assertEquals mit ==.
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.