Test Driven Development anhand von FizzBuzz

mrBrown

Super-Moderator
Mitarbeiter
Im Folgenden will ich die Anwednung von Testgetriebener Entwicklung (Test Driven Development, üblicherweise nur mit TDD benannt) zeigen.
Für einen detailierte Beschreibung von TDD sei einem zB der Wiki-Artikel und entsprechende Literatur ans Herz gelegt, besonders natürich Test Driven Development by Example von Kent Beck.

Anders als im sonst häufig anzutreffenden Entwickelungsprozess, bei der man erst den Code und danach Tests dazu schreibt, kehrt man dies bei TDD um.
Als Erstes, noch bevor man überhaupt Code schreibt, schreibt man immer einen Test – und erst wenn ein Test geschrieben ist, wird der dafür nötige Code geschrieben.

Dabei folgt man drei Schritten, die man solange wiederholt, bis die gesamte gewünschte Funktionalität (oder anders: bis einem keine Testfälle mehr einfallen, die fehlschlagen könnten) implementiert ist:

1. Tests erstellen: Man fügt einen Tests hinzu, welcher Funktionalität erfordert, die bisher noch nicht erfüllt ist – dieser Test schlägt also immer fehl (als fehlschlagender Test gelten dabei auch Compiler-Fehler, zB weil eine Methode fehlt). Dabei sollte man jeweils den einfachst möglichen Testfall hinzufügen. Nach diesem Schritt schlägen mindestens ein Test fehl.
2. Funktionalität implementieren: Man verändert nun den Code, sodass der Tests erfüllt ist (je nach Änderungen muss man dabei auch die vorherigen Tests anpassen, wichtig ist dabei, diese nicht einfach zu entfernen). Dabei folgt man KISS und YAGNI, man fügt grad so viel Funktionalität hinzu, dass die Tests erfüllt sind, aber nicht mehr. Nach diesem Schritt müssen alle Tests erfolgreich durchlaufen.
3. Refactoring: Man überarbeitet den bisherigen Code, man fasst doppelte Dinge zusammen, entfernt überflüssiges, passt den Code-Stil an etc. Durch die Tests ist man dabei abgesichert, dass man nichts kaputt macht. Auch nach diesem Schritt müssen weiterhin alle Tests erfolgreich durchlaufen. Danach startet man wieder mit Schritt 1. (Wenn man sehr strikt ist, entsteht der eigentliche Code auch erst durch das Refactoring, in dem man Dinge in Methoden und Klassen auslagert, das werde ich hier aber nicht machen.)


Das Verfahren bietet mehrere Vorteile:

* Man macht sich mehr Gedanken über das Design, es entsteht nicht "einfach so"
* Jede Funktionalität ist immer durch eine konkrete Anforderung entstanden
* Die Tests fungieren gleichzeitig als Spezifikation und Dokumentation
* Der gesamte Code ist immer von Tests abgedeckt, bei Änderungen ist man dadurch abgesichert



Als Beispiel, um TDD zu demonstrieren, nutze ich hier FizzBuzz (u.U. gibts später noch ein Beispiel mit einer Pfadsuche, was etwas komplexer ist, hängt davon ab, ob ich den Code wiederfinde :D).

FizzBuzz ist ein einfaches Spiel, bei der man Zahlen ab Eins hochzählt. Für ein paar Zahlen gelten dabei Besonderheiten: wenn eine Zahl durch drei Teilbar ist, sagt man stattdessen "Fizz", wenn eine durch 5 teilbar ist "Buzz", und wenn sie durch beides teilbar ist beides, also "FizzBuzz".

Das ganze Beispiel wird *sehr* kleinschrittig, um das Verfahren zu zeigen. Manchen werden die Schritte sicherlich zu klein sein, und in der "echten Welt" werden viele es anders lösen, zur Demonstration ist es aber mit Absicht so gemacht :)

Der Code dazu findet sich unter https://gitlab.com/mrBrown/fizzbuzz :)


