Donnerstag, 18. Dezember 2014
Weitere Testmöglichkeiten mit JUnit

JUnit stellt dem Programmierer weitere interessante Möglichkeiten zur Verfügung. So können Sie in Ihrer Testklasse mit Hilfe bestimmter Methoden Initialisierungen vornehmen, Tests können gruppiert oder kurzzeitig abgeschaltet werden. Außerdem können Sie Timeouts definieren, die darüber bestimmen, wann ein Test fehlgeschlagen ist.

Ein weiteres wichtiges Thema, das Entwickler manchmal gerne übersehen, ist die Tatsache, dass auch Exceptions getestet werden müssen: Wird im richtigen Moment die richtige Ausnahme geworfen? Und wird sie auch korrekt abgefangen? Auch zum Testen von Exceptions stellt JUnit Mechanismen zur Verfügung.

Testen von Exceptions

Natürlich muss beim Testen auch sichergestellt werden, dass an den richtigen Stellen die richtigen Ausnahmen ausgelöst werden. Erinnern wir uns an die Kontoklasse aus dem zweiten Teil des Tutorials: Es gibt eine Methode abbuchen(), die sicherstellen muss, dass der Betrag auf dem Konto nicht unter 0 fällt.Zur Erinnerung sehen Sie im Folgenden noch einmal die Konto-Klasse, mit der gearbeitet werden soll. Es ist noch keine Logik implementiert, denn diese soll ja erst nach den Testfällen programmiert werden, wie Sie im ersten Teil des Tutorials gelernt haben.

public class Konto { 
    private String inhaber; 
    private String kontonr; 
    private float betrag; 
	
    public Konto(String inhaber, 
                 String kontonr, 
                 float betrag) { 	
    }

    public void einzahlen(float betrag) { } 
	
    public void auszahlen(float betrag) { } 
}

In der Methode abbuchen() wird also eine Exception geworfen werden, wenn der Kontobetrag unter 0 fällt. Wir müssen prüfen, ob diese Ausnahme wirklich ausgelöst wird. Falls nicht, handelt es sich um einen Fehler, der unbedingt behoben werden muss. Aber wie testen wir, ob eine Ausnahme wie erwartet geworfen wurde? Mit den bereits vorgestellt equals-Methoden wird das schwierig.

Der nächste Code-Ausschnitt zeigt eine mögliche Test-Methode für die Methode abbuchen().

 
@Test public void testeAuszahlenNegativ() { 
    Konto konto = new Konto("Schmitt", "12345", 500); 
    try 
    { 
        konto.auszahlen(1000); 
        fail("Betrag unter 0 und es wurde keine Ausnahme ausgelöst!"); 
    } catch (Exception e) {
	    // Tun, was getan werden muss.
    } 
}

Wird die Ausnahme wie erwartet ausgelöst, geschieht nichts, da sie abgefangen wird. Wird sie nicht ausgelöst, wird die fail-Methode ausgeführt. Diese löst eine JUnit-Ausnahme mit der entsprechenden Fehlermeldung aus. So wissen wir, dass unser Test nicht funktioniert hat.

Aber auch hier gibt es ein Problem. Was ist, wenn zwar eine Exception ausgelöst wurde, aber die falsche? Wir haben keine Chance herauszufinden, welche Exception fehlerhaft ausgelöst wurde. Wir könnten noch einen try-catch-Block verwenden, aber JUnit bietet eine wesentlich elegantere Lösung: die Annotation @Test wird parametisiert. Was das bedeutet, sehen Sie im folgenden Code-Ausschnitt.

 
@Test(expected = RuntimeException.class) 
public void testeAuszahlenNegativ() { 
    Konto konto = new Konto("Schmitt", "12345", 500); 
    konto.auszahlen(1000); 
}

JUnit erwartet eine RuntimeException. Wird diese Ausnahme ausgelöst, geschieht nichts. Bei einer anderen Ausnahme wird ein AssertionError mit einer Fehlermeldung erzeugt. Der Vorteil liegt in der Kürze des Codes. Wenn Sie in einer Methode jedoch mehrere Exceptions prüfen wollen, kommen Sie mit dieser kürzeren Lösung nicht weit. Hier gilt wie so oft: Es gibt kein Patentrezept, welche Lösung die bessere ist. Es hängt vom Anwendungsfall ab.

