In der Programmentwicklung kann man nicht nur Programme schreiben, sondern auch Programme, welche die Programme testen.
Die Reihenfolge der Arbeitsschritte bei der Entwicklung ist dann:
Beim traditionellen Funktions-Test lässt man das Programm laufen ohne sich um seine interne Arbeitsweise zu kümmern. Es wird getestet, ob das Programm die erwarteten Dinge tut.
Der Unit-Test prüft jede Methode in jeder Klasse einzeln. Der Test prüft in jedem Stadium der Programmierung, ob die Methode tut was sie tun soll. Daher findet er Fehler, bevor sie weitere Kreise ziehen. So wie der Compiler immer die syntaktische Korrektheit prüft, prüft der Unit-Test immer die semantische Korrektheit.
Der Unit-Test ist in gewisser Weise auch eine Art Dokumentation für das entstehende Programm: Er zeigt eindeutig, was das Programm tut. Unit-Tests stellen die use-cases in ablauffähiger Form dar.
In der Praxis wird man Unit-Tests und Funktionstests gleichermassen ausführen.
Unit-Test bringt eine ähnliche Information wie ein Debugger, aber in automatisierter und dokumentierbarer Form. Debugger sind in Java ein vernachlässigtes Thema, und dank Unit-Test braucht man auch keine mehr. ( Ausnahme javax.swing.DebugGraphics)
Unit-Test funktionieren so ähnlich, als würde man an die interessanten Stellen des Programms print()-Befehle hineinschreiben: dann sieht man, was bei der Ausführung im Programm passiert. Die print()-Befehle haben aber Nachteile:
print()-Befehle müssen von Hand kontrolliert werden.Ein Unit-Test verändert demgegenüber das zu testende Programm nicht. Die Tests werden so formuliert, dass sie jeweils einen Return-Wert aus dem Programmlauf mit einem Sollwert vergleichen. Der Unit-Test bringt als Ergebnis true oder false.
Ein Framework koordiniert die einzelnen Unit-Tests zu einem Programm und sammelt die Ergebnisse auf. Bei Bedarf kann das Framework die Ergebnisse auswerten. Es könnte z.B. nur die fehlerhaften Tests anzeigen, und die erfolgsreich verlaufenen - weil sie kein Eingreifen erfordern - mit Stillschweigen behandeln. In dem Framework können die Unit-Tests unter kontrollierten Bedingungen immer wieder laufen - durch Aufruf eines einzigen Programms.
Das Framework, das im Folgenden vorgestellt wird, gibt einfach alle Testergebnisse aus, zusammen mit Beschriftungen, die man an beliebigen Stellen in den Testlauf einfügen kann, um die Absicht eines bestimmten Tests zu verdeutlichen.
In der Literatur werden verschiedene Ansätze für die Durchführung von Unit-Tests diskutiert. Es gibt verschiedene Frameworks.
Eckel benutzt für den Unit-Test ein Framework, bei dem in jede Klasse der Test als innere Klasse eingebaut wird. Vorteil ist, dass so der Test an alle privaten member herankommt. Trotzdem: Nicht sehr schön.
JUnit.org stellt ein Framework, Literatur und Hintergrundinformation zur Verfügung. Das JUnit-Framework macht dem Programmierer gewisse Auflagen, wie er Test-Methoden zu schreiben hat. Da das Framework mit reflection arbeitet, ist es wichtig, wie die test-Methoden benannt werden usw. Die zu testenden Methoden bleiben unverändert: Das Framework beobachtet sie von aussen.
Das Testen wird bei JUnit.org als Bestandteil des Extreme Programming (XP) angesehen. Die Verfahrensweise wird wie folgt beschrieben:
Siehe Literatur.
Man wird also die Programme so schreiben, dass der Unit-Test an alle relevanten Informationen herankommt. Das heisst vor allem, das alle privaten Attribute ihre public getXXX()-Methoden bekommen.
Der Vorteil ist: Die Programme werden einfacher, und klarer strukturiert. Sie können jederzeit umgebaut werden: Wenn der Test hinterher noch läuft, ist es gut gegangen. Andernfalls zeigt der Test sofort die Stellen auf, wo Probleme aufgetreten sind. Dadurch werden Programme bei der Entwicklung ganz von selbst immer einfacher und wirkungsvoller.
Der im Folgenden beschriebene Prototyp eines Framework ist sehr einfach. Er wurde bei der Entwicklung der Sinus-Kurve und des JRechners benutzt und zeigte selbst in dieser rudimentären Form schon wohltuende Wirkung. Er kommt ohne reflection aus und macht keinerlei Vorgaben bzgl. der Namen der Testklassen und wie sie in packages einzuteilen sind. Das ist wichtig, weil davon die Erstellung von Test-Dokumentation mittels javadoc abhängt.
Zu Unit-Tests in diesem Framework gehören immer 2 Arten von Klassen, die zusammen mit den zu testenden Klassen bei der Programmierung zu erstellen sind:
Entwickelt und getestet werden soll hier 1 einfache Zaehlerklasse. Die Klasse soll 1 Konstruktor haben und 1 Methode zum raufzählen mit Schrittweite 1. Nach der reinen Lehre des XP schreibt man den Unit-Test zuerst.
Zuerst der eigentliche Test:
Die TestKlasse ist abgeleitet von abstract class AbstractTest und implementiert deren abstrakte Methode lauf( TestErg). In dieser Methode wird der Konstruktor der zu testenden Klasse aufgerufen. Dann wird die zu testende Methode raufzaehlen() aufgerufen und ihr Returnwert mit einem Sollwert verglichen. Das ist inhaltlich gesehen der Test.
Der Test läuft in einem try catch, weil man vorher nicht weiss, was passieren wird. Der Parameter der Methode lauf( TestErg t )wird bei allen Tests mitgeführt, um Testergebnisse aufzusammeln für die spätere Auswertung.
Und so kommen Testergebnisse in das TestErgebnis-Objekt hinein:
Alle Werte von Ausdrücken, die Parameter von
ergebnis( String )/ergebnis( boolean )
sind, werden lokal vorgemerkt. Als Werte kommen in Betracht:
t.aufsammeln( getErg());.
Das ist also die TestKlasse:
package abstr;
import test.*;
public class TestKlasse extends AbstractTest{
public void lauf( TestErg t ){
ergebnis( " abstr.Zaehler().raufzaehlen() - " );
try{
Zaehler z = new Zaehler( 11 );
ergebnis( z.raufzaehlen() == 12 ); // Sollwert 12
}catch( Exception e ){
ergebnis( e.toString());
}
t.aufsammeln( getErg() );
}
}
In der folgenden Hauptklasse TestLauf wird der eben definierte Test durchgeführt. Die Anmeldung zum Testen erfolgt durch Aufruf des parameterlosen Konstruktor der Testklasse. Es können beliebig viele Testklassen angemeldet werden. Die folgenden Methodenaufrufe führen den angemeldeten Tests durch und werten ihn aus. Zum Schluss wird abgeschaltet:
package abstr;
import test.*;
public class TestLauf extends AbstractTestLauf{
static public void main( String [] args ){
new TestKlasse();
alleTesten( erg );
erg.auswerten();
System.exit(0);
}
}
Und hier die Klasse, die getestet werden soll:
package abstr;
public class Zaehler
{
private int zahl = 0;
public Zaehler( int z ){
zahl = z;
}
public int raufzaehlen()
{
return zahl += 1;
}
}
Der Verlauf eines erfolgreichen Tests am DOS-Prompt sieht so aus:
F:\Java5\kurs>java abstr.TestLauf abstr.Zaehler().raufzaehlen() - true
Das Framework befindet sich in package test. Es gibt 2 interfaces und 3 Klassen:
Die eigentlichen Tests benutzen dieses Framework. Sie sind in separaten packages untergebracht, meist im package der Anwendung, die getestet wird.
Ein Aspekt beim Unit-Test ist: nur wenige Methoden können alleine für sich getestet werden. Eine setXXX()-Methode kann kaum ohne die dazu passende getXXX()-Methode getestet werden. Oder: Die Methode ergAnzeigen(RechnerEvent) der Klasse JRechner kann nicht ohne Bezug auf ein Objekt der Klasse RechnerEvent getestet werden. Dadurch stellt sich in verschärfter Form das Problem, beim Testen den Überblick darüber zu behalten, was eigentlich getestet wird.
Daher ist die Dokumentation der Tests besonders wichtig. Tests können sehr gut mit javadoc dokumentiert werden. Damit die Dokumentation eine übersichtliche Struktur bekommt, kann man die Tests in geeigneter Weise in packages und Klassen unterbringen.
Alle konkreten Tests sind Subklassen von AbstractTest. Sie sind in separaten packages untergebracht. Für jede zu testende Klasse der JRechner-Application gibt es ein gleichnamiges package. Die Klassen in diesen packages heissen ungefähr wie die Methoden, die getestet werden sollen - es werden nämlich in fast allen Tests mehrere Methoden im Zusammenhang getestet. Diese Organisationsform ist zwar aus Sicht des Framework nicht erforderlich. Aber sie ist möglich und sie erleichtert die Dokumentation mit javadoc und verbessert daher den Überblick.
Die Organisation der eigentliche Tests ist also wie folgt:
Hauptklasse ruft alle Tests auf:
package rechner.rechnertest;
import test.*;
/**
* HauptKlasse der Tests für den JRechner
*
*@see test.AbstractTestLauf
*@see rechner.JRechner
*/
public class TestJRechner extends AbstractTestLauf{
/**
* Tests anmelden und durchführen.
* Getestet werden die Methoden des JRechner.
* Jede einzelne Testklasse wird hier mit ihrem
* Konstruktor aufgerufen. Dadurch trägt sie sich selbst in
* die Collection der zu testenden Klassen ein.
* Dann laufen die Tests.
* Dann werden die Testergebnisse ausgewertet.
*/
public static void main( String [] args ){
//TestJRechner:
new rechner.rechnertest.jrechner.GetSet();
new rechner.rechnertest.jrechner.Ziffern();
new rechner.rechnertest.jrechner.DezPkt();
new rechner.rechnertest.jrechner.ErgAnzeigen();
//Test Event:
new rechner.rechnertest.rechnerevent.GetWert();
new rechner.rechnertest.rechnerevent.GetErr();
// TestModel:
new rechner.rechnertest.rechnermodel.NeueRech();
new rechner.rechnertest.rechnermodel.Grundrechenarten();
new rechner.rechnertest.rechnermodel.RechnerBenachrichtigen();
new rechner.rechnertest.rechnermodel.GetSet();
new rechner.rechnertest.rechnermodel.Weiter();
new rechner.rechnertest.rechnermodel.UseCase();
new rechner.rechnertest.rechnermodel.Loesch();
new rechner.rechnertest.rechnermodel.ZahlAnnehmen(); //RUNDUNGSFEHLER ??
alleTesten( erg );
erg.auswerten();
System.exit(0);
}
}
Als Beispiel für die Tests hier nur einer: Testen der Darstellung von Zahlen in den unterschiedlichen Zahlensystemen:
package rechner.rechnertest.jrechner; import test.AbstractTest; import test.TestErg; import rechner.JRechner; /** *zifferAnnehmen()getZiffern(), *getDoublewert()anderesSyst()*/ public class Ziffern extends AbstractTest{ /** * */ public void lauf( TestErg t ){ ergebnis( " rechner.JRechner.zifferAnnehmen() - "); try{ JRechner jr = new JRechner(); // 2 Ziffern machen 1 Zahl jr.zifferAnnehmen( "1" ); jr.zifferAnnehmen( "5" ); ergebnis( jr.getZiffern().equals( "15" )); ergebnis( jr.getDoubleWert() == 15 ); jr.anderesSyst( 16 ); ergebnis( jr.getZiffern().equals( "f" )); ergebnis( jr.getDoubleWert() == 15 ); jr.anderesSyst( 2 ); ergebnis( jr.getZiffern().equals( "1111" )); ergebnis( jr.getDoubleWert() == 15 ); jr.loeschen(); ergebnis( jr.getZiffern().equals( "0" )); ergebnis( jr.getDoubleWert() == 0 ); jr.anderesSyst( 2 ); jr.zifferAnnehmen( "1" ); jr.zifferAnnehmen( "0" ); jr.zifferAnnehmen( "0" ); jr.zifferAnnehmen( "0" ); jr.zifferAnnehmen( "0" ); ergebnis( jr.getZiffern().equals( "10000" )); ergebnis( jr.getDoubleWert() == 16 ); }catch( Exception e ){ ergebnis( e.toString()); } t.aufsammeln( getErg() ); } }
Restliche Unit-Tests entsprechend.
Ein typischer Testlauf sieht aus wie folgt: die Hauptklasse TestJRechner wird aufgerufen und führt alle Tests durch. Die Ausgabe zeigt, dass fast alles funktioniert hat, und gibt sofort Hinweise, wo nachgearbeitet werden muss:
F:\Java5\kurs>java rechner.rechnertest.TestJRechner rechner.JRechner.anderesSyst() - truetruetruetrue rechner.JRechner.setZiffern() - truetrue rechner.JRechner.setDoubleWert() - true rechner.JRechner.zifferAnnehmen() - truetruetruetruetruetruetruetruetruetrue rechner.JRechner.dezimalPunkt() - truetruetruetruetruetruetruetruetruetruetrue rechner.JRechner.ergAnzeigen() - truetruetruetruetruetruetruetrue rechner.RechnerEvent.getWert() - true rechner.RechnerEvent.getErr() - true neueRechnung - truetruetruetruetruetruetruetruetruetruetruetruetruetrue Grundrechenarten - truetruetruetruetruetruetruetruetruetruetruetruetruetruetrue truetruetruetruetruetruetruetruetrue rechnerBenachrichtigen() - truetrue set-/get - truetruetruetruetruetrue Weiter - truetruetruetruetruetruetruetrue UseCase - truetruetruetruetruetruetruetruetruetruetruetruetruefalsetruetrue Loesch - truetruetruetrue Zahlenannehmen - truetruetruefalsetrue
mit javadoc-Kommentaren.
/*
* interface test.Test
*
* Version prototyp 1
*
* 21.7.2001
*
* www.AndreasGoedel.de
*
*/
package test;
/**
* Alle Tests implementieren dieses.
* Die Subklasse von KLasse AbstractTestLauf, Klasse TestLauf führt die Tests durch
* und nimmt nur Klassen, welche dieses interface implementieren.
*/
public interface Test{
/**
* Test laufen lassen.
* Der Testprogrammierer schreibt hier die Tests rein.
* Die Testergebnisse werden in einem TestErg aufgesammelt
*/
public void lauf( TestErg t );
}
/*
* interface test.TestErg
*
* Version prototyp 1
*
* 21.7.2001
*
* www.AndreasGoedel.de
*
*/
package test;
/**
* TestErgebnis implementiert dieses.
* Die Sublasse von AbstractTestLauf, welche die Tests durchführt,
* kann das TestErgebnis nach dem Laufen der Tests auswerten.
*/
public interface TestErg{
/**
* Das Objekt der Klasse TestErgebnis wird allen Tests
* als Parameter mitgegeben und kann also unterwegs
* ein Testergebnis nach dem anderen aufsammeln.
* Die lauf( TestErg )-Methoden rufen
* jeweils zum Abschluss eines Tests diese Methode,
* um das einzelne Testergebnis in das gesamte Ergebnis
* einzubringen.
*
*@see test.AbstractTest#lauf( TestErg )
*@param erg ein Ergebnis eines Tests
*/
public void aufsammeln( Object erg );
}
/* * class AbstractTestLauf * * Version prototyp 1 * * 21.7.2001 * * www.AndreasGoedel.de * */ package test; import java.util.*; /** * Abstrakte HauptKlasse des Frameworks. * Die echten TestLauf-Klassen der echten Tests sind Subklasse von hier. * * Der Testlauf hat 2 statische Felder: * * Collection tests - alle von AbstractTest abgeleiteten Klassen, die * getestet werden sollen * TestErgebnis erg - alle anfallenden Testergebnisse aufsammeln * * Ein Test geht so: * Die Tests in die Collection aufnehmen, laufen lassen unter Mitnahme des * TestErgebnis-Objekts und dieses zum Schluss auswerten. * * Für jeden Test wird also sein Konstruktoren aufgerufen. Diese * tragen sich selbst in die Collection des TestLauf ein. * In jedem Test gibt es 1 Methodelauf( TestErg ), * wo der Test durchgeführt wird. *
* Nach diesemtestHinzufuegen( Test )werden zur Durchführung * der Tests die 3 Methoden aufgerufen wie oben. Diese 3 Aufrufe sind in den *main()reinzuschreiben! * *@see test.TestErg *@see test.Test */ public class AbstractTestLauf{ //Die Tests: static private Collection tests = new ArrayList(); /** * TestErgebnisse aufsammeln. * */ static protected TestErgebnis erg = new TestErgebnis(); /** * Einen Test der Collection hinzufügen. * Die zu testenden Klassen rufen diese Methode in ihren Konstruktoren * auf, so dass sie sich selbst als neuer Test hier anmelden. */ static public void testHinzufuegen( Test t ){ tests.add( t ); } /** * Die Tests in der Collection laufen lassen. * Jeder Test wird mit seinerlauf( TestErg)aufgerufen. * Ein Objekt von TestErg wird dabei mitgeführt und jeder Test trägt * seine Ergebnisse dort ein. * *@see rechner.rechnertest.Test#lauf( TestErg ) *@param e Ein Objekt von TestErg zum Aufsammeln der Testergebnisse */ static public void alleTesten( TestErg e ){ Iterator it = tests.iterator(); while(it.hasNext()){ ((Test)it.next()).lauf( e ); } } }
/*
* class test.TestErgebnis implements TestErg
*
* Version prototyp 1
*
* 21.7.2001
*
* www.AndreasGoedel.de
*
*/
package test;
import java.util.*;
/**
* TestErgebnis sammelt die Ergebnisse aller Tests auf.
* Bei Bedarf kann man sie dann auswerten
*/
public class TestErgebnis implements TestErg{
static private Collection Ergebnisse = new ArrayList();
/**
* Testergebnisse aufsammeln.
* Diese Methode wird von jeder getesteten Klasse zum Abschluss
* ihrer lauf( TestErg )-Methode aufgerufen, um ihr
* Testergebnis in das gesamte TestErgebnis einzubringen.
*
*@param erg das Testergebnis der gerade laufenden TestMethode
*/
public void aufsammeln( Object erg ){
Ergebnisse.add( erg );
}
/**
* Testergebnisse auswerten.
* Das heisst ausgeben.
*/
public void auswerten(){
Iterator it = Ergebnisse.iterator();
while(it.hasNext()){
System.out.println( (String)it.next() );
}
}
}
/*
* abstract class AbstractTest implements Test
*
* Version prototyp 1
*
* 21.7.2001
*
* www.AndreasGoedel.de
*
*/
package test;
/**
* Superklasse aller Testklassen.
*/
public abstract class AbstractTest implements Test{
private String erg = "";
/**
* Sich bei Konstruktoraufruf anmelden zum Test.
* Die Konstruktoren der abgeleiteten Klassen tragen sich in die
* Collection von AbstractTestlauf ein.
*
* Jeder Test hat eine private Variable erg,
* die lokal mit ergebnis( String ) und ergebnis( boolean)
* gefüllt wird. Letzlich wird dies dem TestErg hinzugefügt.
*
*@see test.TestErg#aufsammeln( Object )
*@see test.AbstractTestLauf#testHinzufuegen(Test)
*/
public AbstractTest(){
AbstractTestLauf.testHinzufuegen( this );
}
/**
* Die Methode laufen lassen.
* Wird von den Subklassen überschrieben, weil dort die
* Tests definiert werden.
* Die abstrakte Superklasse stellt alles zur Verfügung,
* was alle TestKlassen brauchen: sich beim Testlauf anmelden,
* Ergebnisse aufsammeln, und Endergebnis rausgeben.
*
* Beispiel für lauf( TestErg ) in einer
* hiervon abgeleiteten Klasse:
*
*
public void lauf( TestErg t ){
ergebnis( " //BEZEICHNUNG DER METHODE HIER" );
try{
// KONSTRUKTOR DER ZU TETSTENDEN KLASSE
ergebnis( METHODENAUFRUF == RETURNWERT );
ergebnis( METHODENAUFRUF.equals( RETURNWERT ));
//...
}catch( Exception e ){
ergebnis( e.toString());
}
t.aufsammeln( getErg() );
}
*
*
*@see test.AbstractTestLauf
*/
public abstract void lauf( TestErg t );
/**
* Eine Zeichenkette dem Ergebnis des Tests hinzufügen.
* Das Ergebnis ist ein String, der anfangs leer ist.
* Als erstes kommt der Name des Tests hinein, dann
* die Testergebnisse als boolean.
*/
public void ergebnis( String e ){
erg += e;
}
/**
* Ein boolean dem Ergebnis des Tests hinzufügen.
*/
public void ergebnis( boolean e ){
erg += e;
}
/**
* Das Ergebnis rausgeben.
*/
public String getErg(){
return erg;
}
}
Ende Quellcode. Benutzung auf eigene Gefahr.
Das Testframework download
Der komplette Rechnertest download
Jetzt das Test-Framework auch in C++
Download C++Dateien als zip