mitmario.dev

Funktionen testen

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 2 von 6

Einen Test zu schreiben ist leicht. Testfälle zu wählen, die etwas aussagen, ist die eigentliche Arbeit, und hier gibt es zum Glück ein Rezept.

Drei Sorten Fälle, jedes Mal

Der Normalfall. Das, wofür die Funktion gebaut ist, mit gewöhnlichen Werten.

Die Ränder. Leer, eins, viele. Null, negativ, sehr groß. Der erste und der letzte Eintrag. Genau eins zu wenig und genau eins zu viel. Dort sitzen die Fehler, weil dort die Bedingungen umschlagen.

Der Fall, der schiefgehen soll. Ungültige Eingabe, fehlendes Feld, verbotener Zustand. Ohne ihn weißt du nur, dass deine Funktion arbeitet, wenn alle lieb sind. Das ist Lektion 17.3.

Nur der Normalfall beweist wenig
import { describe, it } from "node:test";
import assert from "node:assert/strict";

// Der Median: der mittlere Wert einer sortierten Liste.
function median(zahlen) {
  const sortiert = [...zahlen].sort((a, b) => a - b);
  return sortiert[Math.floor(sortiert.length / 2)];
}

describe("nur der Normalfall", () => {
  it("findet den mittleren Wert", () => {
    assert.equal(median([1, 2, 3]), 2);
  });
});

describe("dazu die Raender", () => {
  it("kommt mit einem einzigen Wert klar", () => {
    assert.equal(median([7]), 7);
  });

  it("sortiert vorher, die Eingabe darf unsortiert sein", () => {
    assert.equal(median([9, 1, 5]), 5);
  });

  it("mittelt bei gerader Anzahl die beiden mittleren", () => {
    assert.equal(median([1, 2, 3, 4]), 2.5);
  });
});

Der Median einer ungeraden Liste ist der mittlere Wert, und genau das prüft der erste Test. Er ist grün, und die Funktion ist trotzdem kaputt: Bei gerader Anzahl gehört der Mittelwert der beiden mittleren Werte heraus, und der letzte Test findet das.

Daran siehst du den Wert eines Tests. Er liegt nicht darin, dass er grün ist, sondern darin, welche Fehler er finden könnte. Ein Test, der nur den Normalfall prüft, könnte nur einen Fehler finden, bei dem gar nichts mehr funktioniert, und der wäre ohnehin sofort aufgefallen.

Vorbereiten, ausführen, prüfen

Ein Test hat drei Abschnitte, und es lohnt sich, sie sichtbar zu lassen.

Vorbereiten, ausführen, prüfen
import { describe, it } from "node:test";
import assert from "node:assert/strict";

function neueListe() {
  const eintraege = [];
  return {
    anlegen: (text) => eintraege.push({ id: eintraege.length + 1, text }),
    alle: () => [...eintraege],
  };
}

describe("ein Zustand fuer alle Tests", () => {
  // Sieht sparsam aus und ist der haeufigste Anfaengerfehler.
  const liste = neueListe();

  it("legt die erste Notiz an", () => {
    liste.anlegen("Milch kaufen");
    assert.equal(liste.alle().length, 1);
  });

  it("legt eine Notiz mit der Kennung 1 an", () => {
    liste.anlegen("Brot holen");
    assert.equal(liste.alle()[0].id, 1);
    assert.equal(liste.alle().length, 1);
  });
});

describe("jeder Test bereitet selbst vor", () => {
  it("legt die erste Notiz an", () => {
    // vorbereiten
    const liste = neueListe();

    // ausfuehren
    liste.anlegen("Milch kaufen");

    // pruefen
    assert.equal(liste.alle().length, 1);
  });

  it("legt eine Notiz mit der Kennung 1 an", () => {
    const liste = neueListe();

    liste.anlegen("Brot holen");

    assert.equal(liste.alle()[0].id, 1);
    assert.equal(liste.alle().length, 1);
  });
});

Die erste Gruppe spart sich das Vorbereiten und legt den Zustand einmal für beide Tests an. Das funktioniert genau so lange, wie kein Test etwas verändert, und Tests verändern ständig etwas. Hier sieht der zweite eine Notiz, die der erste angelegt hat.