Setup- und Teardown-Methoden

Erweitern wir unser Beispiel ein wenig. Neben der Klasse Konto gibt es noch die Klassen Kunde, Mitarbeiter und Bank. Um die Klasse Bank testen zu können, brauchen wir Objekte von Konto, Mitarbeiter und Kunde. Und zwar jeweils mindestens drei. Für jeden einzelnen Test. Wir müssen also jedes Mal die entsprechenden Objekte erzeugen. Dass das eine enorme Menge an unnötigem Code ist, muss ich Ihnen nicht sagen. Und vielleicht müssen am Ende noch irgendwelche Aufräumarbeiten durchgeführt werden: Datenbank oder Dateien schließen, Ressourcen freigeben usw.

Um dieses Problem zu lösen, bietet JUnit zwei Annotationen: @Before und @After. Beide stehen vor Methoden, die vor oder nach jedem Testfall ausgeführt werden sollen. Wir können sie nutzen, um bspw. benötigte Objekte zu erzeugen. Wenn Sie sich unsere Konto-Testmethoden aus dem zweiten Teil des Tutorials ansehen, erkennen Sie, dass wir am Anfang jeder Methode ein Objekt erzeugt haben. Mit diesen beiden Annotationen können wir uns diese Code-Dopplung sparen. Im Folgenden sehen Sie das korrigierte Beispiel.

import static org.junit.Assert.assertEquals; 
import org.junit.Before; 
import org.junit.Test; 

public class KontoTest { 
    private Konto konto; 
	
    @Before 
    public void erzeugeObjekt() { 
        konto = new Konto("Schmitt", "12345", 500); 
    } 
	
    @Test 
    public void testeEinzahlen() { 
        konto.einzahlen(500); 
        assertEquals(konto.getBetrag(), 1000.0, 0.001); 
    } 
	
    @Test public void testeAuszahlen() { 
        konto.auszahlen(300); 
        assertEquals(konto.getBetrag(), 200.0, 0.001); 
    } 
}

Sie sehen, dass die Methode erzeugeObjekt() vor dem Aufruf jeder Testmethode aufgerufen wird. Wir erhalten also immer ein neues Konto-Objekt, das immer die gewünschten Daten hat.

Es gibt aber noch ein weiteres Problem. Manchmal gibt es Test-Setups, die zu aufwändig sind, um sie vor jedem Test neu aufzubauen und nach jedem Test wieder "abzureißen". Dieses Problem wird mit den Annotationen: @BeforeClass und @AfterClass gelöst. Beide stehen vor Methoden, die für eine Testklasse nur ein einziges Mal durchgeführt werden.

Die @BeforeClass-Methode wird also noch vor der ersten @Before-Methode durchgeführt. Beachten Sie, dass die @BeforeClass- und die @AfterClass-Methoden statisch sein müssen. Der folgende Code-Ausschnitt skizziert den Einsatz der Annotationen.

public class KontoTest {
@BeforeClass
public static void sehrAufwaendigesSetup() { }

@AfterClass
public static void gibRessourcenFrei() { }
}

Timeouts

Interessant ist auch die Möglichkeit, Timeouts für Tests zu definieren. Sie können für einen Testfall angeben, wie viele Millisekunden er laufen soll. Bei Überschreitung des Zeitlimits wird der Test abgebrochen. Den Parameter timeout übergeben Sie der Annotation @Test.
public class KontoTest {
@Test(timeout = 1000)
public void laufeMax1000Millisekunden() {
}
}

Tests kurzzeitig ausschalten

JUnit bietet die Möglichkeit, einen Test für kurze Zeit auszuschalten. Dies kann unter bestimmten Umständen interessant sein. Verwenden Sie diese Technik aber nie nach Gutdünken, denn man hat schnell einen ignorierten Test übersehen und einen Fehler nicht korrigiert. Seien Sie mit dem Einsatz der Annotation @Ignore sehr vorsichtig. Die Annotation hat einen Parameter, der einen Kommentar enthält, warum der Test ignoriert wird.

