Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 21 Testen mit JUnit
Pfeil 21.1 Softwaretests
Pfeil 21.1.1 Vorgehen beim Schreiben von Testfällen
Pfeil 21.2 Das Test-Framework JUnit
Pfeil 21.2.1 Test-Driven Development und Test-First
Pfeil 21.2.2 Testen, implementieren, testen, implementieren, testen, freuen
Pfeil 21.2.3 JUnit-Tests ausführen
Pfeil 21.2.4 assertXXX(…)-Methoden der Klasse Assert
Pfeil 21.2.5 Matcher-Objekte und Hamcrest
Pfeil 21.2.6 Exceptions testen
Pfeil 21.2.7 Tests ignorieren und Grenzen für Ausführungszeiten festlegen
Pfeil 21.2.8 Mit Methoden der Assume-Klasse Tests abbrechen
Pfeil 21.3 Wie gutes Design das Testen ermöglicht
Pfeil 21.4 Aufbau größerer Testfälle
Pfeil 21.4.1 Fixtures
Pfeil 21.4.2 Sammlungen von Testklassen und Klassenorganisation
Pfeil 21.5 Dummy, Fake, Stub und Mock
Pfeil 21.6 JUnit-Erweiterungen, Testzusätze
Pfeil 21.7 Zum Weiterlesen
 

Zum Seitenanfang

21.2Das Test-Framework JUnit Zur vorigen ÜberschriftZur nächsten Überschrift

