Tutorial: Unit-Tests schreiben
Das Schreiben von Unit-Tests ist eine einfache Sache, es lohnt sich aber das an kleinen Beispielen zu üben bis es locker aus dem Handgelenk kommt. Unit-Tests haben viele Aspekte und Facetten, wir fangen mal mit dem einfachsten Fall an: Testen einer einzelnen Funktion/Methode.
Wähle zunächst eine einfache Funktion. Sie sollte überschaubar sein, aber eine Reihe unterschiedlicher Varianten haben die sich zu testen lohnen. Zum Beispiel:
- Umrechnen von Temperaturen z.B. Celsius-Fahrenheit
- Berechnen der Quersumme einer Zahl
- Fibonacci-Zahle
- Durchschnitt von n Zahlen
Schreibe nun eine Klasse mit einer Methode die die Funktion berechnet.
Normalerweise schreibt man Unit-Tests während man die Funktionalität entwickelt, das ist in dieser Übung aber nicht wichtig. Du kannst die Funktionalität auch durchimplementieren bis Du meinst, daß alles funktioniert und dann die Tests schreiben.
Lege nun eine Unit-Test-Klasse an und schreibe Tests dazu. Lies dazu die Abschnitte "Regeln..." und "Aufbau..." im Spickzettel Unit-Test
Frage Dich dabei
- Was sollen die Tests erreichen?
- Was muß getestet werden, welche Tests benötige ich?
- Wieviele Test sind nötig, wann habe ich genug getestet?
- Woher bekomme ich Testdaten, was ist dabei wichtig?
Unit-Tests nach Abschluß der Implementierung zu schreiben ist nicht empfehlenswert. Zunächst einmal hat dazu kein Enwickler lust. Dementsprechend wird die Qualität –- wenn den überhaupt sinnvolle Tests geschrieben werden. Vor allem aber hat das Programm die Tendenz sich so zu entwickeln, daß es sich gar nicht richtig testen läßt.
Deshalb ist es empfehlenswert, Unit-Tests während der Entwicklung zu schreiben. Das ist auch die Voraussetzung für modernere Programmiertechniken wie agile Entwicklung und TDD.
Aufgabe: Umrechnung römischer in arabische Zahlen
Wir beschränken uns auf die (beliebige) Reihung von Ziffern-Zeichen die addiert werden. Die Ziffern sind: : I = 1 V = 5 X = 10 L = 50 C = 100 D = 500 M = 1000 Bsp: MCCCLXV = 1000 + 3 * 100 + 50 + 10 + 5 = 1365
Die Reihenfolge sei zunächst egal: IV = VI = 6
Der Projekt-Plan
Wir gehen die Aufgabe iterativ an. In jeder Runde fügen wir ein neues Feature hinzu und decken dabei alle Erweiterungen durch Tests ab.
Im Vordergrund steht nicht die Lösung der Aufgabe, sondern das Vorgehen bei der Entwicklung:
Sei Dir bei jedem Schritt bewußt, was Du tust und warum Du es tutst.
Halte Dich an den Plan, auch wenn Du am liebsten alles auf einmal umsetzen möchtest (Lächeln)
Folgende Iterationen sind vorgesehen:
- einzelne Ziffern umrechnen
- Nacheinander wird jede einzelne Ziffer umgerechnet: I V X L C D M
- Die Iteration ist abgeschlossen, wenn das Programm alle sieben Ziffern umrechnen kann.
- Zahlen mit einer festen Zahl (größer eins) von Ziffern umrechnen
- Nacheinander werden Folgen von Ziffern umgewandelt. Beginne mit zwei Ziffern.
- Wenn das Programm für alle 2-Ziffern-Kombination funktioniert wende Dich 3-Ziffern-Kombinationen zu und so fort.
- Überlege Dir dabei, wie Du Dein Programm für beliebig lange Folgen erweitern kannst.
- Gehe zur nächsten Iteration, wenn Du so weit bist.
- Zahlen beliebiger Länge umrechnen
- Das Programm soll nun Zahlenfolgen beliebiger Länge umrechnen können.
- Differenzen berücksichtigen (optional)
- Wenn Du noch nicht genug hast, überlege, wie Du die Differenz-Regel umsetzen kannst.
- Dabei werden kleinere Ziffern, die links einer Größeren stehen abgezogen:
- IV = 4, IX = 9
Vorgehen
Das hier beschriebene Vorgehen wird auch als "Test First" bezeichnet, weil immer erst der Test und anschließend die Implementierung geschrieben wird.
Zur Vorbereitung legst Du eine Klasse mit einer Methode für die Umrechnung an.
Konzentriere Dich auf den jeweiligen Schritt der jeweiligen Iteration. Die erste Methode muß nicht
mit Strings arbeiten, sondern darf auch Zeichen vom Typ char
umrechnen.
Es ist normal, daß Schnittstellen im Laufe der Entwicklung geändert oder erweitert werden.
Als nächstes legst Du dazu eine Test-Klasse an und schreibst den ersten Unit-Test für den ersten Schritt der ersten Iteration. Das Vorgehen wird für jeden Schritt jeder Iteration gleich sein:
- Schreibe einen Test der den Schritt kontrolliert
Der Test ist rot - Erweitere Deine Implementierung um die Funktionalität
Der Test wird grün - Stelle sicher, daß alle anderen Tests grün sind
So verfährst Du bis alles fertig ist.
Variation Test Driven Development (TDD)
Durch Hinzufügen eines weiteren Schritts kann man das Verfahren zu TDD ausweiten.
Gerät durch das sture Hinzufügen einzelner Features der Blick auf das Gesamtprogramm aus dem Fokus, wird das Endergebnis als ein Haufen zusammengeworfener Methoden und Klassen dastehen. Zwar garantieren die Tests, daß das Programm tut was es soll, aber der arme Mensch der das später erweitern soll wird keine Freude daran haben.
Halte nach jedem Schritt (bzw. nach jedem grün gewordenen Test) inne und sieh Dir das Gesamtprodukt an. Überlege Dir:
- Kann ich etwas besser machen?
- Kann ich etwas zusammenfassen?
- Kann ich etwas verallgemeinern?
Setze Maßnahmen, die Dir sinnvoll scheinen um und überprüfe das Ergebnis anhand der Unit-Tests.
Wichtig dabei ist:
Die Änderungen dürfen keine Änderungen an der bestehende Funktionalität mit sich bringen
Dieses Vorgehen –- man nennt es Refactoring –- erfordert Übung und Erfahrung. Zögere nicht, Kollegen hinzuzuziehen, wenn Du damit Schwierigkeiten hast oder Anregungen brauchst.
Frage Dich:
- Wozu dienen Unit-Tests?
- Was gehört zu einem Test?
- Was macht einen guten Test aus?
Im Folgenden sind einige Konventionen für das Schreiben von Unit-Tests zusammengestellt. Es gibt viele verschiedene Meinungen und Ansätze an Unit-Tests heranzugehen; die folgenden Regeln haben sich in der Praxis als eine einfache Variante bewährt um schnell vorwärts zu kommen.
Wichtig ist, daß man sich für ein Vorgehen entscheidet und das dann möglichst konsequent durchzieht, am Besten in Absprache mit dem Entwicklungs-Team mit dem man gerade arbeitet. Merke: Der Unit-Test soll den Entwickler unterstützen und jedem helfen der sie liest. Regeln für Test-Klassen und Test-Methoden
Für jede zu testende Klasse wird eine eigene Test-Klasse mit dem Zusatz Test erstellt,
Zur Klasse TemperaturRechner
erstellt man eine Klasse TemperaturRechnerTest
Alle Test-Methoden haben die gleiche Form (Kein Prä- oder Suffix "Test" am Methodennamen)
@Test public void xx(){...}
Der Bezeichner der Test-Methoden soll in einem Satz beschreiben was getestet wird
und was das erwartete Ergebnis ist. Getestet wird das Interface der zu testenden Klasse:
Alle Methoden mit Ausnahme der private-Methoden werden getestet. Gegebenenfalls kann für einzelne Methode der
zu testenden Klasse eine eigene Test-Klasse angelegt werden, wenn zu viele Tests zusammenkommen. Die
Test-Klasse heißt dann z.B. TemperaturRechnerCelsius2Fahrenheit
Hier ist ein einfaches Beispiel. Die Namensgebung ist hier etwas schwierig, da Methodennamen nicht mit Zahlen beginnen dürfen. Da ist etwas Kreativität gefragt.
@Test public void fahrenheit32ErgibtCelsius0() { TemperaturRechner rechner = new TemperaturRechner(); long celsius = rechner.f2c(32); Assert.assertEquals(clesius, 0); }
Aufbau eines Unit-Tests
Ein Unit-Test hat in der Regel drei Schritte:
- 1. Test vorbereiten
- In diesem Schritt werden Voraussetzungen für den Test geschaffen, zum Beispiel Test-Objekte oder Daten erzeugt.
- in der Literatur wird der Versuchsaufbau auch als Fixture oder Setup bezeichnet.
- Hier werden ggf. auch Voraussetzungen dokumentiert.
- Wenn zum Beispiel ein nicht-leeres Objekt benötigt wird, kann man das mit einem Assert sicherstellen.
- 2. Test durchführen
- In diesem Schritt wird die zu testende Methode in "geeigneter Weise" aufgerufen.
- 3. Ergebnis prüfen
- Am Ende des Unit-Tests muß immer eine Prüfung stehen; ohne Assert ist der Test kein Test!
- Mit einem geeigneten Assert prüft man, ob das gewünschte Ergebnis eingetreten ist.
- Variationen
- Test-Methoden komprimieren
Die drei – im vorangegangenen Abschnitt aufgeführten – Schritte müssen nicht explizit von einander getrennt sein. Kürzere Methoden sind oft leichter zu lesen als lange, erlaubt ist alles, was jeder andere Entwickler ohne Verrenkung verstehen kann. Den Beispiel-Test kann man auch so zusammendampfen:
@Test public void fahrenheit32ErgibtzCelsius0() { Assert.assertEquals(new TemperaturRechner().f2c(32), 0); }
Gemeinsame Test-Objekte
Die meisten Test-Klassen konzentrieren sich auf eine einzelne zu testende Klasse und benötigen oft nur eine einzige Instanz. Ist sie – wie im Beispiel des Temperatur-Rechners – zustandslos, deklariert man sie als Feld und verwendet sie einfach in allen Tests. Das spart jedesmal eine Deklaration. Als Bezeichner kann man z.B. testInstanz oder testObjekt verwenden.
Gelegentlich wird sie auch als "subject under test" bezeichnet und abkürzend mit sut bezeichnet. Solche Abkürzungen sollte man vermeiden, sie sind nicht professionell sondern verwirrend. Der Beispiel-Test sieht dann so aus:
public void TemperaturRechnerTest { TemperaturRechner rechner = new TemperaturRechner(); @Test public void fahrenheit32ErgibtzCelsius0() { Assert.assertEquals(rechner.f2c(32), 0); } }