Testfälle gruppieren

Im Allgemeinen wird man die Tests nicht einzeln starten. Interessanter ist es, alle Testfälle zusammenzufassen und sie auf einmal anzustoßen. Wenn Sie 100 Testmethoden einzeln starten müssten, würden Sie letztendlich vermutlich nicht mehr testen als ohne JUnit. Der gesamte Testablauf soll so weit automatisiert werden, dass die Tests ohne manuelles Eingreifen ablaufen können. Dazu liefert JUnit uns die Testsuite mit.

Im Allgemeinen definiert man sich eine Testsuite-Klasse mit einer statischen Methode. Normalerweise werden spezielle Klassen definiert, die nur solche Testsuiten repräsentieren. Testsuiten können selbst weitere Testsuiten enthalten, so dass Sie Hierarchien aufbauen können.

Stellen wir uns wieder das erweiterte Bank-Beispiel vor mit den Klassen Bank, Kunde, Mitarbeiter und Konto. Sie bilden ein TestSuite-Objekt und fügen diesem die verschiedenen Testklassen hinzu. Gehen wir davon aus, dass wir für jede genannte Klasse eine Testklasse haben. Dann bekämen wir folgende Testuite-Klasse (Testsuite-Klassen werden nach Konvention AllTests genannt).

import junit.framework.Test; 
import junit.framework.TestSuite; 

public class AllTests { 
    public static Test suite() { 
        TestSuite suite = new TestSuite(); 
        suite.addTestSuite(KontoTest); 
        suite.addTestSuite(MitarbeiterTest); 
        suite.addTestSuite(KundeTest); 
        suite.addTestSuite(BankTest); 
        return suite; 
    } 
}

Somit ist es ein Leichtes, alle Tests durch den einfachen Aufruf der statischen Methode zu starten.

... link (0 Kommentare)   ... comment


Montag, 10. November 2014
Einführung in JUnit

Einführung in JUnit

Eine wesentliche Änderung gegenüber JUnit 3 ist, dass es keine speziellen Testklassen mehr geben muss. Die Testmethoden, welche die Tests durchführen, können einen beliebigen Namen haben und in einer beliebigen Klasse stehen. Der Übersichtlichkeit halber finde ich jedoch, dass die Tests in eine eigene Klasse gehören, was ich in meinen Beispielen auch so durchführe. Diese Klasse trägt das Suffix Test und die Testmethoden beginnen mit teste, anschließend folgt der Name der zu testenden Methode.

Getestet werden soll eine einfache Konto-Klasse namens Konto. Die Klasse enthält ein Attribut für den Betrag auf dem Konto und jeweils eine Methode zum Abbuchen und zum Einzahlen. Dabei müssen natürlich bestimmte Fälle geprüft werden. So darf bspw. der Kontostand durch Abbuchen nicht unter 0 fallen.

Die Kontoklasse

Zuerst entwerfen wir einen einfachen Prototyp für unsere Kontoklasse. Wir definieren wie im ersten Teil des Tutorials beschrieben die notwendigen Attribute und die Methoden, die getestet werden sollen. Außerdem benötigten wir einen Konstruktor, der alle verpflichtenden Daten entgegen nimmt. Im Folgenden sehen Sie eine einfache minimale Kontoklasse. Der Übersichtlichkeit halber wird auf Getter und Setter verzichtet. Nach dem Prinzip Test First enthält der Prototyp noch keine Funktionalität.

public class Konto { 
	private String inhaber; 
	private String kontonr; 
	private float betrag; 
	public Konto(String inhaber, String kontonr, 
				 float betrag) { } 
	public void einzahlen(float betrag) { } 
	public void auszahlen(float betrag) { } 
}

Unser erster Test

import org.junit.Test; 
import static org.junit.Assert.*; 
public class KontoTest { 
	@Test public void testeEinzahlen() { 
		Konto konto = new Konto("Schmitt", "12345", 500); 
		konto.einzahlen(500); 
		AssertEquals(konto.getBetrag(), 1000.0, 0.001); 
	} 
	