Oracle definiert kein allgemeines Standard-Framework zur Definition von Unit-Testfällen, und es bietet auch keine Ablaufumgebung für Testfälle. Diese Lücke füllen Test-Frameworks, wobei das populärste im Java-Bereich das freie quelloffene JUnit (http://www.junit.org) ist; mehr als 60 % aller quelloffenen Projekte unter GitHub referenzieren diese Bibliothek. Das JUnit-Framework wurde im Jahr 2000 von Kent Beck und Erich Gamma entwickelt, die aktuellen Änderungen kommen von diversen Entwicklern.

Um die aktuellste Version von JUnit im Klassenpfad zu haben (IDEs tendieren zu älteren Versionen, auch wenn sich JUnit nicht in Lichtgeschwindigkeit entwickelt), muss von der Download-Seite der JUnit-Website, die auf http://sourceforge.net/projects/junit/files/junit weiterleitet, bzw. vom Maven-Archiv unter http://mvnrepository.com/artifact/junit/junit das entsprechende JAR-Archiv geladen (etwa junit-dep-4.10.jar) und in den Klassenpfad aufgenommen werden.

Die Standard-IDEs Eclipse, NetBeans und IntelliJ bringen JUnit gleich mit und bieten Wizards zum einfachen Erstellen von Testfällen aus vorhandenen Klassen an. Über Tastendruck lassen sich Testfälle abarbeiten, und ein farbiger Balken zeigt direkt an, ob wir unsere Arbeit gut gemacht haben.

 

Zum Seitenanfang

21.2.1Test-Driven Development und Test-First Zur vorigen ÜberschriftZur nächsten Überschrift

Unser JUnit-Beispiel wollen wir nach einem ganz speziellen Ansatz entwickeln, der sich Test-First nennt. Dabei wird der Testfall noch vor der eigentlichen Implementierung geschrieben. Die Reihenfolge mit dem Test-First-Ansatz sieht (etwas erweitert) so aus:

  1. Überlege, welche Klasse und Methode geschrieben werden soll. Lege Quellcode für die Klasse und für die Variablen/Methoden/Konstruktoren an, sodass sich die Kompilationseinheit übersetzen lässt. Die Codeblöcke sind leer, enthalten aber mitunter eine return-Anweisung, sodass die Typen »da« sind, aber keine Funktionalität besitzen.

  2. Schreibe die API-Dokumentation für die Methoden/Konstruktoren, und überlege, welche Parameter, Rückgaben und Ausnahmen nötig sind.

  3. Teste die API an einem Beispiel, das zeigt, ob sich die Klasse mit Eigenschaften »natürlich« anfühlt. Falls nötig, wechsle zu Punkt 1, und passe die Eigenschaften an.

  4. Implementiere eine Testklasse.

  5. Implementiere die Logik des eigentlichen Programms.

  6. Gibt es durch die Implementierung neue Dinge, die ein Testfall testen sollte? Wenn ja, erweitere den Testfall.

  7. Führe die Tests aus, und wiederhole Schritt 5, bis alles fehlerfrei läuft.

Der Test-First-Ansatz hat den großen Vorteil, dass er überschnelle Entwickler, die, ohne groß zu denken, zur Tastatur greifen, dann implementieren und nach 20 Minuten wieder alles ändern, zum Nachdenken zwingt. Große Änderungen kosten Zeit und somit Geld, und Test-First verringert die Notwendigkeit späterer Änderungen. Denn wenn Entwickler Zeit in die API-Dokumentation investieren und Testfälle schreiben, dann haben sie eine sehr gute Vorstellung von der Arbeitsweise der Klasse, und große Änderungen sind seltener.

Der Test-First-Ansatz ist eine Anwendung von Test-Driven Development (TDD). Hier geht es darum, die Testbarkeit gleich als Ziel bei der Softwareentwicklung zu definieren. Hieran krankten frühere Entwicklungsmodelle, etwa das wohlbekannte Wasserfallmodell, das das Testen an das Ende – nach Analyse, Design und Implementierung – stellte. Die Konsequenz dieser Reihenfolge war oft ein großer Klumpen Programmcode, der unmöglich zu testen war. Mit TDD soll das nicht mehr passieren. Heutzutage sollten sich Entwickler bei jeder Architektur, jedem Design und jeder Klasse gleich zu Beginn überlegen, wie das Ergebnis zu testen ist. Untersuchungen zeigen: Mit TDD ist das Design signifikant besser.

Zu der Frage, wann Tests durchgeführt werden sollen, lässt sich nur eines festhalten: so oft wie möglich. Denn je eher ein Test durch eine falsche Programmänderung fehlschlägt, desto eher kann der Fehler behoben werden. Gute Zeitpunkte sind daher vor und hinter größeren Designänderungen und auf jeden Fall vor dem Einpflegen in die Versionsverwaltung. Im modernen Entwicklungsprozess gibt es einen Rechner, auf dem eine Software zur kontinuierlichen Integration läuft (engl. continuous integration). Diese Systeme integrieren einen Build-Server, der die Quellen automatisch aus einer Versionsverwaltung auscheckt, compiliert und dann Testfälle und weitere Metriken laufen lässt. Diese Software übernimmt dann einen Integrationstest, da hier alle Module der Software zu einer Gesamtheit zusammengebaut werden und so Probleme aufgezeigt werden, die sich vielleicht bei isolierten Tests auf den Entwicklermaschinen nicht zeigen.

 

Zum Seitenanfang

21.2.2Testen, implementieren, testen, implementieren, testen, freuen Zur vorigen ÜberschriftZur nächsten Überschrift

Bisher bietet Java keine einfache Funktion, um Strings umzudrehen. Unser erstes JUnit-Beispiel soll daher um eine Klasse StringUtils gestrickt werden, die eine statische Methode reverse() anbietet. Nach dem TDD-Ansatz implementieren wir eine Klasse und die Methode, sodass korrekt übersetzt werden kann, aber alles zunächst ohne Funktionalität ist. (Auf die API-Dokumentation verzichtet das Beispiel.)

Listing 21.1com/tutego/insel/junit/util/StringUtils.java, StringUtils

public class StringUtils

{

public static String reverse( String string )

{

return null;

}

}

Gegen diese eigene API lässt sich nun der Testfall schreiben. Spontan fällt uns ein, dass ein Leer-String umgedreht natürlich auch einen Leer-String ergibt und die Zeichenkette »abc« daher umgedreht »cba« ergibt. Unser Ziel ist es, eine möglichst gute Abdeckung aller Fälle zu bekommen. Wenn wir Fallunterscheidungen im Programmcode vermuten, sollten wir versuchen, so viele Testfälle zu finden, dass alle diese Fallunterscheidungen abgelaufen werden. Interessant sind beim Eingeben immer Sonderfälle bzw. Grenzen von Wertebereichen. (Unsere Methode gibt da nicht viel her, aber wenn wir etwa eine Substring-Funktion haben, lassen sich schnell viele Methodenübergaben finden, die interessant sind.)

Listing 21.2com/tutego/insel/junit/util/StringUtilsTest.java

package com.tutego.insel.junit.util;

import static org.junit.Assert.*;

import org.junit.Test;

public class StringUtilsTest

{

@Test

public void testReverse()

{

assertEquals( "", StringUtils.reverse( "" ) );

assertEquals( "cba", StringUtils.reverse( "abc" ) );

}

}

Die Klasse zeigt vier Besonderheiten:

  1. Die Methoden, die sich einzelne Szenarien vornehmen und die Klassen/Methoden testen, tragen die Annotation @Test.

  2. Eine übliche Namenskonvention (obwohl sie nicht zwingend nötig ist) ist, dass die Methode, die den Test enthält, mit dem Präfix »test« beginnt und mit dem Namen der Methode endet, die sie testet. Da in unserem Fall die Methode reverse(…) getestet wird, heißt die Testmethode dementsprechend »testReverse«. Eine testXXX()-Methode liefert nie eine Rückgabe. Testmethoden können auch ganze Szenarien testen, die nicht unbedingt an einer Methode festzumachen sind, aber hier testet testReverse() nur die reverse(…)-Methode.

  3. JUnit bietet eine Reihe von assertXXX(…)-Methoden, die den erwarteten Zustand mit dem Ist-Zustand vergleichen; gibt es Abweichungen, folgt eine Ausnahme. assertEquals(…) nimmt einen equals(…)-Vergleich der beiden Objekte vor. Wenn demnach StringUtils.reverse("") die leere Zeichenkette "" liefert, ist alles in Ordnung, und der Test wird fortgesetzt.

  4. Der statische Import aller statischen Eigenschaften der Klasse org.junit.Assert kürzt die Schreibweise ab, sodass im Programm statt Assert.assertEquals(…) nur assertEquals(…) geschrieben werden kann.

 

Zum Seitenanfang

21.2.3JUnit-Tests ausführen Zur vorigen ÜberschriftZur nächsten Überschrift

In einer Entwicklungsumgebung lässt sich die Testausführung leicht ausführen. Eclipse zeigt zum Beispiel die Ergebnisse in der JUnit-View an und bietet mit einem grünen oder roten Balken direktes visuelles Feedback.

Natürlich lassen sich die Tests auch von der Kommandozeile ausführen, obwohl das selten ist, denn in der Regel werden die Tests im Zuge eines Build-Prozesses – den etwa Ant steuert – angestoßen. Wer das dennoch über die Kommandozeile machen möchte, der schreibt:

Listing 21.3com/tutego/insel/junit/RunTest.java

package com.tutego.insel.junit;

import org.junit.runner.JUnitCore;

import com.tutego.insel.junit.util.StringUtilsTest;

public class RunTest

{

public static void main( String[] args )

{

JUnitCore.main( StringUtilsTest.class.getName() );

}

}
 

Zum Seitenanfang

21.2.4assertXXX(…)-Methoden der Klasse Assert Zur vorigen ÜberschriftZur nächsten Überschrift

Assert ist die Klasse mit den assertXXX(…)-Methoden, die immer dann einen AssertionError auslösen, wenn ein aktueller Wert nicht so wie der gewünschte war. Der JUnit-Runner registriert alle AssertionErrors und speichert sie für die Statistik. Bis auf drei Ausnahmen beginnen alle Methoden der Klasse Assert mit dem Präfix assert – zwei andere heißen fail(…) und eine heißt isArray(…). Die assertXXX(…)-Methoden gibt es einmal mit einer Testmeldung, die dann erscheint, wenn JUnit extra eine Meldung angeben soll, und einmal ohne, wenn keine Extrameldung gefragt ist.

Eigentlich reicht zum Testen die Methode assertTrue(boolean condition) aus. Ist die Bedingung wahr, so ist alles in Ordnung. Wenn nicht, gibt es den AssertionError:

org.junit.Assert
  • static void assertTrue(boolean condition)

  • static void assertTrue(String message, boolean condition)

  • static void assertFalse(boolean condition)

  • static void assertFalse(String message, boolean condition)

Um es Entwicklern etwas komfortabler zu machen, bietet JUnit sechs Kategorien an Hilfsmethoden. Zunächst sind es assertNull(…) und assertNotNull(…), die testen, ob das Argument null bzw. nicht null ist. Ein Aufruf von assertNull(Object object) ist dann nichts anderes als assertTrue(object == null):

  • static void assertNotNull(Object object)

  • static void assertNotNull(String message, Object object)

  • static void assertNull(Object object)

  • static void assertNull(String message, Object object)

Die nächste Kategorie testet, ob das Objekt identisch (nicht equals(…)-gleich) mit einem anderen Objekt ist:

  • static void assertNotSame(Object unexpected, Object actual)

  • static void assertNotSame(String message, Object unexpected, Object actual)

  • static void assertSame(Object expected, Object actual)

  • static void assertSame(String message, Object expected, Object actual)

Statt eines Referenztests führen die folgenden Methoden einen equals(…)-Vergleich durch:

  • static void assertEquals(Object expected, Object actual)

  • static void assertEquals(String message, Object expected, Object actual)

Zum Testen von primitiven Datentypen gibt es im Grunde nur drei Methoden. (Das reicht, denn zum einen werden andere primitive Typen automatisch typangepasst, zum anderen kommt dann Boxing ins Spiel, sodass assertEquals(Object, Object) wieder passt.)

  • static void assertEquals(long expected, long actual)

  • static void assertEquals(String message, long expected, long actual)

  • static void assertEquals(float|double expected, float|double actual, float|double delta)

  • static void assertEquals(String message, float|double expected, float|double actual, float|double delta)

Fließkommazahlen bekommen bei assertEquals(…) einen Delta-Wert, in dem sich das Ergebnis bewegen muss. Das trägt der Tatsache Rechnung, dass vielleicht in der Bildschirmausgabe zwei Zahlen gleich sind, jedoch nicht bitweise gleich sind, da sich kleine Rechenfehler akkumuliert haben. Sind jedoch die Fließkommazahlen in einem Wrapper, also etwa Double, verpackt, leitet ja assertEquals(…) den Test nur an die equals(…)-Methode der Wrapper-Klasse weiter, die natürlich kein Delta berücksichtigt.

Als Letztes folgen Methoden, die Array-Inhalte vergleichen:

  • static void assertArrayEquals(byte[] expecteds, byte[] actuals)

  • static void assertArrayEquals(String message, byte[] expecteds, byte[] actuals)

  • static void assertArrayEquals(char[] expecteds, char[] actuals)

  • static void assertArrayEquals(String message, char[] expecteds, char[] actuals)

  • static void assertArrayEquals(int[] expecteds, int[] actuals)

  • static void assertArrayEquals(String message, int[] expecteds, int[] actuals)

  • static void assertArrayEquals(long[] expecteds, long[] actuals)

  • static void assertArrayEquals(Object[] expecteds, Object[] actuals)

  • static void assertArrayEquals(String message, long[] expecteds, long[] actuals)

  • static void assertArrayEquals(String message, Object[] expecteds, Object[] actuals)

  • static void assertArrayEquals(short[] expecteds, short[] actuals)

  • static void assertArrayEquals(String message, short[] expecteds, short[] actuals)

Neben den assertEquals(…)-Methoden gibt es für einige Varianten Negationen:

  • static void assertNotEquals(long unexpected, long actual)

  • static void assertNotEquals(float unexpected, float actual, float delta)

  • static void assertNotEquals(double unexpected, double actual, double delta)

  • static void assertNotEquals(Object unexpected, Object actual)

  • static void assertNotEquals(String message, long unexpected, long actual)

  • static void assertNotEquals(String message, float unexpected, float actual, float delta)

  • static void assertNotEquals(String message, double unexpected, double actual, double delta)

  • static void assertNotEquals(String message, Object unexpected, Object actual)

 

Zum Seitenanfang

21.2.5Matcher-Objekte und Hamcrest Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Sonderrolle nehmen zwei assertThat(…)-Methoden ein. Sie ermöglichen es, Tests deklarativer zu schreiben, sodass sie sich wie englische Sätze lesen lassen. Stellen wir bei einigen Beispielen assertThat(…) und assertEquals(…) bei den gleichen Aufgaben gegenüber:

assertXXX(…) ohne Matcher

assertThat(…)

assertNotNull(new Object());

assertThat(new Object(), is(notNullValue()));

assertEquals("", StringUtils.reverse(""));

assertThat("", is(equalTo(StringUtils.reverse( ""))));

assertSame("", "");

assertThat("", is(sameInstance( "" )));

assertNotSame("", "a");

assertThat("a", is(not(sameInstance(""))));

Tabelle 21.1Vergleich der assertXXX(…)- und assertThat(…)-Methoden

Zunächst fällt auf, dass das erste Argument bei assertThat(…) den Wert beschreibt, den wir haben, der aber bei den sonstigen assertXXX(…)-Methoden immer erst hinter dem erwarteten Wert folgt. Das zweite Argument ist ein besonderes Matcher-Objekt, das die Bedingung kodiert.

Die allgemeine Syntax von assertThat(…) ist folgende:

org.junit.Assert
  • static <T> void assertThat(T actual, org.hamcrest.Matcher<T> matcher)

  • static <T> void assertThat(String reason, T actual, org.hamcrest.Matcher<T> matcher)

Hamcrest

Ob der Test korrekt ist oder nicht, entscheidet ein org.hamcrest.Matcher-Objekt. An dem anderen Paket, org.hamcrest, lässt sich schon ablesen, dass die Bibliotheken wechseln. JUnit hängt hier eine Bibliothek namens Hamcrest ein, die unter http://hamcrest.org/JavaHamcrest beschrieben ist. JUnit referenziert im Bytecode nur den Typ, aber im JUnit-JAR befindet sich weder eine Matcher-Implementierung noch der Bytecode der Schnittstelle. Um die volle Funktionalität von Hamcrest zu nutzen, muss von der Maven-Webseite http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22java-hamcrest%22 das Java-Archiv java-hamcrest-2.x.y.z.jar geladen und im Klassenpfad eingebunden werden.

Sehen wir uns ein Beispiel an:

Listing 21.4com/tutego/insel/junit/hamcrest/HamcrestDemo.java, HamcrestDemo

public class HamcrestDemo

{

@Test

public void testHamcrestMatcher()

{

assertThat( null + "", is( notNullValue() ) );

assertThat( "", is( equalTo( "" ) ) );

assertThat( 1L, is( sameInstance( Long.valueOf( 1 ) ) ) );

assertThat( 1000L, is( not( sameInstance( Long.valueOf( 1000 ) ) ) ) );

}

}

Da die Matcher über eine clevere Art verschachtelt werden, lesen sich die assertThat(…)-Aufrufe wie Sätze. is(…) hat funktional keine Bedeutung, lässt die Aussage aber noch »englischer« werden. Aber wo sind bei einem Aufruf wie assertThat("", not(sameInstance("a"))); die Objekte? Zunächst gilt, dass die »Wörter« wie »not« und »equalTo« statische Methoden der Klasse org.hamcrest.CoreMatchers sind. Werden diese Methoden statisch eingebunden, ergibt sich die kurze Schreibweise, sonst ist sie lang und lautet:

CoreMatchers.not(CoreMatchers.sameInstance("a"))

Die statischen Methoden geben Matcher-Objekte zurück. Wir hätten auch new IsNot<String> (new IsSame<String>("a")) schreiben können, und dann hätten wir auch wieder das Matcher-Objekt, aber die statischen Methoden sind kürzer als der Konstruktoraufruf. Alle Matcher-Objekte besitzen eine Methode boolean matches(Object item), die letztendlich den Test durchführt und assertThat(…) sagt, ob eine Ausnahme ausgelöst werden muss, weil ein Fehler auftrat.

CoreMatchers-Methoden

Die Methoden is(…), isInstance(…), not(…), notNullValue(…), equalTo(…) sind nicht die einzigen aus CoreMatchers. Die folgende Übersicht zählt die aktuellen statischen Methoden auf:

org.hamcrest.CoreMatchers
  • static <T> Matcher<T> allOf(Iterable<Matcher<? extends T>> matchers)

  • static <T> Matcher<T> allOf(Matcher<? extends T>... matchers)

  • static <T> Matcher<T> any(Class<T> type)

  • static <T> Matcher<T> anyOf(Iterable<Matcher<? extends T>> matchers)

  • static <T> Matcher<T> anyOf(Matcher<? extends T>... matchers)

  • static <T> Matcher<T> anything()

  • static <T> Matcher<T> anything(String description)

  • static <T> Matcher<T> describedAs(String description, Matcher<T> matcher,

    Object... values)

  • static <T> Matcher<T> equalTo(T operand)

  • static Matcher<Object> instanceOf(Class<?> type)

  • static Matcher<Object> is(Class<?> type)

  • static <T> Matcher<T> is(Matcher<T> matcher)

  • static <T> Matcher<T> is(T value)

  • static <T> Matcher<T> not(Matcher<T> matcher)

  • static <T> Matcher<T> not(T value)

  • static <T> Matcher<T> notNullValue()

  • static <T> Matcher<T> notNullValue(Class<T> type)

  • static <T> Matcher<T> nullValue()

  • static <T> Matcher<T> nullValue(Class<T> type)

  • static <T> Matcher<T> sameInstance(T object)

Vorteile von Matcher-Objekten

Stellen wir noch einmal

  • assertEquals( "", StringUtils.reverse( "" ) ); und

  • assertThat( "", is( equalTo( StringUtils.reverse( "" ) ) ) );

gegenüber. Ist assertThat(…) die bessere Alternative? Nicht wirklich, denn es gibt keinen Nutzen, wenn assertThat(…) exakt das übernimmt, was die assertXXX(…)-Methode macht, also in unserem Fall einen Gleichheitstest. assertThat(…) ist dann sogar länger.

Interessant wird assertThat(…) aus zwei Gründen:

  1. Es gibt eine große Sammlung an Matcher-Objekten, die einem Programmierer viel Arbeit abnehmen. Wenn es zu prüfen gilt, ob die zu testende Methode eine Datei mit genau 1.111 Bytes angelegt hat, so müsste das in Java etwa new File(file).exists() && new -File( file).length() == 1111 lauten. Mit entsprechenden Hamcrest-Matchern heißt es dann kompakt: allOf(exists(), sized((Matcher<Long>)equalTo(0L))).[ 255 ](Wobei allOf(exists(), sized(0)) noch etwas besser ist – der Autor ist informiert. ) Ein zweites Beispiel betrifft Mengenabfragen. Die praktische Methode hasItems(…) zum Beispiel testet, ob Elemente in einer Collection sind; ohne Matcher wäre der Test in Java mehr Schreibarbeit.

  2. Die assertEquals(…)-Methode läuft entweder durch oder bricht mit einer Exception ab, was den Test dann beendet. Wir bekommen beim Abbruch dann den Hinweis, dass der gewünschte Wert nicht mit dem berechneten übereinstimmt, aber wo genau der Fehler ist, das fällt weniger auf. assertThat(…) liefert ausgezeichnete Fehlermeldungen.

Folgendes Beispiel fasst die beiden Vorteile zusammen: Stellen wir uns vor, wir haben eine Datenstruktur (in diesem Beispiel eine ArrayList). Sie kann Elemente auch entfernen, und das ist genau die Methode removeAll(…), deren Funktionalität wir testen wollen:

List<String> list = new ArrayList<>();

Collections.addAll( list, "a", "b", "c", "d", "e" );

list.removeAll( Arrays.asList( "b", "d" ) );

Wie kann ein Test aussehen? Ein Test könnte schauen, ob die Größe der Liste von fünf Elementen auf 3 kommt, wenn die beiden Elemente »b« und »d« gelöscht werden. Und der Test kann prüfen, ob »b« und »d« wirklich entfernt wurden, aber »a«, »c« und »e« weiterhin in der Liste sind:

Listing 21.5com/tutego/insel/junit/hamcrest/HamcrestCollectionDemo.java, testHamcrestMatcher()

public void testHamcrestMatcher()

{

List<String> list = new ArrayList<>();

Collections.addAll( list, "a", "b", "c", "d", "e" );

list.removeAll( Arrays.asList( "b", "d" ) );

assertThat( list, hasSize(3) );

assertThat( list,

both( hasItems( "a", "c", "e" ) ).and( not( hasItems( "b", "d" ) ) ) );

}

Die Methode hasSize(…) prüft die Größe der Liste, und hasItems(…) testet, ob Elemente in der Datenstruktur sind. Die Kombination both().and() prüft zwei Bedingungen, die beide erfüllt sein müssen. Alternativ wäre auch allOf() möglich.

So machen diese beiden Zeilen den ersten Punkt der beiden Vorteile für Hamcrest-Matcher deutlich. Der zweite Vorteil waren die Meldungen im Fehlerfall. Ändern wir zum Test das erste hasItems(…) in hasItems("_", "c", "e"). Der Testlauf wird dann natürlich einen Fehler geben. Die Meldung ist (etwas eingerückt):

Expected: (

(a collection containing "_" and a collection containing "c" and a collection

containing "e")

and not

(a collection containing "b" and a collection containing "d")

)

got: <[a, c, e]>

Der Hinweis got: <[a, c, e]> gilt für den ersten Matcher und nicht, wie sonst bei assert-Equals(…) üblich, für den gesamten Ausdruck. So sind die Aussagen deutlich lokaler und besagen nicht einfach nur wie bei Anfragen, die auf assertTrue(…) zurückgehen, dass false kam, aber true erwartet wurde.

 

Zum Seitenanfang

21.2.6Exceptions testen Zur vorigen ÜberschriftZur nächsten Überschrift

Während der Implementierung fallen oft Dinge auf, die die eigentliche Implementierung noch nicht berücksichtigt. Dann sollte sofort diese neu gewonnene Erkenntnis in den Testfall einfließen. In unserem Beispiel soll das bedeuten, dass bisher nicht wirklich geklärt ist, was bei einem null-Argument passieren soll. Bisher gibt es eine NullPointerExcpetion, und das ist auch völlig in Ordnung, aber in einem Testfall steht das bisher nicht, dass auch wirklich eine NullPointerExcpetion folgt. Diese Fragestellung legt den Fokus auf eine gern vergessene Seite des Testens, denn Testautoren dürfen sich nicht nur darauf konzentrieren, was die Implementierung denn so alles richtig machen soll – der Test muss auch kontrollieren, ob im Fehlerfall auch dieser korrekt gemeldet wird. Wenn es nicht in der Spezifikation steht, dürfen auf keinen Fall falsche Werte geradegebügelt werden: Falsche Werte müssen immer zu einer Ausnahme oder zu einem wohldefinierten Verhalten führen.

Wir wollen unser Beispiel so erweitern, dass reverse(null) eine IllegalArgumentException auslöst. Auf zwei Arten lässt sich testen, ob die erwartete IllegalArgumentException auch wirklich kommt. Die erste Variante:

Listing 21.6com/tutego/insel/junit/utils/StringUtilsTest.java, Ausschnitt

try

{

StringUtils.reverse( null );

fail( "reverse(null) should throw IllegalArgumentException" );

}

catch ( IllegalArgumentException e ) { }

Führt reverse(null) zur Ausnahme, was ja gewollt ist, dann wird der catch-Block die Illegal-ArgumentException einfach auffangen und ignorieren, und dann geht es in der Testfunktion mit anderen Dingen weiter. Sollte keine Ausnahme folgen, so wird die Anweisung nach dem reverse(…)-Aufruf ausgeführt, und die ist fail(…). Diese Methode löst eine JUnit-Ausnahme mit einer Meldung aus und signalisiert dadurch, dass im Test etwas nicht stimmte. Allerdings bleibt ein Problem: Was ist, wenn zwar eine Ausnahme ausgelöst wird, aber eine falsche? Eine Nicht-RuntimeException kann es nicht sein, denn dann würde der Compiler uns zum catch-Block zwingen. Aber was wäre mit einer anderen RuntimeException, etwa der NullPointerException? Diese würde zwar von JUnit abgefangen werden, und JUnit würde einen Fehler melden, aber dann ist nicht abzulesen, was das für ein Fehler ist und welche Rolle er spielt. Eine Lösung wäre, noch einen catch-Block anzuhängen und fail(…) aufzurufen, doch das würde erst einmal Quellcodeduplizierung bedeuten. Daher bietet JUnit eine andere, eine elegante Variante – die Annotation @Test wird parametrisiert:

Listing 21.7com/tutego/insel/junit/util/StringUtilsTest.java, testReverseException()

@Test( expected = IllegalArgumentException.class )

public void testReverseException()

{

StringUtils.reverse( null );

}

JUnit erwartete (engl. expected) hier eine IllegalArgumentException. Folgt sie nicht, meldet JUnit das als Fehler. Der Vorteil gegenüber der fail(…)-Variante ist die Kürze; ein Nachteil ist, dass dann unter Umständen mehrere testXXX()-Methoden für eine zu testende Methode nötig sind. Wir haben hier eine zweite Methode, testReverseException(), hinzugenommen. (Der reverse(null)-Test hätte auch am Ende der ersten Methode stehen können, dann müssen wir aber sichergehen, dass dieser Exception-Test am Ende durchgeführt wird.)

 

Zum Seitenanfang

21.2.7Tests ignorieren und Grenzen für Ausführungszeiten festlegen Zur vorigen ÜberschriftZur nächsten Überschrift

Durch Umstrukturierung von Quellcode kann es sein, dass Testcode nicht länger gültig ist und entfernt oder umgebaut werden muss. Damit der Testfall nicht ausgeführt wird, muss er nicht auskommentiert werden (das bringt den Nachteil mit sich, dass sich das Refactoring etwa im Zuge einer Umbenennung von Bezeichnern nicht auf auskommentierte Bereiche auswirkt). Stattdessen wird einfach eine weitere Annotation @Ignore an die Methode gesetzt:

@Ignore

@Test

public void testReverse()

Nach großen Refactorings kann die Software funktional noch laufen, aber vielleicht viel langsamer geworden sein. Dann stellt sich die Frage, ob das im Sinne des Anforderungskatalogs noch korrekt ist, wenn ein performantes Programm nach einer Änderung wie eine Schnecke läuft. Um Laufzeitveränderungen als Gültigkeitskriterium einzuführen, kann die Annotation @Test ein timeout in Millisekunden angeben:

@Test( timeout = 500 )

public void test()

Wird die Testmethode dann nicht innerhalb der Schranke ausgeführt, gilt das als fehlgeschlagener Test, und JUnit meldet einen Fehler.

 

Zum Seitenanfang

21.2.8Mit Methoden der Assume-Klasse Tests abbrechen Zur vorigen ÜberschriftZur nächsten Überschrift

Während die assertXXX(…)-Methoden zu einer Ausnahme führen und so anzeigen, dass der Test etwas gefunden hat, das nicht korrekt ist, bietet JUnit mit Assume.assumeXXX(…)-Methoden die Möglichkeit, die Tests nicht fortzuführen. Das ist zum Beispiel dann sinnvoll, wenn die Testausführung nicht möglich ist, etwa weil der Testrechner keine Grafikkarte hat, das Netzwerk nicht da oder das Datensystem voll ist. Dabei geht es nicht darum, zu testen, wie sich die Routine bei einem nicht vorhandenen Netzwerk verhält – das gilt es natürlich auch zu testen. Aber steht das Netzwerk nicht, dann können logischerweise auch keine Tests laufen, die das Netzwerk zwingend benötigen.

org.junit.Assume
  • static void assumeTrue(boolean b)

  • static void assumeNotNull(Object... objects)

  • static void assumeNoException(Throwable t)

  • static <T> void assumeThat(T actual, org.hamcrest.Matcher<T> matcher)

Die assumeXXX(…)-Methoden führen zu keiner Ausnahme, brechen die Testausführung aber ab.

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
 Zum Katalog
Zum Katalog: Java ist auch eine Insel Java ist auch eine Insel

Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?

Ihre Meinung



 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel

Java ist auch eine Insel




Zum Katalog: Java SE 9-Standard-Bibliothek

Java SE 9-Standard-Bibliothek




Zum Katalog: Professionell entwickeln mit Java EE 8

Professionell entwickeln mit Java EE 8




Zum Katalog: Entwurfsmuster

Entwurfsmuster




Zum Katalog: IT-Projektmanagement

IT-Projektmanagement




 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2017

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de