Test Driven Development (Teil 2 von 3)
Nachdem ich im vorherigen Teil die Basics von TDD erklärt habe, folgt hier ein:
an den Haaren herbeigezogenes Beispiel
Folgendes Beispiel ist inhaltlich total Banane und alles wird ganz übertrieben in Baby Steps abgehandelt: Alles ist in so kleine Schritte wie möglich zerteilt. (Nicht erschrecken, ich kann auch ordentlich programmieren, aber um die Eckdaten des TDD-Vorgehens verständlich zu präsentieren, stelle ich sie hier in leicht überspitzter Form dar. Natürlich macht man das soooo detailliert nicht in echt!)
In diesem Beispiel bauen wir eine Methode, die zwei Zahlen addiert.
Den Anfang macht der folgende Code: Ein erster Test für den denkbar einfachsten Fall „eins und eins ergibt zwei“ (auch so eine Sache: immer mit dem einfachsten anfangen) und eine leere Implementierung der Additions-Methode, die noch nichts tut, uns aber schon mal von allen Compilefehlern befreit.
- import static org.hamcrest.CoreMatchers.is;
- import static org.junit.Assert.assertThat;
- import org.junit.Test;
- public class AdditionTest {
- @Test
- public void testOnePlusOneIsTwo() {
- assertThat(add(1, 1), is(2));
- }
- private int add(int a, int b) {
- return 0;
- }
- }
Der Test schlägt fehl, weil wir in add()
erstmal nur 0
zurückgeben. Prima! Ein neuer Test muss ja auch rot sein. Nun ist unser Auftrag, den Test mit so wenig Aufwand wie möglich grün zu bekommen.
Jetzt nicht, wie ihr denkt, einfach ausprogrammieren, denn das wäre nicht der Weg des geringsten Aufwandes. Stattdessen benutzen wir die Fake It-Strategie und kümmern uns erstmal nicht um die Implementierung, sondern schreiben das Ergebnis als Konstante hin – der Rest kommt später.
- private int add(int a, int b) {
- return 2;
- }
Das reicht, um den Test grün zu kriegen. Wir sehen aber auch sofort, dass wir da ein Problem kriegen werden, wenn wir andere Zahlen addieren.
Das bringt uns auf die Idee, noch einen Test zu schreiben. Per Triangulation versuchen wir, die bestehende Implementierung zu überlisten und einen roten Testfall zu produzieren. Das ist nicht sehr schwierig. Auch hier nehmen wir wieder einen möglichst einfachen Fall, also nicht „dreihunderttausendsiebzehn plus fünfhundertdrölfzig“, sondern den zweit-einfachsten Fall „eins und zwei ergibt drei“ (man sieht, wie ich mich um die Null drücke – neutrales Additionselement, aus meiner Sicht ein Sonderfall):
- @Test
- public void testOnePlusTwoIsThree() {
- assertThat(add(1, 2), is(3));
- }
Kaputt! Wenig überraschend: da kommt nicht 3, sondern 2 zurück.
Zeit für eine Fallunterscheidung:
- private int add(int a, int b) {
- if (b == 2) {
- return 3;
- }
- return 2;
- }
Tadaa, heile!
Auch hier sehen wir schon wieder, dass das nicht dauerhaft gut ist. Aber bleibt bei mir, einmal ziehen wir das noch durch. Erst wieder ein triangulierter Test (ich hab’s im Blut, dass der kaputt sein wird):
- @Test
- public void testTwoPlusTwoIsFour() {
- assertThat(add(2, 2), is(4));
- }
Auch rot, wie zu erwarten.
Das kann man wieder fixen wie vorher:
- private int add(int a, int b) {
- if (a == 2) {
- return 4;
- }
- if (b == 2) {
- return 3;
- }
- return 2;
- }
Grün.
Wie bitte, das wird zunehmend unsinniger?
Richtig, aber ich will ja was zeigen. Daher kommt mir jetzt spontan die Idee, dass man die 4
da ja z.B. auch als 2+2
hinschreiben könnte:
- private int add(int a, int b) {
- if (a == 2) {
- return 2 + 2;
- }
- if (b == 2) {
- return 3;
- }
- return 2;
- }
Test laufen lassen und Bestätigung: immer noch grün, wir haben nichts kaputtgemacht.
Weil das so toll war, machen wir das für die anderen Ergebnisse auch mal. Ich habe das mal (an einem viel tolleren Beispiel, da hat sich dann nach drei trickreichen Schritten eine Rekursion aus dem Code geschält, sehr beeindruckend) als Defactoring kennengelernt. Keine Ahnung, ob das nur als Scherz gemeint war, ich finde den Begriff prima. Wir zerlegen das, was schon da ist, in seine Einzelteile und versuchen, darin Muster zu erkennen.
Wir haben jetzt also sowas
- private int add(int a, int b) {
- if (a == 2) {
- return 2 + 2;
- }
- if (b == 2) {
- return 1 + 2;
- }
- return 1 + 1;
- }
und unsere Tests sind noch grün.
Und jetzt springt uns, wenn wir nochmal auf die Parameter in den Tests gucken (die beiden if
geben das leider nur halbherzig zu erkennen, da ist mein Beispiel nicht perfekt), ins Auge, dass die einzelnen Zahlen im Ergebnis der Eingabe entsprechen (oh Wunder).
Einmal ist keinmal, zweimal ist Zufall, dreimal ist ein Muster.
Damit wir nicht so einen großen Sprung machen (wie gesagt, Baby Steps), wenden wir Parallel Implementation an: Bevor wir mehrere Stellen gleichzeitig ändern, probieren wir das erstmal an einer Stelle aus. Die Tests sind ja schnell durchlaufen:
- private int add(int a, int b) {
- if (a == 2) {
- return a + b;
- }
- if (b == 2) {
- return 1 + 2;
- }
- return 1 + 1;
- }
Grün, also gut.
Den nächsten Teilschritt spare ich mir jetzt tatsächlich mal, also kommen wir hierhin:
- private int add(int a, int b) {
- if (a == 2) {
- return a + b;
- }
- if (b == 2) {
- return a + b;
- }
- return a + b;
- }
Grün.
Jetzt sind wir bei klassischem Refactoring angekommen: Da ist doppelter Code, den sollte man vermeiden. Bevor wir das a+b
jetzt deswegen in eine eigene Methode auslagern, fällt uns auf, dass einfach nur die Bedingungen überflüssig sind. Also weg damit. (Auch hier könnte man wieder Stück für Stück Parallel Implementation betreiben, aber langsam wird mir der Artikel deswegen zu lang…)
- private int add(int a, int b) {
- return a + b;
- }
Grün.
Und darauf wäre ja wohl nun niemand von alleine gekommen ;-)
Bitte den Code erstmal in Ruhe verdauen, der abschließende dritte Teil zum Thema folgt.
Mitch’s Manga Blog am : Test Driven Development (Teil 1 von 3)
Mitch’s Manga Blog am : Test Driven Development (Teil 3 von 3)