Abschnitt 17 · Lektion 5
Was man testet und was nicht
Nach dem ersten grünen Lauf kommt bei fast allen derselbe Gedanke: Jetzt alles testen. Das ist gut gemeint und der schnellste Weg, das Testen wieder aufzugeben. Eine Sammlung, die zu groß ist, wird langsam, bricht bei jeder Umbenennung und wird irgendwer irgendwann abschalten. Die Frage ist also nicht, ob man testet, sondern was.
Der wertvollste Test ist der zu einem gefundenen Fehler
{
"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 Versand
{
public const FREI_AB = 50.0;
public static function kosten(float $summe): float
{
// Die Regel lautet: ab 50 Euro versandkostenfrei.
return $summe > self::FREI_AB ? 0.0 : 4.95;
}
} <?php
declare(strict_types=1);
use App\Versand;
use PHPUnit\Framework\TestCase;
final class VersandTest extends TestCase
{
public function testUnterFuenfzigKostetDerVersand(): void
{
$this->assertSame(4.95, Versand::kosten(49.99));
}
public function testUeberFuenfzigIstErFrei(): void
{
$this->assertSame(0.0, Versand::kosten(80.0));
}
public function testAnDerGrenzeIstErSchonFrei(): void
{
// Genau 50 Euro. Das ist der Fall, den die Kundin gemeldet hat.
$this->assertSame(0.0, Versand::kosten(50.0));
}
} Die Regel lautet: ab 50 Euro versandkostenfrei. Im Code steht > statt >=, und damit zahlt
ausgerechnet die Kundin mit exakt 50 Euro noch 4,95. Zwei der drei Tests sind grün, der dritte ist
rot, und er sagt in einer Zeile, was los ist: Failed asserting that 4.95 is identical to 0.0. Im
Reiter Debug siehst du die drei Aufrufe durch dieselbe Zeile laufen, wenn src/Versand.php vorn
liegt: summe steht auf 49.99, dann 80, dann 50, und beim dritten Mal sagt das > nein.
{
"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 Versand
{
public const FREI_AB = 50.0;
public static function kosten(float $summe): float
{
// Ab 50 Euro heisst: 50 Euro gehoeren dazu.
return $summe >= self::FREI_AB ? 0.0 : 4.95;
}
} <?php
declare(strict_types=1);
use App\Versand;
use PHPUnit\Framework\TestCase;
final class VersandTest extends TestCase
{
public function testUnterFuenfzigKostetDerVersand(): void
{
$this->assertSame(4.95, Versand::kosten(49.99));
}
public function testUeberFuenfzigIstErFrei(): void
{
$this->assertSame(0.0, Versand::kosten(80.0));
}
public function testAnDerGrenzeIstErSchonFrei(): void
{
// Genau 50 Euro. Das ist der Fall, den die Kundin gemeldet hat.
$this->assertSame(0.0, Versand::kosten(50.0));
}
} Ein Zeichen geändert, alle drei grün. Und was wichtiger ist: Dieser Fehler kommt nicht wieder. Wer in einem halben Jahr an der Versandregel arbeitet, bekommt sofort gesagt, wenn die Grenze wieder kippt.
Das ist die beste Investition, die es beim Testen gibt. Ein Fehler, der einmal passiert ist, ist kein Zufall: Die Stelle ist offenbar leicht misszuverstehen. Mach es deshalb zur Gewohnheit, zu jedem gemeldeten Fehler zuerst den Test zu schreiben, der ihn festhält, ihn rot zu sehen, und ihn erst dann zu beheben. Rot zu sehen ist dabei kein Ritual: Ein Test, den du nie hast reißen sehen, könnte genauso gut nichts prüfen.
Wo Tests sich lohnen
- Rechnungen. Alles mit Geld, Mengen, Prozenten, Steuern. Fehler sind hier teuer und fallen spät auf.
- Regeln. Wer darf was, ab wann gilt welcher Preis, welcher Status folgt auf welchen. Das ist der Teil deiner Anwendung, den es nur bei dir gibt.
- Grenzfälle. Genau 50 Euro. Der leere Korb. Der letzte Tag im Monat. Null Treffer. Die meisten Fehler wohnen an den Rändern, nicht in der Mitte.
- Alles, was schon einmal kaputt war. Siehe oben.
Wo sie wenig bringen
{
"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 Versand
{
public const FREI_AB = 50.0;
public static function kosten(float $summe): float
{
// Ab 50 Euro heisst: 50 Euro gehoeren dazu.
return $summe >= self::FREI_AB ? 0.0 : 4.95;
}
} <?php
declare(strict_types=1);
namespace App;
final class Kunde
{
public function __construct(private readonly string $name)
{
}
public function name(): string
{
return $this->name;
}
} <?php
declare(strict_types=1);
use App\Kunde;
use App\Versand;
use PHPUnit\Framework\TestCase;
final class OhneWertTest extends TestCase
{
public function testDerGetterGibtZurueckWasHineingegebenWurde(): void
{
// Prueft, dass PHP eine Eigenschaft speichern kann.
$this->assertSame("Mia", (new Kunde("Mia"))->name());
}
public function testStrtolowerMachtKleinbuchstaben(): void
{
// Prueft die Standardbibliothek von PHP, nicht diesen Code.
$this->assertSame("mia", strtolower("MIA"));
}
public function testRuftNurAufUndBehauptetNichts(): void
{
// Laeuft durch, prueft aber nichts. Fuer eine Abdeckungszahl
// zaehlt diese Zeile trotzdem als geprueft.
Versand::kosten(60.0);
}
} Diese drei sind das Gegenstück, und sie sind absichtlich so gebaut, dass sie sich nützlich anfühlen:
- Der erste prüft einen Getter, der nichts tut, als zurückzugeben, was hineingegeben wurde. Er kann gar nicht fehlschlagen, solange PHP funktioniert.
- Der zweite prüft
strtolower(). Das ist die Standardbibliothek. Fremder Code hat seine eigenen Tests, und wenn nicht, ist ein eigener Test dafür die falsche Antwort auf das Problem. - Der dritte ruft nur auf und behauptet nichts. Sieh dir an, was PHPUnit dazu sagt: Der Punkt in der
Fortschrittszeile wird zu einem
Rfür riskant, und darunter stehtThis test did not perform any assertions. Am Ende steht dannOK, but there were issues!, und der Lauf endet mit einem Exit-Code von null;vendor/bin/phpunit; echo $?im Terminal zeigt die0. Er gilt also als bestanden. Genau das macht diese Sorte Test so tückisch. Wer das nicht durchgehen lassen will, hängt--fail-on-riskyan den Aufruf, dann endet derselbe Lauf mit1.
Ebenfalls selten die Mühe wert: Vorlagen. Ob im HTML ein <li> an der richtigen Stelle steht, ändert
sich bei jeder Gestaltungsrunde. Solche Tests reißen ständig, ohne dass je ein Fehler dahintersteckte,
und genau davon lernt man, das rote Ergebnis nicht mehr ernst zu nehmen.
Testabdeckung ist eine Zahl zum Ansehen
Es gibt Werkzeuge, die messen, welcher Anteil deiner Zeilen beim Testlauf ausgeführt wurde.
PHPUnit kann das, es braucht dafür allerdings eine zusätzliche PHP-Erweiterung, die hier im Kurs
nicht eingebaut ist. Tipp vendor/bin/phpunit --coverage-text ins Terminal, dann sagt es das selbst:
No code coverage driver available, und der Lauf endet mit einer Warnung.
Wichtiger als die Zahl ist ohnehin, wie man sie liest. Der dritte Test oben führt kosten() aus,
ohne irgendetwas zu behaupten. Für die Abdeckung zählt die Zeile damit als erledigt. Eine
Abdeckung von 100 Prozent heißt also: Jede Zeile ist einmal gelaufen. Sie heißt nicht: Jede Zeile tut
das Richtige.
Brauchbar ist die Zahl in genau einer Richtung. Sieh dir an, was nicht abgedeckt ist. Wenn dort die Rabattrechnung steht, hast du etwas gelernt. Als Ziel taugt sie nicht: Eine Vorgabe von 90 Prozent erzeugt Tests wie den dritten oben, und die sind schlimmer als keine, weil sie Sicherheit vortäuschen.
Die einfache Faustregel
Frag dich bei jedem Test: Welcher Fehler würde hier auffallen? Fällt dir keiner ein, schreib ihn nicht. Fällt dir einer ein, der schon passiert ist, schreib ihn sofort.
Zum Mitnehmen
Nach dem ersten grünen Lauf will man alles testen. Das ist der schnellste Weg zu einer Sammlung, die niemand mehr pflegt. Die brauchbare Frage lautet bei jedem einzelnen Test: Welcher Fehler würde hier auffallen?
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.