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.

Stellenmarkt
  1. Prozessmanager IT-Compliance (m/w/d)
    Interhyp Gruppe, München
  2. IT-Spezialist/in mit Softwareentwicklung (w/m/d)
    Investitionsbank Schleswig-Holstein, Kiel
Detailsuche

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:

Golem Akademie
  1. Microsoft Dynamics 365 Guides mit HoloLens 2: virtueller Ein-Tages-Workshop
    16. Februar 2022, Virtuell
  2. Apache Kafka Grundlagen: virtueller Zwei-Tage-Workshop
    21.–22. März 2022, Virtuell
Weitere IT-Trainings

  • 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);

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

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...

Steffo 20. Apr 2021

Klar und kurz vor Release soll dann alles nochmal durchgetestet werden, um dann...



Aktuell auf der Startseite von Golem.de
Microsoft
Sony äußert sich zur Übernahme von Activision Blizzard

Rund 20 Milliarden US-Dollar haben die Aktien von Sony verloren. Nun hat der Konzern erstmals den Kauf von Activision Blizzard kommentiert.

Microsoft: Sony äußert sich zur Übernahme von Activision Blizzard
Artikel
  1. Jochen Homann: Der Regulierer, der nicht regulieren wollte
    Jochen Homann
    Der Regulierer, der nicht regulieren wollte

    Der grüne Verbraucherschützer Klaus Müller kommt, Jochen Homann geht. Eigentlich kann es in der Bundesnetzagentur nur besser werden.
    Ein IMHO von Achim Sawall

  2. Parallel Systems: Ehemalige SpaceX-Mitarbeiter entwickeln neuartige Güterzüge
    Parallel Systems
    Ehemalige SpaceX-Mitarbeiter entwickeln neuartige Güterzüge

    Das Startup Parallel Systems will konventionelle Züge durch modulare Fahrzeuge mit eigenem Antrieb und Energieversorgung ersetzen.

  3. Steam Deck: Valve markiert erste Steam-Spiele für Handheld
    Steam Deck
    Valve markiert erste Steam-Spiele für Handheld

    Portal 2 läuft, The Witcher 3 nur mit Anpassungen: Die ersten Prüfergebnisse für das Steam Deck liegen vor. Fünf Spiele sind durchgefallen.

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 • LG OLED (2021) 40% günstiger (u.a. 65" 1.599€) • WD Black 1TB SSD 94,90€ • Lenovo Laptops (u.a. 17,3" RTX3080 1.599€) • Gigabyte Mainboard 299,82€ • RTX 3090 2.399€ • RTX 3060 Ti 799€ • MindStar (u.a. 32GB DDR5-6000 389€) • Alternate (u.a. Samsung LED TV 50" 549€) [Werbung]
    •  /