	@Test public void testeAuszahlen() { 
		Konto konto = new Konto("Schmitt", "12345", 500); 
		konto.auszahlen(300); 
		AssertEquals(konto.getBetrag(), 200.0, 0.001); 
	} 
}

Nehmen wir einmal die oben gezeigt Testklasse KontoTest auseinander. Zuerst einmal sehen Sie zwei Imports. Der statische Import importiert die statischen Methoden einer anderen Klasse. Mit der Anweisung in unserer Klasse holen wir uns einfach alle statischen Methoden auf einmal. Somit müssen wir den Aufruf der Methode nicht so schreiben: Assert.AssertEquals()

Die Methoden, die als Testfälle ausgeführt werden sollen, werden mit der Annotation @Test markiert. Die Methoden können einen beliebigen Namen haben, allerdings sollten sie wegen guter Konvention mit dem Präfix test oder teste beginnen. Die Testmethoden müssen parameterlos sein und dürfen keinen Rückgabewert haben.

In der Testmethode selbst legen wir ein neues Konto mit einem Startguthaben von 500 Euro an. Dann zahlen wir weitere 500 Euro ein. Interessant ist die Methode AssertEquals. Sie überprüft, ob zwei Werte gleich sind. Da Gleitkommazahlen niemals vollkommen identisch sind, kann ein Toleranzwert als dritter Parameter angegeben werden. Unsere Anweisung überprüft also, ob der im Konto gespeicherte Betrag gleich 1000 ist, wovon wir bei unserem Test ausgehen sollten. Ist dies nicht der Fall, ist der Test schief gelaufen und in unserer Methode einzahlen() ist wahrscheinlich ein Fehler - möglicherweise aber auch schon im Konstruktor.

Ähnlich läuft die zweite Testmethode an. Wir markieren sie mit @Test, legen ein neues Konto an, führen die Methode auszahlen() aus und prüfen, ob das Konto den erwarteten Betrag von 200 Euro hat.

Die Klasse Assert

Die Klasse Assert enthält statische Methoden, mit denen es möglich ist, Werte und Bedingungen zu testen. Diese Bedingungen müssen erfüllt sein, damit der Test korrekt läuft. Ist dies nicht der Fall wird ein AssertionError ausgelöst. Für uns das Signal, dass während des Tests ein Fehler aufgetreten ist, der korrigiert werden möchte.

In der Klasse Assert sind eine ganze Reihe solcher assert-Methoden definiert, die ich im Folgenden kurz vorstellen möchte.

assertTrue() und assertFalse()

assertTrue(boolean condition) überprüft, ob eine Bedingung wahr ist, assertFalse(boolean condition) überprüft, wie der Name schon sagt, ob eine Bedingung falsch ist. In unserem Konto könnte es eine Methode sperren() geben und einen Getter istGesperrt(). Rückgabe wäre vom Typ boolean und somit durch assertTrue() oder assertFalse() auswertbar:

assertTrue(konto.istGesperrt());

assertArrayEquals

Diese Methoden führen Vergleiche von Feldinhalten durch. Die Syntax lautet entsprechend der assertEquals-Methoden: assertArrayEquals(Object[] expected, Object[] actual)

assertEquals()

Die verschiedenen assertEquals-Methoden prüfen auf Gleichheit zweier Werte. Damit können primitive Datentypen (int, double usw.) auf Gleichheit geprüft werden. Bei zwei Objekten wird ein korrekter equals()-Vergleich und kein Referenzenvergleich durchgeführt. Die Syntax dieser Methoden sieht also allgemein so aus: assertEquals(Object expected, Object actual). Nur bei Gleitkommazahlen muss wie bereits erwähnt noch ein Toleranzwert angegeben werden, da bei der Gleitkommarithmetik zwei Werte im Allgemeinen nicht ganz gleich sind.

assertNull(), assertNotNull() und assertSame()

Diese drei Methoden führen Referenzenvergleiche durch. Die ersten beiden prüfen, ob eine Referenz null ist oder nicht. assertSame(Object expected, Object actual) prüft, ob zwei Referenzen gleich sind. Es wird also kein equals()-Test durchgeführt.

