Ein TDD-Beispiel

Um zu veranschaulichen, warum das nicht stimmt, und um die Vorteile von mutationsgetriebenem Testen zu demonstrieren, habe ich das folgende Beispiel zusammengestellt. Die Quelle des Beispiels ist das oberste Google-Ergebnis, wenn Sie nach "TDD Example" suchen.

Man könnte argumentieren, dass der Autor dieses Artikels kein gutes Vorbild für TDD ist und dass "kein echter TDD-Praktiker" Tests wie diese schreiben würde. Aber so zu argumentieren, ist nur verletzter Stolz. Der Autor hat sich bemüht, umfassende Tests zu schreiben, hat gute Arbeit geleistet - und das hier gewählte Beispiel ist sehr einfach. Die Realität ist, dass die meisten Praktiker nicht perfekt sind und Dinge übersehen werden. Solche Fehler können mit mutationsgetriebenem Testen erkannt und behoben werden.

Lassen Sie uns also das Beispiel ansehen, es geht um das Erstellen eines einfachen String-basierten Rechners. Der Einfachheit halber betrachten wir nur die ersten drei Anforderungen des Beispiels, zusammen mit ihren Implementierungen und Tests.

Die Anforderungen:

  • Die Methode kann 0, 1 oder 2 durch ein Komma getrennte Zahlen annehmen.
  • Bei einer leeren Zeichenkette gibt die Methode 0 zurück.
  • Die Methode gibt die Summe der Zahlen zurück.

Die Tests:

  1. private static final TddExample EXAMPLE = new TddExample();
  2.  
  3. @Test(expected = RuntimeException.class)
  4. public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
  5. EXAMPLE.add("1,2,3");
  6. }
  7.  
  8. @Test
  9. public final void when2NumbersAreUsedThenNoExceptionIsThrown() {
  10. EXAMPLE.add("1,2");
  11. Assert.assertTrue(true);
  12. }
  13.  
  14. @Test(expected = RuntimeException.class)
  15. public final void whenNonNumberIsUsedThenExceptionIsThrown() {
  16. EXAMPLE.add("1,X");
  17. }
  18.  
  19. @Test
  20. public final void whenEmptyStringIsUsedThenReturnValueIs0() {
  21. Assert.assertEquals(0, EXAMPLE.add(""));
  22. }
  23.  
  24. @Test
  25. public final void whenOneNumberIsUsedThenReturnValueIsThatSameNumber() {
  26. Assert.assertEquals(3, EXAMPLE.add("3"));
  27. }
  28.  
  29. @Test
  30. public final void whenTwoNumbersAreUsedThenReturnValueIsTheirSum() {
  31. Assert.assertEquals(3+6, EXAMPLE.add("3,6"));
  32. }

Die Implementierung:

  1. public int add(final String numbers) {
  2. int returnValue = 0;
  3. String[] numbersArray = numbers.split(",");
  4. if (numbersArray.length > 2) {
  5. throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
  6. }
  7. for (String number : numbersArray) {
  8. if (!number.trim().isEmpty()) { // After refactoring
  9. returnValue += Integer.parseInt(number);
  10. }
  11. }
  12. return returnValue;
  13. }

Das scheint eine gute Testsuite zu sein, die alle Funktionen abdeckt. Aber wie gut ist sie im Vergleich zu mutationsgetriebenen Tests? In der realen Welt würde ich jetzt einen Fehler nach dem anderen einsetzen und die Tests nach jedem einzelnen ausführen. Der Einfachheit halber setzen wir hier jedoch alle relevanten Bugs auf einmal ein.

Mutation 1: Leer vs. Leerzeichen

  1. if (!number.trim().isEmpty())

Das scheint zu einfach, aber genau deswegen lohnt es sich hinzuschauen. Denn: Was wäre, wenn wir den trim()-Aufruf aus Versehen aus unserer Implementierung entfernen würden? So was kann passieren.

  1. if (!number.isEmpty())

Mutation 2: Rückgabe von 0 für einen leeren String

  1. if (!number.trim().isEmpty()) { // After refactoring
  2. returnValue += Integer.parseInt(number);
  3. }

Die Anforderung besagt, dass bei einer leeren Zeichenkette 0 zurückgegeben werden soll. Basierend auf der Implementierung des Autors bedeutet das vermutlich, dass jeder leere Substring als 0 behandelt werden soll, während vorangegangene nicht-leere Substrings weiterhin summiert werden sollen. Was aber, wenn die Implementierung etwas anderes macht und 0 zurückgibt, sobald sie eine leere Zeichenkette sieht?

  1. if (number.trim().isEmpty()) { return 0; }
  2. returnValue += Double.parseDouble(number);

