zurück zum Artikel

Testgetriebene Entwicklung nach der Münchner Schule

Marco Emrich

(Bild: Blackboard/Shutterstock.com)

TDD blickt auf eine lange Geschichte zurück. Im Vergleich zu den bekannten Schulen Chicago und London bietet die Münchner Schule interessante Besonderheiten.

Testgetriebene Entwicklung (Test Driven Development, TDD) ist eine beliebte und nützliche Methodik. Richtig angewendet reduziert TDD die Fehlerdichte signifikant [1], ohne dabei Produktivitätsverluste in Kauf nehmen zu müssen. Vor allem hilft TDD, Softwaredesign zu verbessern.

Der Wiederentdecker von TDD Kent Beck verrät, dass er die Idee aus einem äußerst alten Programmierhandbuch übernommen hat [2]. Tatsächlich lassen sich die Ursprünge bis hin zu John von Neumann ins Jahr 1957 zurückverfolgen, der TDD seinerzeit mit vorgestanzten Lochstreifen betrieb.

In vielen Projekten der 60er und 70er Jahre unter anderem bei der NASA finden sich Hinweise auf frühes TDD. Leider ist das Wissen über die Technik anschließend für längere Zeit in Vergessenheit geraten oder war nur noch wenigen Eingeweihten bekannt. Erst 1989 entwickelte Kent Beck sUnit für Smalltalk und später jUnit für Java und begründete damit das moderne TDD. Über 60 Jahre TDD sind Grund genug zu fragen, wie sich die Methodik seither weiterentwickelt hat.

Wer TDD-Entwicklerinnen und -Entwicklern über die Schulter schaut, erkennt schnell unterschiedliche Methoden. Im Laufe der Jahre sind verschiedene Stile entstanden, die die Szene gerne als TDD-Schulen bezeichnet. Tatsächlich lassen sich Parallelen zur Kampfkunst feststellen – dort tragen Schulen oft den Namen ihres regionalen Ursprungs. Dasselbe gilt für das Test-driven Development.

Aktuell lassen sich mindestens sechs Schulen identifizieren:

Daneben gibt es weitere TDD-Stile, aber die sechs stechen hervor, weil sie mit einem eigenen Namen benannt und ihre jeweiligen Konzepte gut dokumentiert sind. Chicago und London sind die beiden ältesten, für die viel Literatur zur Verfügung steht. Die anderen Schulen sind bisher unterrepräsentiert. Das ist ein Grund, in diesem Artikel die Münchner Schule detaillierter vorzustellen.

David Völkel hat die Münchner Schule vor allem als Reaktion auf die Londoner Schule ins Leben gerufen. Letztere verzeichnet als eine der herausragenden Eigenschaften das sogenannte Outside-in-Vorgehen: Ein Akzeptanztest bildet auf hoher Ebene eine geforderte Anforderung ab. Um ihn zum Bestehen (grün) zu bringen, ist einiges an Aufwand nötig. Dazu dient weitere testgetriebene Entwicklung von der äußeren API bis in den Kern des Produktionscodes. Der erste Akzeptanztest bleibt derweil rot.

Erst nach dem Ende des inneren TDD-Zyklus färbt sich der äußere Akzeptanztest grün, und damit beginnt ein neuer Zyklus. Emily Bache beschreibt dieses Vorgehen [9] als Double-Loop ATDD (Acceptance Test Driven Development).

Das Double Loop ATDD erweitert den klassischen TDD-Zyklus um einen äußeren Akzeptanztest-getriebenen Zyklus.

Die konkrete Anforderung steht auch bei dem inneren Loop stets im Vordergrund. Das Vorgehen erzeugt eine Layer-Struktur, bei der jede neue Schicht die Anforderung der darüber liegenden erfüllt. Das verhindert nach dem YAGNI-Prinzip [10] ("You Aren't Gonna Need It"), dass überflüssiger Code entsteht. Jedes Stück Produktionscode ist erwiesenermaßen am Ende für das Projekt erforderlich. Damit die Tests nicht über die ganze Zeit des inneren Loops rot bleiben, erfolgt die Verifikation mit Mocks oder Spies sozusagen durch die Hintertür – die sogenannte Backdoor-Verification.