Also, anfangen mit der ersten Interation der drei Schritte: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/a283897ae99a6ccf1de8941b45516d4779212d1e)

Schritt 1:

Wir starten erstmal bei Null, mit dem allereinfachsten Testfall. Bei FizzBuzz geht es ums Hochzählen, und was ist dabei "das wenigste" was man machen kann? Einfach gar nicht erst anfangen zu zählen, also quasi "bis 0" zählen. (Diese Regel findet man bei den meisten Tests, egal zu welchem Thema, zB in Form einer leeren Liste, eines leeren Strings, einer 0, ...)

Wenn man ab 1 hochzählt und dabei "bis 0" zählt, was ist dann das Ergebnis? Einfach nichts.
Und wie könnte man das in Java am einfachsten ausdrücken? Einfach als null

Außerdem brauchen wir irgendeine Methode, die wir ausführen können, um das Ergebnis zu erhalten, und für eine Methode natürlich auch eine Klasse:

Aus diesem bauen wir jetzt den ersten Testfall:

Java:
    @Test
    void countToZero() {
        Object result = FizzBuzz.generate();

        assertNull(result);
    }

Schritt 2:

Der Code dazu ist ziemlich einfach, wir erstellen die Klasse mit der Methode und geben einfach nur null zurück – der Test ist danach erfolgreich:

Java:
public class FizzBuzz {

    public static Object generate() {
        return null;
    }

}
 

mrBrown

Super-Moderator
Mitarbeiter
Dann die zweite Iteration: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/b5fbc3fc7835cd1723b02ae37fdb351539e640d8)

Schritt 1:

Das nächst einfachste, was man sich überlegen könnte, wäre bis Eins zu zählen – und als Ergebnis würden wir dann auch einfach eine Eins erwarten.

Also wieder einen passenden Testfall überlegen: Wir müssen der Methode erstmal "mitteilen", dass wir bis Eins zählen wollen – am einfachsten, indem wir einfach eine 1 übergeben.
Außerdem müssen wir überlegen, was wir zurückgeben wollen, wir erwarten irgendwie eine Eins - und am simpelsten ist da auch einfach eine 1.
Also:

Java:
@Test
void countToOne() {
    Object result = FizzBuzz.generate(1);

    assertEquals(1, result);
}

Schritt 2:

Durch die 1, die wir der Methode übergeben, schlägt das Kompilieren fehl – also bekommt die Methode erstmal einen Parameter spendiert, 1 ist ein int, also einen int.
Java:
public static Object generate(int upTo) {
    return null;
}

Allerdings kompiliert dann der erste Test nicht mehr, da wir dort nichts mehr übergeben – lösen können wir das aber einfach, indem wir dort eine 0 übergeben, der Test soll ja sowieso das "Zählen bis 0" zeigen:
Java:
@Test
void countToZero() {
    Object result = FizzBuzz.generate(0);

    assertNull(result);
}

Der Code kompiliert damit zumindest wieder, allerdings schlägt der neue Test fehl, wenn man ihn ausführt:
Code:
org.opentest4j.AssertionFailedError: 
Expected :1
Actual   :null

Logisch – der Code gitb ja immer null zurück, obwohl wir in dem Test eine 1 erwarten.

Also den Code anpasse, auf die einfachste Weise: wenn 1 übergeben wird, wird 1 zurück gegeben:

Java:
public static Object generate(int upTo) {
    if (upTo == 1) {
        return 1;
    }
    return null;
}

Beide Tests sind danach erfolgreich, also zum nächsten Schritt:

3. Schritt

Bisher entspricht der Code überall unseren Vorstellungen von "gutem Code", wirklich etwas zu refactoren gibt es also nicht – wir lassen den Code also einfach, wie er ist.
 

mrBrown

Super-Moderator
Mitarbeiter
Zur dritten Iteration: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/055f2ade35b6bdb6b17e706e97a342e6a90811e7)

Schritt 1:

