mitmario.dev

Warum Tests

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 1 von 6

Bis hierher hast du deinen Code auf eine einzige Art geprüft: Du hast ihn laufen lassen und dir angesehen, was herauskommt. Das ist völlig in Ordnung, und es ist der Anfang von allem. Es hat nur eine Eigenschaft, die mit jedem Tag schlechter wird: Es passiert im Kopf, und es passiert nur einmal.

Wie es ohne Tests aussieht

Von Hand ausprobieren
<?php

declare(strict_types=1);

function rabatt(float $summe, bool $stammkunde): float
{
    $satz = 0.0;

    if ($summe >= 100) {
        $satz = 5.0;
    }

    if ($summe >= 500) {
        $satz = 10.0;
    }

    // Zwei Punkte obendrauf, aber nur, wenn es ueberhaupt einen Rabatt gibt.
    if ($satz > 0.0 && $stammkunde) {
        $satz += 2.0;
    }

    return round($summe * $satz / 100, 2);
}

echo "80 Euro, Laufkunde:     ", rabatt(80, false), "\n";
echo "100 Euro, Laufkunde:    ", rabatt(100, false), "\n";
echo "500 Euro, Laufkunde:    ", rabatt(500, false), "\n";
echo "500 Euro, Stammkunde:   ", rabatt(500, true), "\n";
echo "80 Euro, Stammkunde:    ", rabatt(80, true), "\n";

Eine Rabattfunktion, fünf Aufrufe, fünf Zahlen. Sieh sie dir an und entscheide, ob sie stimmen. Du kannst das, denn die Regeln sind überschaubar:

  • Unter 100 Euro gibt es keinen Rabatt.
  • Ab 100 Euro sind es 5 Prozent.
  • Ab 500 Euro sind es 10 Prozent.
  • Stammkunden bekommen zwei Prozentpunkte mehr, aber nur, wenn es überhaupt einen Rabatt gibt.

Und genau darin steckt das Problem. Nicht im Prüfen, sondern im Wiederholen. Beim ersten Mal gehst du alle fünf Zeilen durch. Beim dritten Mal überfliegst du sie. Beim sechsten Mal, freitags um halb sechs, siehst du auf die erste, denkst „passt schon” und machst Feierabend. Das ist kein Charakterfehler, das ist der Normalzustand.

Die Prüfung hinschreiben statt sie zu denken

Dieselbe Prüfung, aufgeschrieben
<?php

declare(strict_types=1);

function rabatt(float $summe, bool $stammkunde): float
{
    $satz = 0.0;

    if ($summe >= 100) {
        $satz = 5.0;
    }

    if ($summe >= 500) {
        $satz = 10.0;
    }

    // Zwei Punkte obendrauf, aber nur, wenn es ueberhaupt einen Rabatt gibt.
    if ($satz > 0.0 && $stammkunde) {
        $satz += 2.0;
    }

    return round($summe * $satz / 100, 2);
}

// Mehr als das ist ein Test nicht: aufrufen, vergleichen, hinschreiben.
function pruefe(string $name, float $erwartet, float $bekommen): bool
{
    $ok = $erwartet === $bekommen;

    echo $ok ? "ok     " : "FEHLER ", $name, "\n";

    if (!$ok) {
        echo "       erwartet ", $erwartet, ", bekommen ", $bekommen, "\n";
    }

    return $ok;
}

$ergebnisse = [
    pruefe("unter 100 Euro gibt es nichts", 0.0, rabatt(80, false)),
    pruefe("ab 100 Euro sind es 5 Prozent", 5.0, rabatt(100, false)),
    pruefe("ab 500 Euro sind es 10 Prozent", 50.0, rabatt(500, false)),
    pruefe("Stammkunden bekommen zwei Punkte mehr", 60.0, rabatt(500, true)),
    pruefe("ohne Rabatt bringt auch die Treue nichts", 0.0, rabatt(80, true)),
];

$rot = count(array_filter($ergebnisse, fn (bool $ok) => !$ok));

echo "\n", count($ergebnisse), " Pruefungen, ", $rot, " davon rot\n";

Dieselbe Funktion, dieselben fünf Fälle. Der Unterschied ist, dass die Erwartung jetzt im Code steht. Vorher wusstest du, dass bei 500 Euro für einen Stammkunden 60 herauskommen muss. Jetzt weiß es das Programm.

Sieh dir pruefe() an, denn viel mehr ist ein Test nicht: Sie bekommt einen Namen, einen erwarteten Wert und den Wert, der wirklich herauskam, vergleicht beide und schreibt eine Zeile. Alles, was ein Testwerkzeug darüber hinaus kann, ist Bequemlichkeit. Der Kern ist ein Vergleich, eine Zeile Ausgabe und ein Rückgabewert.

Zwei Dinge daran sind schon jetzt wichtig. Der Name ist ein Satz und keine Nummer: Im Fehlerfall steht dort, welche Regel verletzt ist, nicht welcher Aufruf. Und die Ausgabe nennt bei einem Fehler beide Werte, den erwarteten und den bekommenen. Ein Test, der nur „FEHLER” sagt, schickt dich auf die Suche, die er dir eigentlich abnehmen sollte.

Der Vergleich mit === ist bei Kommazahlen übrigens heikler, als er hier aussieht. Hier geht er gut, weil round() auf zwei Stellen kürzt. Den sauberen Weg für alle anderen Fälle gibt es in der Lektion über Behauptungen, zwei weiter.

