Testgetriebene Entwicklung mit JUnit & FIT

Aus THM-Wiki
Wechseln zu: Navigation, Suche

Testgetriebene Entwicklung vereinfacht den Fortschritt, die funktionale Qualität ist durch die Tests abgesichert, und die Änderbarkeit wird durch stetiges Refactoring erreicht. Der Wert einer Software ist umso höher, je besser sie weiterentwickelt werden kann. Ohne Tests wird die Entwicklung stellenweise dem Zufall überlassen. Frameworks wie JUnit oder FIT bieten eine hervorragende Voraussetzung für die, vor allem in komplexen Systemen, sehr wichtigen Tests.

Testgetriebene Entwicklung mit JUnit & FIT

Unit Tests

Softwareprojekte neigen dazu, im Laufe der Zeit unübersichtlich zu werden. Es entstehen viele Abhängigkeiten innerhalb des Codes. Wenn eine neue Funktionalität implementiert oder eine bestehende erweitert wird, kommt ein allgemeines Problem zum Vorschein: Code, der bis hierhin funktioniert hat, versagt möglicherweise. Die Komplexität der Entwicklung ist zu groß als dass ein Programmierer alle Eventualitäten überblicken könnte. Die Auswirkungen können direkt erkennbar sein oder aber später beim Kunden.

Testen ist nicht nur für die Entwickler vorteilhaft, man kann den Kunden in Echtzeit auf dem Laufenden halten über das was die Software bis jetzt zu leisten vermag. Der Kunde kann seinerseits auf diverse Tests Einfluss nehmen, doch hierzu mehr im Abschnitt Akzeptanztests.

Testen wird allgemein gerne vernachlässigt, die Gründe hierfür sind vielfältig: Die häufigste Ursache ist Termindruck, man kann keine Zeit mit Testen verschwenden, da der Kunde wartet. Allerdings sind die Rückschläge, die hierdurch erkauft werden, oft schmerzvoll, besonders wenn Fehler erst spät erkannt werden, die eine Umarrangierung des Codes nötig machen. Jede Änderung kann wiederum neue Probleme schaffen. Ohne eine ausgereifte Testeinrichtung kann nicht alles fehlerfrei bedacht werden.

Test driven development mit JUnit

Um solche Probleme in den Griff zu bekommen bietet sich die "Testgetriebene" Entwicklung an. Ziel hierbei ist es, von beginn an zuerst Testfälle zu schreiben, danach die eigentliche Funktion. Testen erfolgt also zeitnah zur Programmierung. Die Tests müssen automatisierbar sein, und z.B. nach der Kompilierung laufen, um sofort Resultate zu erhalten.

Die drei Direktiven von JUnit:

Jede Programm-Änderung wird durch einen Test motiviert

Der Trick ist durch die geschickte Fragestellung nach den Testfällen herauszufinden, was überhaupt programmiert werden soll. Wenn zuerst der Test existiert, können wir sicherstellen, das wir genau das richtige Programmieren, um den Test erfolgreich abschließen zu können. Es wird nichts Unnötiges entwickelt. Man sollte dies besonders Berücksichtigen, da das Produkt selten bereits am Anfang fertig ist. Testen hilft den richtigen Weg vom Anfang bis zum Ziel zu finden, zu definieren, und erfolgreich zu absolvieren.

Code in die einfache Form bringen

Gut strukturierter Code ist notwendig für jede Art der Weiterentwicklung. Deshalb ist stetiges Refactoring notwendig. Auftretende Probleme werden durch die Tests automatisch aufgedeckt.

Code sooft integrieren wie nötig

Das stellt im Team sicher das aufkommende Fehler schnell erkannt werden und nicht zu noch größeren Problemen heranwachsen.

Das Verfahren lässt sich besonders gut mit eXtreme Programing verbinden. Zwei Programmierer erörtern die Testfälle und ergänzen sich fachlich.

JUnit wurde Initial entwickelt von Kent Beck und Erich Gamma.

Fortschrittsbalken