Nach Null und Eins kommt ... natürlich die Zwei. Also als nächstes ein Test der bis zwei zählt.
Als Ergebnis ist dabei dann nicht mehr eine einzelne Zahl das Ergebnis, sondern zwei: Eins und Zwei – in Java am leichtesten als Liste ausdrückbar, also schreiben wir das jetzt als Test:

Java:
@Test
void countToTwo() {
    Object result = FizzBuzz.generate(2);

    assertEquals(List.of(1, 2), result);
}

Wie erwarten schlägt auch der Test fehl:
Code:
org.opentest4j.AssertionFailedError:
Expected :[1, 2]
Actual   :null

Also weiter zu Schritt 2:

Und dann wieder die einfachste Lösung, mit der der Test erfolgreich ist: wenn eine Zwei übergeben wird, geben wie die erwartete Liste zurück:

Java:
public static Object generate(int upTo) {
    if (upTo == 1) {
        return 1;
    }
    if (upTo == 2) {
        return List.of(1,2);
    }
    return null;
}

Der Test läuft jetzt erfolgreich durch, allerdings beginnt der Code ziemlich unschön zu werden, so werden zB für unterschiedliche Eingaben unterschiedliche Typen zurückgegeben – verschönern kann man das mit einem Refactoring, wir machen also weiter mit Schritt 3:

Als erstes Ändern wir den Rückgabetyp so, dass es immer eine Liste ist. Statt der 1 geben wir also eine List.of(1) zurück. Das gleiche machen wir auch mit der null, mit einer Liste können wir "keinen Wert" ja auch mit einer leeren Liste ausdrücken:

Java:
public static List<Integer> generate(int upTo) {
    if (upTo == 1) {
        return List.of(1);
    }
    if (upTo == 2) {
        return List.of(1,2);
    }
    return List.of();
}

Die Änderung spiegel wir dann auch in den Tests wieder, auch dort ersetzen wir die erwarteten Werte mit Listen.
Java:
@Test
void countToZero() {
    List<Integer> result = FizzBuzz.generate(0);

    assertEquals(List.of(),result);
}

@Test
void countToOne() {
    List<Integer> result = FizzBuzz.generate(1);

    assertEquals(List.of(1), result);
}

@Test
void countToTwo() {
    List<Integer> result = FizzBuzz.generate(2);

    assertEquals(List.of(1, 2), result);
}


Außerdem können wir unseren Code noch etwas überarbeiten:
Wir geben eine Liste zurück, die für 0 keine Werte enthält, für 1 genau eine 1, für 2 eine 1 und eine 2 – und das ganze klingt doch sehr nach einer Schleife:

Java:
public static List<Integer> generate(int upTo) {
    List<Integer> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        result.add(i);
    }
    return result;
}

Die Tests sind weiterhin erfolgreich, noch weiter refactoren können wir nicht, also sind wir mit der Iteration fertig.
 

mrBrown

Super-Moderator
Mitarbeiter
Dann Iteration vier: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/65a0054f94534ed86434946a7a5cce18451eda17)

Schritt 1:

Und wieder von vorn, den nächsten Testfall überlegen, bei dem es einen Fehler geben könnte. Die nächst logische Zahl wäre eine Drei, und mit ein bisschen Nachdenken kommt man schnell drauf, dass bei einer Drei als Eingabe eine Drei auch die Ausgabe wäre – stattdessen aber "Fizz" gefordert wäre.

Also wieder Testfall schreiben:
Java:
@Test
void countToThree() {
    List<Integer> result = FizzBuzz.generate(3);

    assertEquals(List.of("1", "2", "Fizz"), result);
}

Der Test schlägt auch direkt fehl:
Code:
org.opentest4j.AssertionFailedError:
Expected :[1, 2, Fizz]
Actual   :[1, 2, 3]

Also weiter zu Schritt 2:

Irgendwie müssen wir jetzt den String in der Liste unterbringen, aktuell ist sie aber als List<Integer> definiert – als erstes müssen wir also das anpassen:
Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        result.add(String.valueOf(i));
    }
    return result;
}