pushSpy = jest.spyOn(Array.prototype.push);
deck.addCardOnTop("9-Hearts");
expect(pushSpy).toHaveBeenCalledWith("9-Hearts");

Die Beispiele in diesem Artikel sind in JavaScript verfasst, aber die Programmiersprache spielt für die TDD-Schulen und die damit verbundenen Konzepte keine Rolle.

Im Code legt die Methode addCardOnTop die Karte "9-Hearts" auf ein Kartendeck. Statt den geänderten State ("9-Hearts" oben liegend, eine Karte mehr als vorher im Deck) zu überprüfen, stellt Backdoor-Verification sicher, dass der Produktionscode die darunter liegende Methode push des Array-Objektes mit dem richtigen Parameter "9-Hearts" aufgerufen hat. Auf die Weise lässt sich ein Layer des Produktionscodes schreiben, ohne dass der darunterliegende vorhanden sein muss.

Auf der Plus-Seite der Londoner Schule steht also die Beweisbarkeit, dass der Produktionscode erforderlich ist. Somit schreibt niemand überflüssigen Code. Ein weiterer Vorteil ist das konsequente Vorgehen: Es ist normalerweise deutlich, welcher Schritt als nächstes ansteht. Ein freies Experimentieren wie bei der Chicago Schule ist nicht nötig.

Auf der anderen Seite ist der häufige Einsatz von Mocks ein Nachteil. Sie sind oft schwer zu lesen und führen zu einer starken Verzahnung von Test- und Produktionscode. Als Folge ergeben sich fragile Tests.

Diese Schwächen auszugleichen war die Motivation bei der Gründung der Münchner Schule. Sie versucht, die Frage zu beantworten, ob sich das Outside-In-Vorgehen mit all seinen positiven Eigenschaften beibehalten lässt, ohne auf Mocks zurückgreifen zu müssen.

Die Münchner Schule ist kein völlig neues Konzept, sondern greift bestehende Muster und Konzepte auf, die länger bekannt sind und die sie zu einem kohärenten Vorgehen zusammenschnürt. Folgende Konzepte dienen als Grundlage:

Dazu gesellen sich zwei weitere, die Völkel entwickelt hat, um sein Vorgehen zu ermöglichen: Backward Calculation und Intermediate Test.

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 [13].

Zur Verdeutlichung der Münchner Schule dient im Folgenden die Diamond-Kata [14]. Dabei handelt es sich um eine bekannte Programmierübung, um TDD zu demonstrieren. Die Aufgabenstellung lautet folgendermaßen:

Ziel: Zu einem gegebenen Buchstaben wird eine Raute ausgegeben, die mit 'A' beginnt und den angegebenen Buchstaben an der breitesten Stelle hat.

diamond('B') erzeugt

.A.
B.B
.A.

diamond('C') gibt Folgendes aus:

..A..
.B.B.
C...C
.B.B.
..A..

Hinweis: Zur besseren Lesbarkeit verwendet der Beispielcode Punkte statt Leerzeichen.

Die Münchner Schule beginnt die Umsetzung ebenso wie die Londoner Schule mit einem Akzeptanztest. Andere Schulen wie London oder St. Pauli können mit einem einfachen Akzeptanztest starten. Für die Diamond-Kata wäre das beispielsweise der Diamant zu "A":

expect( diamond("A") ).toEqual("A")

Die Münchner Schule setzt stattdessen auf einen komplexen Test, der alle wichtigen Fälle der zu findenden Implementierung enthält. Das bedeutet für die Diamond-Kata, dass zumindest jede Art von Zeile enthalten sein muss. Deswegen ist der Diamant zu "C" als erster Test gut geeignet. Der Diamant enthält:

Der Diamant zu "D" ist nicht erforderlich, weil er keine weiteren Zeilentypen generiert. Der Weg bis zum "B"-Diamanten reicht dagegen nicht aus, da ihm die Zeile mit Leerzeichen außen und in der Mitte fehlt. Dementsprechend sieht der erste Akzeptanztest folgendermaßen aus:

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

Im nächsten Schritt würde die Londoner Schule einen inneren Test schreiben, der dazu dient, die zusammenarbeitenden Funktionen zu identifizieren und die Methodik der obersten Implementierungsebene mit Mocks zu testen. Die Münchner Schule setzt stattdessen auf das Fake-It-Konzept und liefert eine erste Fake-Implementierung.