Sobald die Tests gestartet werden, sucht das Framework automatisch nach allen Testklassen und führt jeweils separat die Testmethoden aus. Angezeigt werden abschließend die Anzahl der Fehler und ein Balken der im Falle von Fehlern rot, bei Fehlerfreiheit grün ist.

Installation

Zu finden ist die Open Source Software als Zip Datei unter http://junit.sf.net. Entpackt ist die Test-Framework Datei junit.jar zu finden, neben Dokumentation, FAQ, etc.

Zur Installation einfach junit.jar in den CLASSPATH einbinden.

Standalone

Wenn JUnit im Standalone Modus aufgerufen werden soll, ist eine main Methode nötig, die einen von drei Runnern startet.

import junit.framework.TestCase;

public class KundeTest extends TestCase {
	public static void main(String[] args) {
		junit.swingui.TestRunner.run(KundeTest.class);
	}
}

Mit Swing:

junit.swingui.TestRunner

Mit AWT:

junit.awtui.TestRunner

Textmodus:

junit.textui.TestRunner

In IDE eingebunden

In Eclipse ist JUnit bereits integriert. Über File -> New kann direkt eine neue Testklasse erzeugt werden. Über Run... kann ein Testlauf hinzugefügt werden. Man kann wählen zwischen einzelnen Testklassen, oder ob alle getestet werden sollen.

Existierender Code

Existierender Code ist schwer im Nachhinein mit Tests zu versehen. Gehen die Entwicklung von Test und Code hand in hand, öffnen sich erst die Wege, die einen leichten Umgang mit dem Code ermöglichen.

Vorteile

Der Wert einer Software ergibt sich aus seiner Funktionalität und Struktur. Beide Faktoren werden sowohl durch die Automatischen Tests, und stetiges Refactoring erhöht.

Konzepte

Test first

Als erstes wird ein Test so geschrieben, dass er fehlschlägt. Deshalb, da die nötige Funktion noch nicht entwickelt wurde. Daraufhin erst, wird Code geschrieben.

Wie der Test erfolgreich abgearbeitet wird

Anfänglich werden die Tests direkt zum Erfolg geführt. Die neu geschriebene Funktion erfüllt einfach direkt die Testbedingung, in dem sie das erwartete Ergebnis einfach ohne Berechnung zurückgibt. "Fake it till' you fix it" ist die Devise. Der Test ist nun erfolgreich installiert, ein Durchlauf ergibt keinen Fehler und der Balken ist grün. Jetzt wird die Funktion implementiert. Sollte nun alles klappen wird der Test ebenso erfolgreich sein. Wenn ein Fehler aufgetreten ist, d.h. eine Assert-Bedingung fehlgeschlagen ist, markiert JUnit den entsprechenden Test als fehlgeschlagen. Der Prozess wiederholt sich solange bis die Funktion entsprechend arbeitet und der Test erfolgreich abgeschlossen wird.

Laufzeitabhängigkeiten von Tests

Besonders interessant ist das Konzept, das jeder Testfall ohne besondere Laufzeitabhängigkeiten entwickelt wird. Es ist jedoch nicht erforderlich, dass das Programm läuft. Jegliche notwendige Initialisierung von Objekten oder Diensten wird im Testfall selbst erledigt. Jeder Testfall läuft autark ab. Ebenso können Ressourcen bei Beendigung des Tests wieder freigegeben werden.

Beispiel

Als Beispiel wird JUnit an der Entwicklung einer kleinen Shop-Logik demonstriert.

Erst die Testklasse

Unser Shop benötigt einen Kunden, der Waren kauft und gerne zahlen möchte. Mit JUnit lassen sich Testfälle einfach realisieren, wir fangen mit einer leeren Testklasse an:

import junit.framework.TestCase;

public class KundeTest extends TestCase {

}

Ein erster durchlauf ergibt einen Error: Kein Test gefunden.

Der Testfall