Das gleiche müssen wir auch in den Tests ändern, dort ist es auch als Integer definiert, hier nur für countToOne, im git-Repo findet sich der ganze Code:
Java:
@Test
void countToOne() {
    List<String> result = FizzBuzz.generate(1);

    assertEquals(List.of("1"), result);
}

Der Code kompiliert jetzt schon mal korrekt, der Test schlägt aber immer noch fehl, da statt "Fizz" immer noch 3 in der Liste steht. Also fangen wir genau den Fall einfach ab und fügen in der Schleife "Fizz" statt 3 hinzu:

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (i == 3) {
            result.add("Fizz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

Die Tests laufen jetzt alle erfolgreich durch, der Code sieht auch einigermaßen aus, also überspringen wir einfach mal Schritt 3...
 
Zuletzt bearbeitet:

mihe7

Top Contributor
Müsste es nicht:
Java:
@Test
void countToThree() {
    List<Integer> result = FizzBuzz.generate(3);

    assertEquals(List.of("1", "2", "Fizz"), result);
}
heißen? (Meinen Beitrag kannst Du gerne wieder löschen, dann stört es beim Lesen nicht)
 

mrBrown

Super-Moderator
Mitarbeiter
Müsste es nicht:
Java:
@Test
void countToThree() {
    List<Integer> result = FizzBuzz.generate(3);

    assertEquals(List.of("1", "2", "Fizz"), result);
}
heißen? (Meinen Beitrag kannst Du gerne wieder löschen, dann stört es beim Lesen nicht)
Oh, ja natürlich, ist im echten Code auch passend gewesen 😅
 

mrBrown

Super-Moderator
Mitarbeiter
...und machen weiter mit Iteration 5: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/6d06eebf55291f9b8570eb0e31b1e060987a3942)

Schritt 1:

Wir überlegen wieder den nächst einfachen Testfall. Wenn wir weiter machen wie bisher wäre das die Vier, allerdings sind wie uns ziemlich sicher, dass für Vier auch schon das passende Ergebnis ausgegeben wird (nur die Drei wird mit "Fizz" ersetze, alles anderen Zahlen bleiben gleich, genau so wie es sein sollte). Die nächste Zahl wäre dann die Fünf – und da wird unser Code ziemlich sicher das falsch ausgeben, also ist Fünf der nächste Testfall:

Java:
@Test
void countToFive() {
    List<String> result = FizzBuzz.generate(5);

    assertEquals(List.of("1", "2", "Fizz", "4", "Buzz"), result);
}

Schritt 2:

Den Code können wir wieder genauso wie im vorherigen Schritt anpassen, wir fügen dem if-else einen weiteren Fall hinzu:

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (i == 3) {
            result.add("Fizz");
        } else if (i == 5) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

Tests laufen wieder alle erfolgreich durch, Refactoring ist auch nicht nötig, also weiter mit ...
 

mrBrown

Super-Moderator
Mitarbeiter
Iteration 6: (https://gitlab.com/mrBrown/fizzbuzz/-/commit/649c6804aa57d1a303ab6dd3c96a8ac79a8a0d64)

Schritt 1:

Wieder den nächsten interessanten Testfall suchen, die nächste offensichtliche Zahl wäre Sechs, und tatsächlich dürfte der Code dort nicht korrekt sein. Bisher wird nur für die Drei "Fizz" ausgegeben, für 6 muss das aber natürlich auch passieren.

Also fügen wir den Testfall hinzu:

Java:
@Test
void countToSix() {
    List<String> result = FizzBuzz.generate(6);

    assertEquals(List.of("1", "2", "Fizz", "4", "Buzz", "Fizz"), result);
}

Wie erwartet schlägt der Test auch fehl, statt "Fizz" wird "6" ausgegeben.

Schritt 2:

Und wieder suchen wir die einfachste Lösung, in diesem Fall können wir das if einfach um den Fall 6 erweitern:

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (i == 3 || i == 6) {
            result.add("Fizz");
        } else if (i == 5) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

Der Test ist damit erfolgreich.

Schritt 3:

Die Bedingung, i == 3 || i == 6, kann man jetzt noch verschönern. Sowohl 3 als auch 6 sind durch 3 teilbar (und zufällig ist das sogar genau das, was bei FizzBuzz gefordert ist), also ersetzen wir es damit: i % 3 == 0:

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (i % 3 == 0) {
            result.add("Fizz");
        } else if (i == 5) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

Die Tests laufen weiterhin durch, also alles richtig gemacht :)
 

mrBrown

Super-Moderator
Mitarbeiter
Und, um das zu einem Abschluss zu bringen:

Erstmal ein kurzes Refactoring der Test-Klasse (vielleicht lernt da ja noch jemand was über JUnit und das N Testfälle nicht N-mal sie viel Code wie ein Testfall ist ;) )

