8.2 Testbare Programme gestalten
Einer der Vorteile objektorientierter Systeme ist die verbesserte Testbarkeit, die darauf beruht, Unterklassen der Testumgebung zu bilden und dabei dynamisch Methoden zu
redefinieren. Dennoch gibt es bei objektorientierten Systemen einige Problemstellungen, die die Definition von Testumgebungen, die Durchführung des Tests oder die Feststellung des Testerfolgs
erschweren oder sogar verhindern. Diese resultieren grundsätzlich aus statischen Attributen und Methoden, der Objekterzeugung und der Verwendung vordefinierter Frameworks oder Komponenten.
Diese Probleme und ihre Behebung werden im Folgenden diskutiert. Dabei werden weitere Testmuster unter Verwendung von UML-Diagrammen vorgestellt. Interessanterweise sind Polymorphie und dynamische
Bindung zwar Komplexitätstreiber für Tests, aber aufgrund der dadurch entstehenden Flexibilität auch wesentliche Elemente zur Testfalldefinition.
Um ein System testbar zu machen, werden im Folgenden eine Reihe von Strukturveränderungen vorgeschlagen, die die Simulation einer Systemumgebung erleichtern
oder erst ermöglichen. Zusätzlich wird der Testling für die Testdurchführung instrumentiert. Beide Arten von Veränderungen greifen in das System ein. Die vorgeschlagenen
Strukturveränderungen sind permanent und erfordern daher die Akzeptanz durch die Entwickler. Objektorientierte Methoden unterstützen dieses Vorgehen tendenziell deutlich eher als
frühere Paradigmen. Außerdem ist es bei einer agilen Vorgehensweise von Vorteil, dass die Tests nicht erst nach Fertigstellung des Systems entwickelt werden und damit a priori
Einfluß auf die Systemstruktur nehmen können.
Die Instrumentierung des Testlings ist demgegenüber nicht permanent und erfordert damit größte Vorsicht, da sie die Funktionsfähigkeit des Testlings nicht verändern
darf. Vom Codegenerator korrekt durchgeführte Instrumentierungen führen zumindest zu einer geringfügigen Veränderung der Laufzeiten und verfälschen damit Laufzeitmessungen,
aber nicht funktionale Tests.
8.2.1 Statische Variablen und Methoden
Eine immer wiederkehrende Problemstellung ist die Verwendung von statischen Variablen und Methoden. Soweit wie möglich sollte auf die Verwendung derartiger statischer Elemente
verzichtet werden. Wo dies nicht möglich oder sinnvoll ist, sollte aber zum Beispiel statt der Variablen selbst ein Objekt verwendet werden, das die eigentliche Variable kapselt. Im
Auktionsprojekt wurde zum Beispiel das in Abbildung 3.7 dargestellte Singleton-Objekt AllData verwendet, um damit
Zugang auf alle Auktionen, Personen und weitere Datenstrukturen zu erhalten. Dieses Objekt ist im UML-Modell durch das statische und mit dem Zugriffsrecht readonly
versehene Attribut AllData.ad zugänglich. Bei der Codegenerierung wird daraus eine geeignete Zugriffsmethode erstellt, allerdings keine Methode zur Änderung
des Attributwerts selbst. Das heißt, das Singleton ist von außen zugreifbar, aber gegen Ersetzung geschützt. Dennoch wird nachfolgend ein Muster diskutiert, das statische Variablen
vollständig kapselt und statische Methoden für Tests besser zugänglich macht.
Das Singleton für Protokolle wird im Attribut prot der Klasse Protocol abgelegt. Wie Abbildung 8.8 zeigt, wird zusätzlich eine Kapselung des Attributs in einer statischen Methode vorgenommen. Eine Meldung im Protokoll kann dann mit
Java Protocol.writeToLog("Meldung") |
vorgenommen werden. Um die initiale Besetzung des Attributs prot sicherzustellen, prüft die Methode -das statische Attribut und besetzt es bei Bedarf,
führt dann aber ausschließlich eine Delegation an dieses Objekt durch.
Die Ersetzung des Protocol-Objekts durch ein Dummy ist nun einfach. Die Methode doWriteToLog wird im Dummy überschrieben und das
Dummy-Objekt macht sich durch den Aufruf von setAsProtocol() selbst zum Protokollempfänger.
Dieses Verfahren lässt sich in Tabelle 8.9 als Muster zusammenfassen. In Abschnitt 10.1.5 ist darüber hinaus eine Refactoring-Regel angegeben, die dieses Testmuster in ein bestehendes Modell der Struktur eines Systems einführt.
|
|
Muster: Singleton hinter statischen Methoden
|
|
|
|
|
|
Intention
|
Das Muster ermöglicht einerseits kompakten Zugriff auf ein Singleton-Objekt, das für Tests durch ein Dummy ersetzt werden kann, vermeidet aber andererseits eine öffentlich
zugängliche statische Variable für dieses Objekt.
|
|
|
|
Motivation
|
Siehe vorherige Diskussion zur Testbarkeit von Code mit statischen Variablen. Beispiele sind Objekte, die Protokoll-Aufgaben, Abfragen der Zeit oder einen Factory-Mechanismus
realisieren.
|
|
|
|
Anwendung
|
Eine Anwendung dieses Musters ist sinnvoll, wenn
-
von einer Klasse nur ein Singleton, dieses aber an vielen Stellen benutzt wird,
-
ein kompakter Zugriff der Form Singleton.method() gewünscht ist,
-
die Speichervariable für das Singleton verborgen bleiben soll und
-
das Singleton in Tests durch ein Dummy ersetzbar sein soll.
|
|
|
|
Struktur
|
|
|
|
|
Implementierung Singleton
|
initialize(new Singleton( … )); } |
static initialize(Singleton s) { singleton=s; } |
static method(Arguments) { |
// eigene Initialisierung wenn notwendig |
if(singleton==null) initialize(); |
return singleton.doMethod(Arguments); |
// hier wird gearbeitet … |
|
|
|
|
Implementierung Dummy
|
Java/P class SingletonDummy { |
setAsSingleton() { initialize(this); } |
// hier wird Arbeit simuliert … |
|
|
|
|
Zugriff
|
Der Zugriff auf das Singleton erfolgt mit dem Ausdruck Singleton.method(Arguments). Eine vorherige
Initialisierung ist nicht notwendig.
|
|
|
|
Beachtenswert
|
Das Problem der unvollständigen Initialisierung wird behoben, indem als Default ein Objekt der Klasse selbst erzeugt wird. Eine restriktivere Form könnte hier eine
Fehlermeldung erzeugen, da erfahrungsgemäß gerade bei Tests die adäquate Besetzung des Singletons gerne übersehen wird.
|
|
|
|
|
|
|
|
Tabelle 8.9 : Muster: Singleton hinter statischen Methoden
|
|
|
8.2.2 Seiteneffekte in Konstruktoren
Eines der wesentlichen Probleme bei dem gezeigten Verfahren ist die durch Java vorgegebene Notwendigkeit, dass Objekte aus Unterklassen einen Konstruktor der Oberklasse aufrufen.
Wenn dieser Konstruktor Seiteneffekte verursacht, also im Beispiel die Protokolldatei öffnet, so ist die Definition von Dummies ohne Seiteneffekte für diese Klasse nicht mehr
möglich. Aus diesem Grund sollten Konstruktoren relativ wenig Funktionalität beinhalten und gegebenenfalls zusätzliche Funktionen angeboten werden, die solche Initialisierungen
vornehmen.
8.2.3 Objekterzeugung
Ein ähnlich gelagertes Problem ist die Erzeugung neuer Objekte. Ein Kommando der Form new Klasse() im Testling legt die Form des dabei
entstehenden Objekts genau fest. Es ist hier nicht möglich, in Testläufen statt dem angegebenen Objekt ein geeignetes Dummy einzusetzen. Das führt zu schlecht testbarem Code. Dieses
Problem lässt sich beheben, indem im Produktionscode eine Factory [GHJV94] eingesetzt wird.
Wie in Abschnitt 5.1.7 beschrieben, kann diese Factory aus dem gegebenen UML-Modell erzeugt werden, indem für alle auftretenden
Konstruktoren entsprechende Methoden der Factory erzeugt werden. In analoger Weise werden durch den Generator alle Konstruktoraufrufe in den Java-Coderümpfen durch Factory-Aufrufe ersetzt. Die
Factory ist selbst ein Singleton, das typischerweise in einer statischen Variable abgelegt ist. Es kann daher mit dem bereits für Protokolle angewandten Muster dynamisiert und durch ein
FactoryDummy-Objekt für Tests vorbereitet werden. Abbildung 8.10 zeigt einen noch weitergehenden Ansatz,
der ein Dummy-Objekt als Factory angibt, das mehrere vorbereitete Dummy-Objekte für den tatsächlichen Testablauf bereitstellt.
Die im Testablauf benötigten Objekte werden also nicht mehr während des Tests generiert, sondern bereits vorab erstellt und dann nur noch übergeben. Deshalb kann die Factory zum
Beispiel durch ein Objektdiagramm wie in Abbildung 8.11 initialisiert werden. Dabei kann exakt bestimmt werden, welche Klassen genutzt und,
falls es sinnvoll ist, wie die Attribute vorbelegt werden sollen.
8.2.4 Vorgefertigte Frameworks und Komponenten
Die in den letzten Abschnitten beschriebenen Umformungen sind jedoch nicht durchführbar, wenn vorgegebene Frameworks, Klassenbibliotheken oder Komponenten verwendet werden
sollen, die nicht umgebaut werden können. Ziel eines Tests sind dabei nicht die vorgegebenen Frameworks, sondern die selbst entwickelten Klassen, deren Funktionalität darauf aufbaut und
deshalb Teile des Frameworks in der Testumgebung benötigt. Nach [LF02] können zum Beispiel Enterprise JavaBeans (EJB)
[MH00] nur sehr schlecht in Tests einbezogen werden. Das hat im Allgemeinen mehrere Gründe:
- Die Ablauflogik kann durch ein Framework festgelegt sein. Das in Frameworks übliche „Don’t call us, we call you“-Prinzip [FPR01] erlaubt es in Tests nur mit hohem Aufwand, die Kontrolle zu übernehmen.
- Die Erzeugung neuer Objekte ist im Framework bereits fixiert. Ein Eingreifen mit einer Factory ist nicht möglich.
- Statische Variablen, insbesondere wenn sie gekapselt sind, können im Test nicht ausreichend kontrolliert und nicht geeignet besetzt werden.
- Gekapselte Objektzustände erlauben den Zugriff für die Bewertung des Testerfolgs nicht.
- Von den zur Verfügung stehenden Klassen können keine Unterklassen und damit keine Dummies gebildet werden, weil (1) die Klasse oder eine darin enthaltene Methode als
final deklariert ist, (2) kein öffentlicher Konstruktor existiert, (3) Konstruktoren unerwünschte Seiteneffekte haben oder (4) die interne Ablauflogik
unbekannt ist.
- Die Instrumentierung der Klassen ist nicht möglich, so dass zum Beispiel die für die Prüfung von Invarianten und Sequenzdiagrammen notwendige Information nicht
zugänglich ist.
Um Software dennoch testen zu können, ist daher eine Separation der Applikationslogik von derartigen Frameworks oder Komponenten notwendig. Dazu kann generell das Adapter-Entwurfsmuster [GHJV94] verwendet werden. [SD00] beschreibt diese Trennung als wichtig für die unabhängige Wiederverwendbarkeit der Applikationslogik und des technischen Codes, aber auch
für die Verbesserung der Wartbarkeit. Ein weiterer positiver Effekt dieser Trennung ist die bessere Testbarkeit. In Abbildung 8.12 ist
ein Adapter für Java Server Pages (JSP) dargestellt. Das Klassendiagramm beschreibt eine Trennung der Verarbeitung von übers Web eingegebenen Datensätzen und der tatsächlichen
Speicherung in HttpServletRequest-Objekten, die von den JSP [FK00] zur Verfügung gestellt werden.
Diese Request-Objekte beinhalten die vom Anwender über ein Formular eingegebenen Daten und können unter anderem über die Liste der Parameter getParameterNames und das Auslesen einzelner Parameterwerte getParameter erfragt werden. Weitere Methoden wie getSession
liefern zum Beispiel den Kontext der Session, zu der das Formular gehört.
Der Interaktionsmechanismus, den JSP fordert, um zum Beispiel die Eingabedaten auszulesen, macht die komplexe Adaption notwendig. Dabei sind gegebenenfalls Parameter und Ergebnisse jeweils
wieder zu ver- und entpacken. Im Auktionssystem wurde dieser Mechanismus verwendet, um die JSP-Oberfläche vom Applikationskern zu trennen.
Für die Separation des entwickelten Codes von benutzten Frameworks und Komponenten gibt es primär zwei Varianten. Zum einen kann eine vollständige Sammlung von Adaptern für
alle Klassen des Frameworks angeboten werden. Zum anderen kann eine Minimalversion der gerade benötigten Klassen und der davon verwendeten Methoden erstellt werden.
Die Minimalversion entspricht der Idee, dass möglichst wenig Aufwand in solche technischen Definitionen gesteckt werden sollte, hat aber den Nachteil, dass mit der Notwendigkeit für
weitere Methoden die Adapter-Schicht iterativ ausgebaut werden muss. Andererseits hat diese Beschränkung auch den Vorteil, dass eine Anpassung an eine neue Version des Frameworks sowie eine
Migration zu einem anderen Framework leichter möglich ist.
Demgegenüber hat eine vollständige Adapter-Schicht den Vorteil, dass sie eine größere Wiederverwendbarkeit besitzt. Leider lässt sich, wie die Methode getSession zeigt, diese Adapter-Schicht nicht vollautomatisch generieren. Es ist deshalb von Vorteil, wenn das Framework selbst solche Adapter bereits besitzt oder durch Interfaces
und Factory-Objekte so gekapselt ist, dass die direkte Verwendung von Dummy-Objekten möglich wird. Idealerweise bringt das Framework in einem zusätzlichen Paket sogar eine Reihe von
Dummy-Klassen mit, die für verschiedene Testzwecke verwendet werden können. Dies würde die Testentwicklung in Framework-abhängigen Projekten stark vereinfachen.
Umgekehrt ist es aber auch sinnvoll, bei der Veröffentlichung einer Komponente oder eines Frameworks eine Sammlung von Tests mit herauszugeben, die demonstriert, dass die Komponente
beziehungsweise das Framework sich entsprechend einer gegebenen Spezifikation verhält. Dies dient gleichzeitig dazu, das Zutrauen der Komponentennutzer zu erhöhen und den Nutzern durch
Beispiele die Anwendung zu erklären.
Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012