Mutation 3: Drei Eingaben sind nicht erlaubt

  1. if (numbersArray.length > 2) {
  2. throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
  3. }

Die Anforderung besagt, dass die Methode "0, 1 oder 2 Zahlen" annehmen kann. Sollten wir also vielleicht auch auf drei Zahlen prüfen und dann eine Exception auslösen?

Klar: Das ist ein ziemlich dämlicher Fehler, aber man sollte nie unterschätzen, wie kreativ Menschen beim Fehlermachen sein können.

  1. if (numbersArray.length == 3) {
  2. throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
  3. }

Mutation 4: Double vs. Int

  1. returnValue += Integer.parseInt(number);

Bei einem "String -> Numerisch"-Rechner mit Integer-Endergebnis gibt es viele Möglichkeiten, String-Konvertierungen zu implementieren:

1. String in Int konvertieren, Int-Operationen durchführen, ein Int-Ergebnis zurückgeben. Lösen Sie eine Exception aus, wenn der Eingabestring kein Int ist.

2. String in Double konvertieren, Double in Int umwandeln, Int-Operationen durchführen, Int-Ergebnis zurückgeben.

3. String in Double konvertieren, Double-Operationen ausführen, Endergebnis in Int umwandeln und zurückgeben.

Die drei Ansätze erzeugen völlig unterschiedliche Ausgaben, wenn etwa "1.5, 1.5" eingegeben wird. Im Beispiel hat der Autor Option 1 implementiert. Nehmen wir an, das ist tatsächlich das gewünschte Verhalten. Aber was, wenn er fälschlicherweise Option 3 implementiert hätte?

  1. returnValue += Double.parseDouble(number);

Bitte aktivieren Sie Javascript.
Oder nutzen Sie das Golem-pur-Angebot
und lesen Golem.de
  • ohne Werbung
  • mit ausgeschaltetem Javascript
  • mit RSS-Volltext-Feed
 Softwareentwicklung: Wenn testgetriebene Entwicklung einfach nicht gut genug istUnd jetzt fügen wir alles zusammen 
  1.  
  2. 1
  3. 2
  4. 3
  5.  


Robert.Mas 21. Apr 2021

Du meinst so wie hier im Artikel: "Es gibt übrigens Werkzeuge(Link: https://pitest.org...

kayozz 21. Apr 2021

+1 Allein dadurch, dass ich von außen einen Test schreibe, und danach die...

Steffo 20. Apr 2021

Sagt er doch gar nicht: "1. Erreichen Sie einen Zustand, in dem Sie sowohl Code als auch...

Steffo 20. Apr 2021

Alles, was du hier ansprichst, behandelt doch der Artikel. Dadurch lassen sich natürlich...



Aktuell auf der Startseite von Golem.de
Beamer im Test
Mini-Projektoren, die nicht Mist sind

Sie sind kompakter und günstiger als große Heimkinoprojektoren. Unser Test von vier Mini-Projektoren zeigt, dass einige inzwischen auch fast so gut sind.
Ein Test von Martin Wolf

Beamer im Test: Mini-Projektoren, die nicht Mist sind
Artikel
  1. Gegen Drohnen: Kawasaki stellt mobile Laserwaffe vor
    Gegen Drohnen
    Kawasaki stellt mobile Laserwaffe vor

    Die Laserkanone, die auf einem kleinen Geländefahrzeug montiert ist, schießt Drohnen aus 100 Metern Entfernung ab.

  2. Gigabyte Brix: In flache Mini-PCs passen Alder Lake und zwei SSDs
    Gigabyte Brix
    In flache Mini-PCs passen Alder Lake und zwei SSDs

    Gigabyte gestaltet die Gehäuse der Brix-PCs so um, dass sie wesentlich flacher sind. Auch gibt es neue Intel-Prozessoren.

  3. 3D-Drucker: Der Prusa MK4 ist da
    3D-Drucker
    Der Prusa MK4 ist da

    Mit dem Prusa MK4 hat der Hersteller viele Elemente verbessert oder komplett ausgetauscht. Der 3D-Drucker kalibriert sich etwa automatisch.

Du willst dich mit Golem.de beruflich verändern oder weiterbilden?
Zum Stellenmarkt
Zur Akademie
Zum Coaching
  • Schnäppchen, Rabatte und Top-Angebote
    Die besten Deals des Tages
    • Daily Deals • MindStar: Gigabyte RTX 4080 1.229€ statt 1.299€, Intel Core i9-12900K 399€ statt 474€ • SSDs & Festplatten bis -60% • AOC 34" UWQHD 279€ • Xbox-Controller & Konsolen-Bundles bis -27% • Windows Week • 3 Spiele kaufen, 2 zahlen [Werbung]
    •  /