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:
private static final TddExample EXAMPLE = new TddExample(); @Test(expected = RuntimeException.class) public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() { EXAMPLE.add("1,2,3"); } @Test public final void when2NumbersAreUsedThenNoExceptionIsThrown() { EXAMPLE.add("1,2"); Assert.assertTrue(true); } @Test(expected = RuntimeException.class) public final void whenNonNumberIsUsedThenExceptionIsThrown() { EXAMPLE.add("1,X"); } @Test public final void whenEmptyStringIsUsedThenReturnValueIs0() { Assert.assertEquals(0, EXAMPLE.add("")); } @Test public final void whenOneNumberIsUsedThenReturnValueIsThatSameNumber() { Assert.assertEquals(3, EXAMPLE.add("3")); } @Test public final void whenTwoNumbersAreUsedThenReturnValueIsTheirSum() { Assert.assertEquals(3+6, EXAMPLE.add("3,6")); }
Die Implementierung:
public int add(final String numbers) { int returnValue = 0; String[] numbersArray = numbers.split(","); if (numbersArray.length > 2) { throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed"); } for (String number : numbersArray) { if (!number.trim().isEmpty()) { // After refactoring returnValue += Integer.parseInt(number); } } return returnValue; }
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
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.
if (!number.isEmpty())
Mutation 2: Rückgabe von 0 für einen leeren String
if (!number.trim().isEmpty()) { // After refactoring returnValue += Integer.parseInt(number); }
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?
if (number.trim().isEmpty()) { return 0; } returnValue += Double.parseDouble(number);
Mutation 3: Drei Eingaben sind nicht erlaubt
if (numbersArray.length > 2) { throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed"); }
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.
if (numbersArray.length == 3) { throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed"); }
Mutation 4: Double vs. Int
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?
returnValue += Double.parseDouble(number);
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 ist | Und jetzt fügen wir alles zusammen |
Du meinst so wie hier im Artikel: "Es gibt übrigens Werkzeuge(Link: https://pitest.org...
+1 Allein dadurch, dass ich von außen einen Test schreibe, und danach die...
Sagt er doch gar nicht: "1. Erreichen Sie einen Zustand, in dem Sie sowohl Code als auch...
Alles, was du hier ansprichst, behandelt doch der Artikel. Dadurch lassen sich natürlich...