Softwareentwicklung: Testgetriebene Entwicklung nach der St. Pauli Schule

Seite 3: Grün statt Rot

Inhaltsverzeichnis

Es ist nun Zeit für einen Validierungstest, der beweist, dass die Berechnung auch für andere Fälle nicht bricht:

it("should return the number of spaces " +
   "for letter D", () => {
  expect(numberOfSpacesInMiddleLine("D")).toEqual(5);
});

Das Vorgehen stellt eine kleine Abweichung zum klassischen TDD dar. Dort gilt immer, dass ein neuer Test zunächst einen roten Zustand produzieren, also scheitern muss. Der Validierungstest ist aber überaus nützlich, weil er sicherstellt, dass die Implementierung nicht nur auf die Testfälle spezialisiert ist, sondern eine generische Umsetzung darstellt. Wer noch nicht genügend Vertrauen in die Testsuite hat, kann einfach weitere Validierungstests ergänzen.

Wichtig: Die Erwartungshaltung beim Validierungstest ist stets der grüne Zustand: der sofortige Erfolg!

Die Triangulation lässt sich rückwärts auflösen. Ein passender Funktionsaufruf ersetzt die Konstante numberOfSpacesInMiddleLine:

const middleLine = (letter) => {
  switch (letter) {
    case "B": {
      return letter + 
        spaces(numberOfSpacesInMiddleLine(letter)) +
        letter;
    }
    case "C": {
      return letter + 
        spaces(numberOfSpacesInMiddleLine(letter)) + 
        letter;
    }
  }
};

Da beide Zweige wieder identisch sind, entfällt der switch-Block:

const middleLine = (letter) =>
  letter + 
  spaces(numberOfSpacesInMiddleLine(letter)) + 
  letter;

Ein weiterer Validierungstest für die Mittellinie von "D" kann nicht schaden:

it("should return a middleLine for D", () => {
  expect(middleLine("D")).toEqual("D.....D");
});

Er ist ebenfalls sofort grün. Der bis zu diesem Zeitpunkt komplette Implementierungscode sieht folgendermaßen aus:

const diamond = (letter) => {
  switch (letter) {
    case "A":
      return "A";
    case "B": {
      const middleLine = "B.B";
      return [".A.", middleLine, ".A."].join("\n");
    }
    case "C": {
      const middleLine = "C...C";
      return ["..A..", ".B.B.", middleLine,
              ".B.B.", "..A.."].join("\n");
    }
  }
};

const middleLine = (letter) =>
  letter + 
  spaces(numberOfSpacesInMiddleLine(letter)) + 
  letter;

const spaces = (n) => ".".repeat(n);

const letterNumber = (letter) => 
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(letter);

const numberOfSpacesInMiddleLine = (letter) =>
  1 + 2 * (letterNumber(letter) - 1);

Die middleLine-Konstante ist durch diese Funktion ersetzbar:

const diamond = (letter) => {
  switch (letter) {
    case "A":
      return "A";
    case "B": {
      return [".A.", middleLine("B"), ".A."].join("\n");
    }
    case "C": {
      return ["..A..", ".B.B.", middleLine("B"), 
              ".B.B.", "..A.."].join("\n");
    }
  }
};

Nun wäre es Zeit, sich den Zeilen ober- und unterhalb der Mittellinie zuzuwenden. Zum Verständnis für das Prinzip der Schule trägt das nichts bei, da im weiteren Verlauf keine Neuerungen hinzukommen. Abschließend bietet es sich noch an, einen Gesamtvalidierungstest für den "D"-Diamanten zu ergänzen:

describe("Diamond Validation Test", () => {
  it("should return the whole diamond for D", () => {
    expect(diamond("D")).toEqual(
      "...A...\n" +
        "..B.B..\n" +
        ".C...C.\n" +
        "D.....D\n" +
        ".C...C.\n" +
        "..B.B..\n" +
        "...A..."
    );
  });
});

Läuft dieser letzte Test erfolgreich durch, ist die Umsetzung vermutlich richtig, auch wenn immer ein Restrisiko besteht. Der Validierungstest ist ein gutes Mittel, um vergessene Fakes aufzuspüren. Deswegen empfiehlt es sich durchaus, bei anderen Fake-basierten Ansätzen wie der Münchner Schule am Ende ebenfalls einen Validierungstest zu ergänzen. Folgendes Video zeigt die Originalumsetzung von Collins:

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