Der Kunde soll Artikel für einen bestimmten Betrag kaufen können. Die Testfunktion beginnt immer mit "test" gefolgt von einem aussagekräftigen Namen für den Test. Jede Testfunktion ist public und hat keinen Rückgabewert. Tests beginnen mit "assert" gefolgt von dem Typ des Tests. Die verschiedenen Möglichkeiten sind weiter unten aufgeführt. In dem konkreten Beispiel wird ein Kunde angelegt, der zwei Waren kauft. Es wird getestet ob der Gesamtbetrag stimmt. Der Test muss nicht unbedingt kompliziert sein, aber eine Gewährleistung bieten, dass alles wie gewünscht klappt. Später kann sich der Test verfeinern durch Aufgaben wie z.B. Rabattberechnung und Versandkosten. Die Funktion assertEquals vergleicht den berechneten Wert mit dem erwarteten Wert. In diesem Fall inklusive der maximalen Abweichung delta. Bei float und double kann dies nötig sein.

import junit.framework.TestCase;

public class KundeTest extends TestCase {

	public void testArtikelKaufen() {
		Kunde kunde = new Kunde();
		kunde.artikelKaufen(20.99);
		kunde.artikelKaufen(9.95);
		assertEquals(kunde.gesamtBetrag(), 30.94, 0.01);
	}

}

Jetzt kommt ein anderer Fehler. Klasse Kunde nicht gefunden.


Der erste Erfolg

Um zu anfänglichem Erfolg zu gelangen, muss man die fehlende Klasse entwerfen und den Test direkt gelingen lassen.

public class Kunde {
	public Kunde() {
	}
	public void artikelKaufen(double artikelBetrag) {
	}
	public double gesamtBetrag() {
		return 30.94;
	}
}

Ein grüner Testdurchlauf signalisiert, Test erfolgreich.


Funktionalität entwerfen

Natürlich ist es zwar schön wenn alle Tests erfolgreich waren, aber es fehlt eindeutig die Logik. Was genau fehlt können wir jetzt absolut präzise bewerten, erkennen und implementieren, da wir die exakte Funktionsvorgabe durch unseren Test bereits definiert haben. Wir haben eine Semantik entworfen, die uns genau zu der Realisierung unseres Ziels führt.

public class Kunde {
	double betrag;
	public Kunde() {
		betrag = 0;
	}
	public void artikelKaufen(double artikelBetrag) {
		betrag += artikelBetrag;
	}
	public double gesamtBetrag() {
		return betrag;
	}
}

Der Testlauf ergibt wie gewünscht keinen Fehler.

Der nächste Test

Negative Gesamtbeträge stellen Auszahlungen dar, verursacht durch Warenrückgabe. Diese sollen separat behandelt werden.

public class KundeTest extends TestCase {

	...

	public void testArtikelZurueck() {
		Kunde kunde = new Kunde();
		kunde.artikelZurueck(9.95);
		assertEquals(kunde.gesamtBetrag(), -9.95, 0.01);
	}

}

Die Testfunktion zur Warenrückgabe:

public class Kunde {

	...

	public void artikelZurueck(double artikelBetrag) {
		betrag -= artikelBetrag;
	}
}

Tests werden in kleinen Portionen entwickelt, man hangelt sich kontinuierlich an einer testbaren Sicherheitsleine entlang. Wie ein Bergsteiger mit einem Haken sichert man jeden einzelnen funktionalen Punkt durch einen Test ab. Den Weg zum Ziel generiert man sich selbst durch die Festlegung der Tests. Eine kurzfristige Richtung ist also vorgegeben, in diese Richtung wird entwickelt.

Refactoring

Nun ist nicht jeder Weg auch gleichzeitig ein guter Weg. Manchmal muss strukturell etwas verändert werden, um das Ziel zu erreichen. Refactoring ist der Weg um zu einfacheren Code zu finden. Ziel ist es durch strukturelle Funktionsverlagerungen, eine einfachere und besser wartbare Struktur zu erhalten. Wegänderungen sind oft nötig, da nicht vorweg die gesamte Lösung erkannt werden kann. In unserem Beispiel ist es angebracht den Kunden etwas zu entlasten und einen Warenkorb einzuführen. In den Warenkorb fließen Funktionen die der Kunde bisher erledigen musste. Jeder Kunde erhält einen Warenkorb.

Der neue Kundentest:

import junit.framework.TestCase;

public class KundeTest extends TestCase {

	public void testLeerenWarenkorb() {
		Kunde kunde = new Kunde();
		assertTrue(kunde.warenkorbIstLeer());
	}

}


Die neue Klasse Kunde:

public class Kunde {
	Warenkorb warenkorb;
	public Kunde() {
		warenkorb = new Warenkorb();
	}
	public Warenkorb getWarenkorb() {
		return warenkorb;
	}
	public boolean warenkorbIstLeer() {
		if (warenkorb.gesamtArtikel()==0)
			return true;
		else
			return false;
	}
}


Der Warenkorb muss erweitert werden, um die Ausgabe der Anzahl der Waren. Motiviert durch den Test:

import junit.framework.TestCase;

public class WarenkorbTest extends TestCase {

	public void testZweiArtikelKaufen() {
		Warenkorb waren = new Warenkorb();
		waren.artikelKaufen(20.99);
		waren.artikelKaufen(9.95);
		assertEquals(waren.gesamtBetrag(), 30.94, 0.01);
		assertTrue(waren.gesamtArtikel()==2);
	}

	public void testArtikelZurueck() {
		Warenkorb waren = new Warenkorb();
		waren.artikelZurueck(9.95);
		assertEquals(waren.gesamtBetrag(), -9.95, 0.01);
		assertTrue(waren.gesamtArtikel()==1);
	}

}

Implementiert durch:

public class Warenkorb {
	double betrag;
	int anzahlArtikel;
	public Warenkorb() {
		betrag = 0;
		anzahlArtikel = 0;
	}
	public void artikelKaufen(double artikelBetrag) {
		betrag += artikelBetrag;
		anzahlArtikel++;
	}
	public void artikelZurueck(double artikelBetrag) {
		betrag -= artikelBetrag;
		anzahlArtikel++;
	}
	public double gesamtBetrag() {
		return betrag;
	}
	public double gesamtArtikel() {
		return anzahlArtikel;
	}
}

Besonderheiten

Wenn in einem Testfall mehrere Bedingungen geprüft werden (testZweiArtikelKaufen) ist es für JUnit unerheblich welche Bedingung versagt, der Test ist nur dann erfolgreich, wenn alle Bedingungen positiv erfüllt sind. So kann es notwendig werden für jede einzelne Bedingung einen Testfall zu schreiben, um die genaue Ursache herauszufinden.


Fixtures

Sollen verschiedene Aspekte eines Objekts geprüft werden kann dieses Objekt als Instanzvariable angelegt werden und in der setUp() Methode initialisiert werden. tearDown() wird aufgerufen wenn die Tests beendet sind. Hier können ebenso Netzwerk-, oder Datenbankverbindungen aufgebaut werden.

public class PositiveNummerTest .. {

	// Fixture
	private Kunde kunde;

	// Wird vor Begin der Tests aufgerufen
	protected void setUp() {
		kunde = new Kunde();
	}

	// wird bei Beendigung aufgerufen
	protected void tearDown() {
	}

}

Exceptions

Code kann bei Fehlbenutzung Exceptions werfen. Diese können mit JUnit geprüft werden. Entweder wird durch einen Fehler unerwartet eine Exception geworfen, oder es wird explizit eine Exception erwartet wie in diesem Beispiel:

Der Testfall würde fehlschlagen, wenn nach der Erzeugung von "nummer" die Exception ausbleibt. Der Aufruf von fail(string beschreibung) lässt den Test scheitern.

public class PositiveNummerTest .. {
	public void testNegativ() {
		try {
			PositiveNummer nummer = new PositiveNummer(-2);
			fail("nummer muss positiv sein");
		} catch (IllegalArgumentException expected) {
		}
	}
}

Die zu testende Klasse wirft wie gefordert eine Exception wenn die Zahl negativ ist.

public class PositiveNummer {
	double nummer;
	public PositiveNummer(double nummer) {
		if (nummer<0)
			throw new IllegalArgumentException("nummer muss positiv sein");
		this.nummer = nummer;
	}
}


Asserts