Zu allen hier vorgestellten Methoden gibt es noch eine Variante, bei der als erster Parameter eine Fehlermeldung als String mitgegeben werden kann.

Was passiert, wenn ein Test fehlschlägt?

Der laufende Test wird im Fehlerfall abgebrochen. Im Fehlerfall bedeutet, dass die assert-Methoden die Behauptungen nicht verifizieren konnten. Es wird eine Ausnahme vom Typ AssertionError geworfen. Die Fehler werden in einem speziellen Object, dem Result, gespeichert. Dieses wird einem Runner übergeben, der normalerweise in der IDE integriert ist. Der Runner gibt die Testergebnisse für uns aus.

Daher können Unit-Tests im Allgemeinen sehr einfach durch die IDE ausgeführt werden. BlueJ, Eclipse, Netbeans usw. bieten alle entsprechende Komponenten, um Unit-Tests mit JUnit durchführen und die Ergebnisse bewundern zu können.

... link (0 Kommentare)   ... comment


Samstag, 8. November 2014
Wie kann JUnit 4 das Testen von Software vereinfachen?

Eine der Aufgaben, die Programmierer am wenigstens mögen, ist das Testen. Die Ausreden sind vielfältig:

  • Ich habe keine Zeit zum Testen.
  • Testen ist stupide und langweilig.
  • Mein Code ist praktisch fehlerfrei.
  • Mein Code ist gut genug.
  • usw.

Leider ist Testen ein sehr wichtiger Vorgang, denn nur so kann man sicherstellen, dass der Kunde ein Programm erhält, bei dem keine - eventuell teuren - Nachbesserungen notwendig sind. Die Tatsache, dass die meisten Entwickler dieser wichtigen Tätigkeit nur ungerne nachgehen, haben auch Erich Gamma und Kent Beck erkannt. Daraufhin haben sie JUnit entwickelt.

JUnit ist ein kleines aber mächtiges Framework für Java-Programme. Es ermöglicht das Schreiben und Ausführen von automatischen Tests. Die mit JUnit entwickelten Testfälle sind selbstüberprüfend und damit wiederholbar. Somit entfällt das ständige Eingeben von Testdaten.

Die Tests werden definiert und anschließend können sie immer wieder gestartet werden. Im Idealfall werden Tests nach jedem Kompilieren durchgeführt. Was natürlich in der Praxis nicht getan wird. Mit JUnit ist sogar das möglich, da die Tests ja bereits definiert sind und nur noch gestartet werden müssen.

Download und Installation von JUnit

Die aktuelle Version von JUnit ist 4.12. In der Version 4 hat es gegenüber der Version 3 einige große Änderungen gegeben. So ist das Framework z.B. komplett auf die Annotationen umgestellt worden, die es seit Java 1.5 gibt. Herunterladen können Sie das Framework von der Projektseite.

JUnit befindet sich in einem jar-Archiv. Darin finden Sie auch die Quelltexte, die Dokumentation und weitere interessante Dokumente. Kopieren Sie die datei junit.jar in Ihren Classpath und schon ist das Framework einsatzbereit.

Test first

Eine empfehlenswerte Vorgehensweise ist test first. Das bedeutet, dass die Tests vor der eigentlichen Anwendung entwickelt werden. Der Code soll möglichst einfach zu testen sein. Das hat den Vorteil, dass überschnelle Entwickler, die erst programmieren und dann denken, erst einmal über den Code nachdenken müssen. Sonst müssen sie unter Umständen nach einiger Zeit wieder alles ändern, was für Chaos sorgt und Zeit und Geld kostet. Bei konsequent durchgeführtem test first hat man bereits vor der Entwicklung eine gute Vorstellung von den Klassen und spätere Änderungen sind nicht mehr so häufig notwendig. Außerdem ist eine Klasse, die gut zu testen ist, auch gut zu benutzen. Immerhin wird sie bei den Tests ja benutzt.

Wenn sich der Entwickler bei jeder Klasse sofort überlegt, wie sie am besten zu testen ist, gibt es am Ende keinen Code, der unmöglich zu testen ist. Und da Tests bei der Minimierung von Fehlern helfen sollen, ist das eigentlich im Sinne aller Beteiligten.