Die bisherigen Testfälle sind ja alle nahezu gleich aufgebaut, bis auf den "Startwert" und das erwartete Ergebnis. Der logische Schritt wäre also, dass schon mal in eine Methode auszulagern, und die Tests rufen diese dann nur noch auf.
Mit @ParametrizedTests geht das aber noch etwas schöner: anstatt die Parameter selbst zu übergeben, kann JUnit das übernehmen. Die Argumente lassen sich dabei auf verschiedensten Wegen definieren, in diesem Fall ist dabei @CsvSource am passendste, wobei man einfach Daten im CSV-Format angibt, welche von JUnit passend umgewandelt werden.

Nutzt man das, sieht die Testmethode im Ergebnis so aus:

Java:
@ParameterizedTest(name = "Should count up to {0}")
@CsvSource( {
        "0",
        "1, 1",
        "2, 1,2",
        "3, 1,2,Fizz",
        "4, 1,2,Fizz,4",
        "5, 1,2,Fizz,4,Buzz",
        "6, 1,2,Fizz,4,Buzz,Fizz",
})
void shouldCountCorrectly(int upTo, @AggregateWith(ArrayAggregator.class) String[] expected) {
    List<String> result = FizzBuzz.generate(upTo);

    assertEquals(List.of(expected), result);
}

(Mit [/icode]@AggregateWith(ArrayAggregator.class)[/icode] wird dabei auf einen ArgumentsAggregator verwiesen, der die letzten Spalten in den CSV-Daten zu einem Array zusammenfasst. Möglich wären auch andere Varianten, zB "[1,2,Fizz]" als String, also quasi die toString-Repräsentation einer Liste, und einen ArgumentConverter dafür angeben. Den Code dazu spar ich mir hier mal, wer interessiert ist findet den im Repo.)


Falls man seinem Refactoring nicht traut: einfach die normale Klasse "kaputt machen" – wenn die Tests das merken, war das Refactoring erfolgreiche, falls sie aber trotzdem noch erfolgreich sind, hat das Refactoring die Tests kaputt gemacht :)
Das ganze ist übrigens ein weiters Argument für "Test first". Dadurch, dass der Test immer erst einmal fehlschlagen muss, bevor man weiter macht, stellt man sicher, dass auch was getestet wird. Mit nachträglichen Tests rutscht schnell mal ein kaputter Test durch, z.B. eine fehlende Assertion.


Iteration 7:

Schritt 1:

Und wieder den nächsten Testfall überlegen: Vielfaches von drei haben wir schon, also als nächstes Vielfaches von 5. Wir können das einfach der schon bestehenden Methode als neuen Parameter hinzufügen:

Java:
@ParameterizedTest(name = "Should count up to {0}")
@CsvSource( {
        "0",
        "1, 1",
        "2, 1,2",
        "3, 1,2,Fizz",
        "4, 1,2,Fizz,4",
        "5, 1,2,Fizz,4,Buzz",
        "6, 1,2,Fizz,4,Buzz,Fizz",
        "10, 1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz",
})
void shouldCountCorrectly(int upTo, @AggregateWith(ArrayAggregator.class) String[] expected) {
    List<String> result = FizzBuzz.generate(upTo);

    assertEquals(List.of(expected), result);
}