assertTrue(boolean condition) Testet ob condition wahr ist.

assertFalse(boolean condition) Testet ob condition falsch ist.

assertEquals(Object expected, Object actual) Testet ob expected gleich actual ist.

assertEquals(double expected, double actual, double delta) Testet ob die Kommazahl expected gleich actual ist, optional mit einer Abweichung von delta.

assertNull(Object object) Testet ob object Null ist.

assertNotNull(Object object) Testet ob object nicht Null ist.

assertSame(Object expected, Object actual) Testet ob expected identisch mit actual ist.

assertNotSame(Object expected, Object actual) Testet ob expected nicht identisch mit actual ist.

Akzeptanztests

FIT "Framework for Integrated Tests"

Entwickler sprechen selten die Sprache des Kunden. Der wiederum hat die nötige Fachkenntnis über die zu erbringende Software.

FIT ist ein Framework für die Automatisierung von Akzeptanztests. Entwickelt wurde es von Ward Cunningham.

Akzeptanztests spezifizieren den Funktionsumfang und überprüfen ihn zugleich. Es sind Blackbox Tests im Gegensatz zu den Unit Tests, die genaue Kenntnisse über die Implementierung des Systems haben. Es wird das gesamte System getestet.

Datengetriebene Akzeptanztests werden Tabellarisch in HTML erstellt. FIT liest die Tabelle ein und führt das Programm mit den Daten aus der Tabelle aus. Es speichert das Ergebnis der Tests innerhalb der Tabelle ab.

Installation

FIT ist zu Beziehen unter http://fit.c2.com

Wie bei JUnit muss die fit.jar Datei in den CLASSPATH hinzugefügt werden.

Aufruf

java fit.FileRunner <Eingabe HTML Test Datei> <Ausgabe HTML Test Datei>

Z.B.: java fit.FileRunner tests.html tests-ergebnis.html

Ein positiver Durchlauf ergibt: 0 right, 0 wrong, 0 ignored, 0 exceptions

Akzeptanztest als HTML Tabelle

Test Erklärung

In dem vorliegenden Beispiel handelt es sich um die Berechnung der Gesamtsumme eines Verkaufs, in Abhängigkeit der Mitgliedschaft. Sprachlich wird definiert, wie sich die Berechnung ergeben soll:

Bei einem Betrag von unter 100€ fällt eine Versandpauschale von 10€ an. Bei Premiummitgliedschaft entfällt die Pauschale und zu zahlen sind 90% des Betrags.


Objekte und Funktionen

Getestet werden soll die Klasse Preis. Die Variable betrag wird vorgegeben um zu testen ob die Funktionen gesamtStandard() und gesamtPremium() wie gewünscht arbeiten.


Werte

Die Werte, die eingangsseitigen (hier der Betrag) und die erwarteten Testresultate, werden in die Tabelle eingefügt.


Beispiel

Gesamtsumme berechnen

Bei einem Betrag von unter 100€ fällt eine Versandpauschale von 10€ an. Bei Premiummitgliedschaft entfällt die Pauschale und zu zahlen sind 90% des Betrags.

Preis
betrag gesamtStandard() GesamtPremium()
10 20 9
50 60 45
100 100 90
200 200 180

Unsere zu testende Klasse:

public class Preis extends fit.ColumnFixture {
	
	double betrag;
	
	public Preis() {
		betrag = 0;
	}

	public double gesamtStandard() {
		if (betrag<100)
			return betrag+10;
		else
			return betrag;
	}

	public double GesamtPremium() {
			return betrag-(betrag/10);
	}
}

Testdurchlauf

Nachdem FIT gestartet wurde werden die Spalten der Tabelle, je nach Testresultat, mit grün, gelb oder rot markiert:

Gesamtsumme berechnen

Bei einem Betrag von unter 100€ fällt eine Versandpauschale von 10€ an. Bei Premiummitgliedschaft entfällt die Pauschale und zu zahlen sind 90% des Betrags.

Preis
betrag gesamtStandard() GesamtPremium()
10 20 9
50 60 45
100 100 90
200 200 180

Fixture Typen

Aktions Typen