const diamond = () =>
    "..A..\n" +
    ".B.B.\n" +
    "C...C\n" + 
    ".B.B.\n" +
    "..A..";

Dadurch ist der erste Test erfolgreich und der Spezialfall für "C" umgesetzt. Dabei ist die Implementierung nicht echt, da sie nur für den einen Spezialfall gilt. Die nächsten Schritte bei der Münchner Schule bestehen darin, über Refactoring den Fake schrittweise durch realen Code zu ersetzen.

Eine offensichtliche strukturelle Gemeinsamkeit ist der Zeilenumbruch. Um später die einzelnen Zeilen zu generieren, bietet es sich an, den Zeilenumbruch zu externalisieren:

const diamond = () =>
   ["..A..",
    ".B.B.",
    "C...C",
    ".B.B.",
    "..A.."].join("\n");

Nun ist es sinnvoll, die einzelnen Zeilen zu betrachten. Insbesondere die Mittellinie ist ein einfacher Kandidat, da sie laut Beschreibung den gewählten Buchstaben verwendet und dazwischen aus einer Anzahl von Leerzeichen besteht. Es ist außerdem an der Zeit, aus dem ersten grünen Akzeptanztest herauszubrechen und einen Zwischentest einzuführen:

describe("middleLine", () => {
  it('should return a middle Line for a letter', () => {
    expect(middleLine("C")).toEqual("C...C");
  });
});

Dafür genügt zunächst eine einfache Fake-Implementierung:

const middleLine = letter => "C...C";

Bei "C" handelt es sich um den Buchstaben aus dem Eingangsparameter letter:

const middleLine = letter => letter + "..." + letter;

Als Nächstes ist es sinnvoll, die Leerzeichen in der Mitte zu ersetzen. Die Funktion "space" generiert sie, entwickelt mit dem passenden Zwischentest:

describe("spaces", () => {
  it('should return a given number of spaces', () => {
    expect(spaces(3)).toEqual("...");
  });
});

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

Sobald die Funktion existiert, lässt sich der String "..." durch den Aufruf spaces(3) ersetzen:

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

Die Anzahl der Leerzeichen ist nun durch eine Zahl ausgedrückt und lässt sich somit für andere Buchstaben als C berechnen. In der Rechnung 1 + 2 * 1 ist die letzte 1 die Buchstabenposition für "C". Bei "B" wäre der Wert 0 und bei "D" 2. Auf der Grundlage lässt sich eine weitere Funktion zur Berechnung der Buchstabenposition plus dazugehörigem Zwischentest entwickeln. Nach dem Schema geht es weiter, bis kein Fake mehr vorhanden ist. Listing 1 zeigt die vollständige Implementierung [15].

Bei den zuletzt gezeigten Schritten ist das Kernmuster der Schule gut zu beobachten: das Backward-Calculation-Pattern. Dabei ersetzt man immer wieder einen Wert durch die Berechnung, die zu ihm führt.

Im Vergleich zu anderen TDD-Stilen hat die Münchner Schule einige Vor- und einige Nachteile.

TDD ist kein Automatismus. Damit es gut funktioniert, benötigen Entwicklerinnen und Entwickler Erfahrung, um aus sinnvollen Design-Alternativen für den Code wählen zu können [16]. Eine Herausforderung bei der Münchner Schule ist zu entscheiden, mit welcher Refactoring-Methode wie Extract Class oder Extract Method welche Code-Struktur entsteht.

Ein Risiko stellen fehlende Zwischentests dar. Theoretisch kann die Münchner Schule eine Aufgabe vollständig lösen und am Ende gibt es im Code nur einen einzigen Test. An der Stelle ist die Erfahrung der Einzelnen gefragt, um zu den passenden Zeitpunkten die richtige Menge an Zwischentests einzubringen.

Für den ersten Test ist bereits ein komplexerer Akzeptanztest erforderlich. Folgender einfacher Test würde nicht ausreichen:

expect(diamond("A")).toEqual("A")