Das Tückische daran ist nicht der rote Test, sondern was er dir erzählt: Er sagt „die Kennung stimmt nicht”, und in Wahrheit stimmt der Zustand nicht. Solche Tests schickt man auf die falsche Fährte, und sie werden je nach Reihenfolge rot oder grün.

Die zweite Gruppe baut in jedem Test frisch auf. Das sind zwei Zeilen mehr, und dafür ist jeder Test für sich lesbar und für sich lauffähig. Das ist die Regel: Ein Test bringt alles mit, was er braucht.

Wie ein Testname aussieht

Der Name ist die Fehlermeldung, die du bekommst, wenn der Test rot wird. Er sollte also den Satz enthalten, den du dann lesen willst.

Gut: gibt bei einer leeren Liste 0 zurueck. Das beschreibt Verhalten. Wird der Test rot, weißt du sofort, was nicht mehr stimmt, ohne den Test zu öffnen.

Schlecht: durchschnitt, test 2, funktioniert. Der erste benennt die Funktion, die anderen gar nichts. Wenn zwölf Tests funktioniert heißen, ist die Bilanz am Ende eine Zahl und keine Auskunft.

Eine Faustregel, die fast immer trägt: Der Name beginnt mit einem Verb und beschreibt, was herauskommt, nicht was hineingeht. wirft bei negativem Preis ist besser als negativer Preis, weil der Name dann für sich steht.

Warum Tests nicht auf Interna schauen

Der letzte Punkt ist der wichtigste und der am häufigsten verletzte.

Ein Test auf Interna und was er kostet
import { describe, it } from "node:test";
import assert from "node:assert/strict";

// Dieselbe Notizliste, zweimal gebaut. Von aussen verhalten sie sich gleich.
function mitArray() {
  const eintraege = [];
  return {
    eintraege,
    anlegen(text) {
      const notiz = { id: eintraege.length + 1, text };
      eintraege.push(notiz);
      return notiz;
    },
    alle: () => [...eintraege],
  };
}

function mitMap() {
  const nach_id = new Map();
  return {
    nach_id,
    anlegen(text) {
      const notiz = { id: nach_id.size + 1, text };
      nach_id.set(notiz.id, notiz);
      return notiz;
    },
    alle: () => [...nach_id.values()],
  };
}

for (const [name, baue] of [["mit Array", mitArray], ["mit Map", mitMap]]) {
  describe(name, () => {
    it("schaut auf die Interna", () => {
      const liste = baue();
      liste.anlegen("Milch kaufen");
      assert.equal(liste.eintraege.length, 1);
    });

    it("schaut auf das Verhalten", () => {
      const liste = baue();
      liste.anlegen("Milch kaufen");
      assert.equal(liste.alle().length, 1);
    });
  });
}

Zweimal dieselbe Notizliste, einmal mit einem Array darin und einmal mit einer Map. Von außen verhalten sie sich gleich: anlegen, auflisten, zählen. Der Test auf das Verhalten ist bei beiden grün. Der Test auf die Interna bricht beim Umbau, obwohl sich am Verhalten nichts geändert hat.

Ein Test, der bei einer Umbenennung rot wird, ist eine Bremse und kein Netz. Er hindert dich daran, den Code zu verbessern, und er schützt dich vor nichts, denn ein Fehler im Verhalten wäre ihm gar nicht aufgefallen.

Das ist derselbe Gedanke, der diesen ganzen Kurs trägt und der in jeder Challenge steckt: das Ergebnis prüfen statt den Code-Text. Keine deiner Aufgaben hat je verlangt, dass eine Variable einen bestimmten Namen hat oder eine Schleife eine for-Schleife ist. Geprüft wurde immer, was herauskommt, und genau so schreibst du auch deine eigenen Tests.

Praktisch heißt das: Teste über die Schnittstelle, die auch andere benutzen. Exportierte Funktionen ja, interne Hilfsfunktionen nein. Rückgabewerte ja, private Felder nein. Wenn dir dabei auffällt, dass sich etwas nur über Interna prüfen lässt, ist das oft ein Hinweis, dass der Schnittstelle etwas fehlt.

Zum Mitnehmen

Ein Test mit nur einem Normalfall beweist fast nichts. Die Fälle, die etwas beweisen, liegen an den Rändern, und der Testname beschreibt das Verhalten, nicht die Funktion.

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.