Der Tag, an dem sich etwas ändert

Eine Änderung, die etwas anderes kaputt macht
<?php

declare(strict_types=1);

function rabatt(float $summe, bool $stammkunde): float
{
    $satz = 0.0;

    if ($summe >= 100) {
        $satz = 5.0;
    }

    if ($summe >= 500) {
        $satz = 10.0;
    }

    // Neu von heute: die dritte Stufe.
    if ($summe >= 1000) {
        $satz = 15.0;
    }

    if ($stammkunde) {
        $satz += 2.0;
    }

    return round($summe * $satz / 100, 2);
}

function pruefe(string $name, float $erwartet, float $bekommen): bool
{
    $ok = $erwartet === $bekommen;

    echo $ok ? "ok     " : "FEHLER ", $name, "\n";

    if (!$ok) {
        echo "       erwartet ", $erwartet, ", bekommen ", $bekommen, "\n";
    }

    return $ok;
}

$ergebnisse = [
    pruefe("unter 100 Euro gibt es nichts", 0.0, rabatt(80, false)),
    pruefe("ab 100 Euro sind es 5 Prozent", 5.0, rabatt(100, false)),
    pruefe("ab 500 Euro sind es 10 Prozent", 50.0, rabatt(500, false)),
    pruefe("Stammkunden bekommen zwei Punkte mehr", 60.0, rabatt(500, true)),
    pruefe("ohne Rabatt bringt auch die Treue nichts", 0.0, rabatt(80, true)),
];

$rot = count(array_filter($ergebnisse, fn (bool $ok) => !$ok));

echo "\n", count($ergebnisse), " Pruefungen, ", $rot, " davon rot\n";

Jetzt kommt eine neue Anforderung: ab 1000 Euro 15 Prozent. Eine Zeile, harmlos. Beim Einbauen ist allerdings die Bedingung verlorengegangen, dass der Stammkunden-Bonus einen Rabatt voraussetzt.

Lass das Beispiel laufen, php index.php im Terminal. Vier Prüfungen bleiben grün, eine wird rot, und sie nennt dir die Regel, den erwarteten und den tatsächlichen Wert (erwartet 0, bekommen 1.6). Von Hand hättest du diesen Fall vermutlich gar nicht mehr angesehen: Du hast an der 1000er-Regel gearbeitet, und der kaputte Fall hat mit 1000 Euro nichts zu tun. Das ist die Sorte Fehler, die Wochen später als Kundenmail zurückkommt.

Nebenbei zeigt derselbe Lauf noch etwas: Für die neue Regel gibt es keine Prüfung. Fünf grüne Zeilen bedeuten nicht, dass alles geprüft ist, sondern nur, dass das Geprüfte stimmt. Dieser Unterschied bekommt später eine eigene Lektion.

Der eigentliche Gewinn ist nicht das Finden

Tests werden meistens damit erklärt, dass sie Fehler finden. Das tun sie, aber es ist der kleinere Teil. Der größere ist: Tests machen Code veränderbar.

Ohne sie ist jede Änderung an fremdem oder altem Code eine Wette. Du weißt nicht, was noch daran hängt, also fasst du möglichst wenig an, baust lieber daneben etwas Neues und lässt das Alte stehen. So entstehen die Dateien, vor denen sich alle fürchten. Mit einer Handvoll Tests wird aus der Wette eine Frage, die in zwei Sekunden beantwortet ist: umbauen, laufen lassen, hinsehen. Erst das macht Aufräumen möglich, und Aufräumen ist die Hälfte der Arbeit.

Deshalb lohnt sich ein Test besonders dort, wo du dich nicht traust. Nicht dort, wo du sicher bist.

Was sich schlecht testen lässt

Manche Funktionen wehren sich gegen jeden Test. Eine, die selbst eine Datenbank öffnet. Eine, die date() aufruft und deshalb morgen etwas anderes tut. Eine, die ihr Ergebnis nicht zurückgibt, sondern direkt ausgibt. Bei allen dreien merkst du beim Testen, dass du gar nicht herankommst.

Die Versuchung ist dann, den Test aufwendiger zu machen. Fast immer ist es aber der Code, der nicht passt: Er holt sich seine Zutaten selbst, statt sie entgegenzunehmen. Ein Test ist an dieser Stelle kein Prüfer, sondern ein Anzeiger. Wenn er wehtut, ist der Schnitt schuld. Wie man den ändert, ist eine eigene Lektion in diesem Abschnitt, und es ist die, die deinen Code am meisten verbessert.

Und ab jetzt mit Werkzeug

Alles, was du in dieser Lektion gesehen hast, funktioniert. Du könntest so weitermachen. Was fehlt, ist Kleinkram, den niemand fünfmal schreiben will: Fälle finden, ohne sie einzeln aufzurufen, den Unterschied zweier Werte lesbar anzeigen, den Lauf mit einem Exit-Code beenden, damit eine Maschine das Ergebnis versteht. Genau das nimmt dir PHPUnit ab, und zwar in der nächsten Lektion.

Zum Mitnehmen

Ein Test ist keine Zeremonie und kein Werkzeug. Ein Test ist ein zweites Stück Code, das dein erstes benutzt und hinterher sagt, ob das Ergebnis stimmt. Alles Weitere ist Bequemlichkeit.

Jetzt du

Basis Konto, kostenlos

Im Editor änderst du die Beispiele dieser Lektion und lässt sie gleich laufen. So merkst du am schnellsten, ob es sitzt.

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.