Der eigentliche fachliche Code lässt sich durch diesen Test nicht treiben, da die wichtigen Fälle wie mehrere Zeilen oder Leerzeichen in der Mitte fehlen. Viele TDD-Anwenderinnen und -Anwender sehen im komplexen Starttest einen Nachteil der Schule. So hat Steven Collins die Schule St. Pauli unter anderem als Reaktion auf die Münchner Schule gegründet, um das Problem des komplexen Starttests zu lösen.

Wer beim Durchführen der Diamond-Kata die Zeit beachtet, die Tests jeweils im roten und im grünen Zustand verbringen, bemerkt eine Besonderheit. Im Gegensatz zu den anderen TDD-Schulen verbringt die Münchner Schule deutlich mehr Zeit im grünen Bereich. Das ist eine der gewünschten Eigenschaften. Mehr Zeit "im Grünen zu verbringen" bedeutet, dass insgesamt ein geringeres Risiko besteht steckenzubleiben.

Letzteres ist ein typisches TDD-Phänomen: Während ein roter Test vorliegt und die Entwicklerin oder der Entwickler versucht eine Implementierung zu finden, können Bugs entstehen, die nicht auffallen. Zu diesem Zeitpunkt ist "Rot" der erwartete Zustand. Andere Bugs die ebenfalls zu einem roten Zustand führen, bleiben dadurch unbemerkt. Bewegt sich die Entwicklung aber per Refactoring von Grün nach Grün, gibt es dieses Risiko nicht.

In der testgetriebenen Entwicklung haben sich über die Jahre viele Schulen herausgebildet, die für verschiedene Problemstellungen unterschiedlich gut geeignet sind. Die Münchner Schule bietet sich vor allem für Problemstellungen an, die eine klare Struktur aufweisen. Neben dem offensichtlichen Beispiel der Diamond-Kata ist sie für zeitliche Strukturen oder Abfolgen gut geeignet. Ungünstig sind dagegen Aufgaben mit vielen Sonderfällen. Bei solchen ist es schwierig, einen Einstiegstest zu formulieren, der von Anbeginn viele Sonderfälle behandelt.

In der TDD-Community ist die vorherrschende Meinung, dass alle Schulen ihre individuellen Vor- und Nachteile besitzen. Aus Software-Craft-Sicht lautet die Standardantwort auf die Frage nach der besten Schule oder dem besten Konzept deswegen: "Es ist eine gute Idee, den eigenen Werkzeuggürtel zu erweitern". Wer mehr Konzepte kennt, kann für eine konkrete Herausforderung das passende auswählen. In der Praxis spricht nichts dagegen, mehrere Schulen zu mischen oder einfach zu wechseln, wenn der gewählte Ansatz nicht zum Erfolg führt.

Wer nach einem guten Einstieg in das Thema Test-driven Development sucht, findet ihn in folgenden Medien:

Marco Emrich
ist Senior Consultant bei codecentric und leidenschaftlicher Verfechter von Software Craft und Codequalität. Er hält regelmäßig Vorträge auf bekannten Konferenzen und ist Autor mehrerer Fachbücher. Wenn Marco nicht gerade Entwicklertreffen organisiert, erklärt er seinem Sohn wahrscheinlich gerade, wie man Roboterschildkröten programmiert.

  1. Kent Beck; Test-driven Development by Example; Addison-Wesley Professional, 2002
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const letterNumber = (letter) => ALPHABET.indexOf(letter);
const spaces = (n) => ".".repeat(n);
const reverseString = (str) => str.split("").reverse().join("");

const diamondWith = (letter) =>
 /*one middle space*/ 1 + letterNumber(letter) *
 /*left and right*/ 2;

const numberOfMiddleSpaces = (letter) =>
 diamondWith(letter) - /* two letters on the borders*/ 2;

const numberOfOuterSpaces = ({ lineLetter, diamondLetter }) =>
 (diamondWith(diamondLetter) -
   numberOfMiddleSpaces(lineLetter) -
   /*two letters*/ 2) /
 2;

const peak = (letter) =>
 spaces(letterNumber(letter)) + "A" +
   spaces(letterNumber(letter));

const leftPartOfLine = ({ lineLetter, diamondLetter }) =>
 spaces(numberOfOuterSpaces({ lineLetter, diamondLetter }))
 + lineLetter;

