mitmario.dev

Behauptungen

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 3 von 6

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

Locker vergleichen und genau vergleichen
<?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

Die Handvoll Behauptungen, die reicht
<?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:

  • assertTrue und assertFalse für alles, was ohnehin schon bool ist. Was du damit nicht tun solltest, ist einen Vergleich hineinzuschreiben. assertTrue($korb->anzahl() === 5) meldet im Fehlerfall Failed 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.
  • assertNull für den Fall „gibt es nicht”. Der Bericht nennt dabei den Wert, den er stattdessen vorgefunden hat.
  • assertCount für Arrays. Etwas besser als assertSame(2, count(...)), weil die Meldung von Größen spricht statt von zwei nackten Zahlen: actual size 3 matches expected size 2 ist im Vorbeilesen klarer als 3 is identical to 2.
  • assertStringContainsString fü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

Eine erwartete Ausnahme
<?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, 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.