Neben den Vorteilen, die sich die St. Pauli Schule mit der Münchner Schule teilt, stechen vor allem zwei Neuerungen hervor: die inkrementell-iterative Entwicklung von Tests und die Validierungstests.

Die St. Pauli Schule führt zu einer inkrementell-iterativen Entwicklung nicht nur des Codes, sondern auch der Tests. Komplexe Probleme lassen sich damit auf simple Situationen begrenzen und später iterativ erweitern – ein Konzept, das TDD im Allgemeinen verfolgt und die St. Pauli Schule noch weiter vorantreibt.

Ein Validierungstest hat mehrere Vorteile: Zum einen kann er vergessene Fakes entlarven und zum anderen erheblich Zeit sparen. Beispielsweise kann es beim Entwickeln zu einem Bug kommen, der nicht auffällt, weil die für den TDD-Zyklus erforderlichen Testfälle erfolgreich waren. Dann muss später viel Zeit in das Debugging fließen.

Generell gilt: Je später ein Bug gefunden wird, umso teurer ist es, ihn zu beheben. Dank Validierungstests fällt der Bug nicht erst beim Ausliefern oder im manuellen Test auf. Im Grunde unterteilen Validierungstests die Implementierung durch mehrere Schotten: Schotten in einem Schiff verhindern bei einem Leck, dass mehrere Kammern volllaufen. Validierungstests helfen, Bugs stärker in einem Bereich zu lokalisieren.

Grundsätzlich spricht nichts dagegen, Validierungstests bei anderen TDD-Schulen einzusetzen, die in irgendeiner Form Fake- oder Mock-getrieben sind. Gerade bei der Münchner Schule sind sie eine sinnvolle Ergänzung.

Ein Blick in die Vergleichstabelle zeigt, dass München und St. Pauli sich im Wesentlichen durch den ersten Testfall unterscheiden. Wer das Vorgehen konsequent verwendet, benutzt auch unterhalb des API-Levels Triangulation und Normalisierung.

Schule St. Pauli Chicago /Detroit London München
Richtung Outside-In Inside-Out Outside-In Outside-In
Erster Testfall Einfach Einfach Einfach Komplex
Benutzung von Mocks vermeiden vermeiden nutzen vermeiden

Darin lässt sich das Trade-off der beiden Schulen erkennen: Die Münchner Schule kann schneller sein, da Refactoring nicht in mehreren Alternativfällen (wie im Beispiel in den switch-Anweisungen) gleichzeitig erfolgen muss. Der St.-Pauli-Stil ist dagegen explizit und macht Muster deutlich sichtbar. Das hilft vor allem in Situationen, in denen die Muster weniger gut erkennbar sind. In der Praxis ist es deswegen ratsam, je nach Situation zwischen beiden Schulen zu wechseln.

Ein weiterer Unterschied ist die Zielgruppe: Die Münchner Schule ist vor allem für diejenigen geeignet, die Erfahrung mit testgetriebener Entwicklung haben. Es ist wichtig zu erkennen, wann Zwischentests erforderlich sind, damit am Ende nicht nur ein einziger Test existiert. Das gezielte Ein- und Auskommentieren von Tests ist für Anfänger nur schwer einzuschätzen. Die St. Pauli Schule hat die "Append-only"-Regel, die das Vorgehen klar verbietet. Insgesamt ist die Herangehensweise bei St. Pauli mechanischer als bei der Münchner Schule, die mehr Freiheiten bietet. Das stark strukturierte Vorgehen nach St. Pauli erleichtert den Einstieg.

Bleibt die Frage, in welchem Stil man eine Aufgabe angeht. Sollte der erste äußere Testfall einfach oder komplex sein? Es kommt auf die Problemstellung an: Lässt sich ein Großteil der Anforderungen mühelos in einem einzigen Test abbilden, ist München sicherlich eine gute Wahl. Bei vielen unterschiedlichen Anforderungsszenarien lohnt es sich, mit einem einfachen Test im St.-Pauli-Stil zu beginnen.

Ähnlich wie bei der Münchner Schule ist der Zeitraum, den man unter grünen Tests im Refactoring verbringt, deutlich höher als der Anteil im Rotbereich. Das sorgt für ein verringertes Risiko. Das konsequente Outside-in-Vorgehen, das St. Pauli von der Londoner Schule geerbt hat, stellt sicher, dass kein unnötiger Code entsteht.