const rightPartOfLine = (lineData) =>
  reverseString(leftPartOfLine(lineData));

const middlePartOfLine = (lineLetter) =>
 spaces(numberOfMiddleSpaces(lineLetter));

const line = ({ lineLetter, diamondLetter }) =>
 leftPartOfLine({ lineLetter, diamondLetter }) +
 middlePartOfLine(lineLetter) +
 rightPartOfLine({ lineLetter, diamondLetter });

const upperDiamond = (diamondLetter) => [
 peak(diamondLetter),
 ...ALPHABET.slice(1, letterNumber(diamondLetter))
   .split("")
   .map((lineLetter) => line({ lineLetter, diamondLetter })),
];
const lowerDiamond = (letter) => upperDiamond(letter).reverse();

const diamond = (letter) =>
 upperDiamond(letter)
   .concat(line({ lineLetter: letter, diamondLetter: letter }))
   .concat(lowerDiamond(letter))
   .join("\n");

describe("inBetweenLine", () => {
 it("should return .B.B. for Line for B for the C-Diamond",
    () => {
   expect(line({ lineLetter: "B",
                 diamondLetter: "C" })).toEqual(".B.B.");
 });

 it("should return a middle Line for a letter", () => {
   expect(line({ lineLetter: "C",
                 diamondLetter: "C" })).toEqual("C...C");
 });
});
describe('peakFor("C")', () => {
 it("should return ..A.. for the C-Diamond", () => {
   expect(peak("C")).toEqual("..A..");
 });
});

describe("numberOfLetterInAlphabet", () => {
 it("should return 2 for Letter 'C' - since A:0, B:1, C:2, ...",
    () => {
   expect(letterNumber("C")).toEqual(2);
 });
 it("should return 3 for Letter 'D' - since A:0, B:1, C:2, ...",
    () => {
   expect(letterNumber("D")).toEqual(3);
 });
});
describe("numberOfMiddleSpaces", () => {
 it("should return 1 space for a B-Diamond", () => {
   expect(numberOfMiddleSpaces("B")).toEqual(1);
 });
 it("should return 3 spaces for a C-Diamond", () => {
   expect(numberOfMiddleSpaces("C")).toEqual(3);
 });
 it("should return 5 spaces for a C-Diamond", () => {
   expect(numberOfMiddleSpaces("D")).toEqual(5);
 });
});

describe("spaces", () => {
 it("should return a given number of spaces", () => {
   expect(spaces(3)).toEqual("...");
 });
});

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

(rme [19])


URL dieses Artikels:
https://www.heise.de/-6287450

Links in diesem Artikel:
[1] https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf
[2] https://arialdomartini.wordpress.com/2012/07/20/you-wont-believe-how-old-tdd-is/
[3] https://github.com/testdouble/contributing-tests/wiki/Detroit-school-TDD
[4] http://coding-is-like-cooking.info/2013/04/the-london-school-of-test-driven-development/
[5] https://www.slideshare.net/DavidVlkel/fake-it-outsidein-tdd-itake-2018
[6] https://www.tddstpau.li
[7] https://ralfw.de/hamburg-style-tdd/
[8] https://www.infoq.com/presentations/TDD-as-if-You-Meant-It/
[9] http://coding-is-like-cooking.info/2013/04/outside-in-development-with-double-loop-tdd/
[10]  https://www.martinfowler.com/bliki/Yagni.html
[11] https://martinfowler.com/articles/preparatory-refactoring-example.html
[12] https://programming-with-ease.circle.so/c/articles/integration-operation-segregation-principle
[13] https://www.heise.de/Datenschutzerklaerung-der-Heise-Medien-GmbH-Co-KG-4860.html
[14] http://claysnow.co.uk/recycling-tests-in-tdd/
[15] https://www.heise.de/hintergrund/Testgetriebene-Entwicklung-nach-der-Muenchner-Schule-6287450.html?artikelseite=5
[16] https://www.codurance.com/publications/2015/05/12/does-tdd-lead-to-good-design
[17] https://www.devteams.at/red_green/2019/03/04/red_green_part_1_introduction.html
[18] http://www.jamesshore.com/v2/books/aoad1/test_driven_development
[19] mailto:rme@ix.de