Schritt 2:

Die Tests schlagen wie zu erwarten fehl, also wieder den normalen Code anpassen. Für die drei haben wir das ja schon gemacht, also machen wir das gleiche auch für 5:

Java:
public static List<String> generate(int upTo) {
     List<String> result = new ArrayList<>();
     for (int i = 1; i <= upTo; i++) {
         if (i % 3 == 0) {
            result.add("Fizz");
        } else if (i % 5 == 0) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

Die Tests laufen danach wieder wie erwartet erfolgreich durch.

Schritt 3:

Jetzt gibt es eine kleine Code-Doppelung, also refactoren wir den Code (in üblichen IDEs zB einfach markieren und "extract Method"):

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (isDivisibleBy(i, 3)) {
            result.add("Fizz");
        } else if (isDivisibleBy(i, 5)) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}

private static boolean isDivisibleBy(final int value, final int factor) {
    return value % factor == 0;
}

(Wenn man jetzt z.B. Kotlin nutzen würde, könnte man das auch als i.isDivisibleBy(3), mit Lombok müsste das gleichermaßen möglich sein. In Java ist das (leider|zum Glück) nicht möglich.)




Iteration 8:

Schritt 1:


Und wieder von vorn, den nächsten Testfall überlegen: Alle "normalen" Zahlen und Vielfache von drei oder fünf klappen bereits, aber was passiert bei Vielfach von drei und fünf, z.B. 15?

Den Code dafür kürzen wir etwas ab, besonders interessiert uns ja jetzt der 15te Wert:

Java:
@Test
void shouldReturnCorrectResultFor15() {
    String result = FizzBuzz.generate(15).get(14);

    assertEquals("FizzBuzz", result);
}

Wenn wir die Test jetzt laufen lassen, schlägt dieser fehl: Erwartet wird "FizzBuzz", das Ergebnis ist aber "Fizz" – logisch, 15 ist durch 3 teilbar, also wird "Fizz" zurückgegeben.

Schritt 2:

Also, Code anpassen. Der "Fall 15" muss ja vor dem "Fall 3" abgefangen werden, also fügen wir das vorher einfach hinzu:

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        if (isDivisibleBy(i, 15)) {
            result.add("FizzBuzz");
        } else if (isDivisibleBy(i, 3)) {
            result.add("Fizz");
        } else if (isDivisibleBy(i, 5)) {
            result.add("Buzz");
        } else {
            result.add(String.valueOf(i));
        }
    }
    return result;
}


Die Tests laufen dann wieder alle erfolgreich durch.
 

mrBrown

Super-Moderator
Mitarbeiter
Und abschließend noch etwas Refactoring:

Man könnte jetzt den Code noch etwas weiter refactoren: Die Schleie und die Berechnung des einzelnen Ergebnisses könnte man zB trennen (das gleiche wäre aber natürlich auch in jedem Schritt vorher möglich gewesen):

Java:
public static List<String> generate(int upTo) {
    List<String> result = new ArrayList<>();
    for (int i = 1; i <= upTo; i++) {
        result.add(get(i));
    }
    return result;
}

private static String get(final int i) {
    if (isDivisibleBy(i, 15)) {
        return "FizzBuzz";
    } else if (isDivisibleBy(i, 3)) {
        return ("Fizz");
    } else if (isDivisibleBy(i, 5)) {
        return ("Buzz");
    } else {
        return (String.valueOf(i));
    }
}

private static boolean isDivisibleBy(final int value, final int factor) {
    return value % factor == 0;
}

Wenn man möchte, könnte man auch Streams statt Schleifen nutzen:

Java:
public static List<String> generate(int upTo) {
    return IntStream.rangeClosed(1, upTo).mapToObj(FizzBuzz::get).collect(Collectors.toList());
}


Der wesentliche Punkt dabei ist aber, egal welche Änderung man macht, man ist dabei immer durch Tests abgesichert.
 
Ähnliche Java Themen

Ähnliche Java Themen


Oben