Aber wie sollte man nun genau vorgehen? Die nächste Auflistung kann als Anregung für ein gutes Vorgehen betrachtet werden:

  • Klassen und Methoden festlegen, gerne auch mit UML-Klassendiagramm.
  • Klasse und Methoden soweit anlegen, dass Kompilierung möglich ist (ohne Logik).
  • API-Dokumentation schreiben
  • Wenn sich die Klasse nicht "richtig" anfühlt, wird sie ab Schritt 1 noch mal geändert.
  • Eine Testklasse implementieren.
  • Nun wird die Logik der Methoden implementiert.
  • Wenn sich durch die Implementierung neuer zu testender Code ergeben hat, werden die Testfälle erweitert.
  • Tests durchführen und Fehler beheben.

Das klingt auf den ersten Blick nach mehr Arbeit. Allerdings hat sich gezeigt, dass das nur im ersten Moment so ist. Die Investition am Anfang zahlt sich aus, da der Code später fehlerfreier und vor allem besser wartbar ist.

Wann sollen Tests durchgeführt werden? So oft wie möglich! Je eher ein Test durch einen Fehler fehlschlägt, desto eher kann der Fehler behoben und Folgefehler können verhindert werden. Es gilt also: Nach größeren Änderungen wird das gesamte Programm getestet! Und wenn dies nicht automatisch geschieht, geht hier jede Menge Zeit verloren. Folge: Es wird nicht getestet!

Testfälle

Mit JUnit können Sie sich Testfälle definieren. In diesen rufen Sie Methoden auf und lassen JUnit überprüfen, ob die Methoden bestimmte erwartete Ergebniswerte produzieren. Dabei testen Sie wie mit einer Benutzeroberfläche. Dort geben Sie auch bestimmte Testwerte ein und prüfen, ob Sie das erwartete Ergebnis erhalten.

Sie haben z.B. eine Methode add(int wert1, int wert2) geschrieben. Sie können die Methode testen, indem Sie sie folgendermaßen aufrufen: add(5, 9) und kontrollieren, ob Sie den Wert 14 erhalten. Erhalten Sie ein anderes Ergebnis, ist die Methode falsch.

Genauso arbeitet JUnit. Mit einem einzigen Unterschied: Sie geben die Testwerte nicht jedes Mal neu ein, sondern speichern die Testfälle an. Mal angenommen, unsere Methode add() erzeugt das Ergebnis 10. Eindeutig falsch. Nun geht an die Korrektur. Anschließend testen wir die Methode noch einmal mit zwei Werten. Wieder ein falsches Ergebnis. Irgendwann läuft unser Test. Aber wir haben mehrmals Testdaten eingeben müssen. Bei einer Methode zum Addieren ist der Aufwand noch überschaubar, bei größeren Programmen kann dies jedoch schnell nerven. Mit JUnit jedoch erzeugen Sie die Testfälle genau einmal, speichern sie ab und starten sie einfach. Sie erhalten die Testergebnisse und können die Fehler korrigieren. Der Testaufwand ist wesentlich geringer.

Das klingt ja toll! Gibt es so etwas auch für andere Programmiersprachen?

Auch für andere Sprachen gibt es ähnliche Frameworks, da die Notwendigkeit richtigen Testens nicht nur für Java erkannt wurde. Hier eine kleine Auflistung.

  • Java:
    • JUnit
    • TestNG
  • C#:
    • NUnit
    • NCover
    • PartCover
  • Delphi: DUnit
  • PHP:
    • PHPUnit
    • SimpleTest
    • ApacheTest
  • C++:
    • API Sanity Autotest
    • ATF
    • CppUnit

Diese Liste legt keinerlei Anspruch auf Vollständigkeit, sondern soll nur einen kleinen Eindruck von der Vielzahl verschiedener Testframeworks und damit auch der Notwendigkeit solcher Hilfsmittel geben. Auch für Java gibt es andere Test-Frameworks als JUnit wie z.B. TestNG

... link (1 Kommentar)   ... comment