114
0 Untersuchung der Auswirkungen von Abhängigkeiten auf den Test von objektorientierter Software Diplomarbeit Alexander Müller Niedernhausen, 05. Februar 2004 Betreuer: Prof. Dr. Hans-Werner Six Lehrgebiet Software Engeneering Fachbereich Informatik FernUniversität in Hagen

Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

0

Untersuchung der Auswirkungen von Abhängigkeiten auf den Test von

objektorientierter Software

Diplomarbeit

Alexander Müller

Niedernhausen, 05. Februar 2004

Betreuer: Prof. Dr. Hans-Werner Six

Lehrgebiet Software Engeneering Fachbereich Informatik

FernUniversität in Hagen

Page 2: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

1

��������� Hiermit versichere ich, die vorliegende Arbeit selbstständig und nur unter Nutzung der angegebenen Literatur und Hilfsmittel angefertigt zu haben. Wörtlich übernommene Sätze und Satzteile sind als Zitate belegt, andere Anlehnungen hinsichtlich Aussage und Umfang unter den Quellenangaben kenntlich gemacht. Die Arbeit hat in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegen. Niedernhausen, 05. Februar 2004 Alexander Müller (Matr.-Nr.: 5330491)

Page 3: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

2

� � �� � In dieser Arbeit werden die Auswirkungen von bestimmten Arten von Klassen-Abhängigkeiten auf die Testbarkeit von Softwaresystemen untersucht. Das Testen ist fester Bestandteil des Softwareentwicklungsprozesses, denn nur durch systematische Tests kann ein vorgegebener Qualitätsstandard erreicht werden. Die Gestaltung der Architektur und die konkrete Implementierung eines Softwaresystems können dabei zu einer guten oder schlechten Testbarkeit führen. In einigen wissenschaftlichen Arbeiten wurde untersucht, inwieweit die Eigenschaften von Klassen sowie deren Beziehungen untereinander zu einer guten Testbarkeit beitragen können. Die vorliegende Diplomarbeit reiht sich in diesen Kontext ein, wobei der Fokus auf dem Test einzelner Klassen liegt (im Gegensatz zum Test von Gesamtsystemen). Der erste Teil dieser Arbeit untersucht die Auswirkungen von indirekten Abhängigkeiten auf die Testbarkeit. Nach der Auswahl bestimmter Klassen aus einem Fallbeispiel werden entsprechende Testklassen für den Unit-Test entwickelt. Die Aufwände und Probleme, die infolge indirekter Abhängigkeiten bei den Tests auftreten, werden analysiert und dokumentiert, und es wird eine tabellarische Auswertung dieser Probleme erstellt. Der zweite Teil der Arbeit testet die Hypothese, ob mithilfe einer vorgegebenen Metrik Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit ermöglicht, ohne dass die nötigen Änderungsaufwände dabei unverhältnismäßig hoch sind. Den Abschluss der Arbeit bilden einige Entwurfsrichtlinien, die aus den gewonnenen Erkenntnissen erarbeitet worden sind, und deren Einhaltung eine bessere Testbarkeit von Softwaresystemen gewährleisten kann.

Page 4: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

3

� � ��� � ���� Ich möchte mich bedanken bei Dipl.-Ing. Stefan Jungmayr für die Betreuung während der Erstellung dieser Arbeit. Er hat mir die Möglichkeit gegeben, an diesem interessanten Thema zu arbeiten. In vielen Mails und in oft mehrstündigen Telefonaten hat er mich stets wieder in die richtige Richtung gebracht, wenn ich thematisch abgeschweift bin. Ein besonderer Dank gilt auch meinen Eltern und meiner Familie, die mich stets uneingeschränkt unterstützt haben. Besonders meine liebe Frau Karina hatte während der letzten zwölf Monate besonders viel ertragen wegen meiner stark eingeschränkten Verfügbarkeit. Bei Dr. Dietmar Grubert aus Wiesbaden möchte ich mich ebenfalls herzlich bedanken für die konstruktiven Verbesserungsvorschläge.

Page 5: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

4

��� � � � � � �� � �� � ��� � ����� � ��������������������������������������������������������������������������������������������������������������

1.1 Motivation ............................................................................................................... 5 � � ���� �� �� � ��������������������������������������������������������������������������������������������������������

2.1 Wichtige Begriffe..................................................................................................... 9 2.2 Metriken................................................................................................................ 13 2.3 Werkzeuge, die im Rahmen dieser Arbeit verwendet werden ............................... 14

� � ��� � � �� ! � �� � � � �" � � �� �� � �������������������������������������������������������������������

3.1 Fallbeispiel SeminarIS .......................................................................................... 15 3.2 Ansatz zur Erstellung von Testklassen.................................................................. 17 3.3 Ansatz zur Untersuchung von indirekten Abhängigkeiten...................................... 18 3.4 Ansatz zur Untersuchung einer Hypothese bezüglich der Metrik DSTMh.............. 19 3.5 Gesamtstrategie.................................................................................................... 20

# " � � �� �� � ���� ���� ��� � � � � � ������� � � � ����������������������������������������������

4.1 Auswahl von Klassen............................................................................................ 21 4.2 Untersuchung der Klasse FSVErfassenK.............................................................. 25 4.3 Untersuchung der Klasse RechnungsempfaengerRelation ................................... 29 4.4 Untersuchung der Klasse SpaltenModel ............................................................... 33 4.5 Zusammenfassung und Auswertung ..................................................................... 36

� " � � �� �� � ���� ��� �$ % & � � � � � � � ' ���� � � � �( � �������� ���������������������)

5.1 Hypothese............................................................................................................. 38 5.2 Untersuchung von Abhängigkeiten anhand der Metrik DSTMh ............................. 38 5.3 Untersuchung von zufällig ausgewählten Abhängigkeiten..................................... 47 5.4 Exemplarische Refaktorisierung............................................................................ 50 5.5 Vergleich und Auswertung .................................................................................... 51

* + � � � �� , � � & � � �� - � ��������������������������������������������������������������������������������������

6.1 Die zyklische Abhängigkeit zwischen einer Klasse und ihrem zugehörigen Interface................................................................................................................ 53

. / �� � - - � �0� � � ���������������������������������������������������������������������������������������������)

7.1 Die Ergebnisse der untersuchten Fragen.............................................................. 58 7.2 Wege zu testfreundlicher Architektur..................................................................... 58 7.3 Ausblick ................................................................................................................ 63

�� � �� , � � ��� � � � � �����������������������������������������������������������������������������������������������**

A.1 Test der Klassen Geschaeftspartner, Firma, Person............................................. 66 A.2 Test der Klasse FSVErfassenK............................................................................. 72 A.3 Test der Klasse RechnungsempfaengerRelation .................................................. 79 A.4 Test der Klasse SpaltenModel ............................................................................ 100

�� � ��1 2 � 0� � ��� �� ����� � �1 � �� � � �34 � � � ��� � � ���� � � � � �����������������������������5 ) �� � ��6 2 � 0� �� �� � � �������������������������������������������������������������������������������������������������

Page 6: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

5

� ����� � ���

Dieses Kapitel bietet einen Einstieg in das Thema Testbarkeit sowie einen Überblick über die Inhalte und Ziele dieser Arbeit. Die Wichtigkeit der zu untersuchenden Probleme soll dabei verdeutlicht werden. 1.1 Motivation Von Software gesteuerte Geräte durchdringen immer mehr Bereiche des täglichen Lebens. Die Möglichkeit der potentiellen Schadenshöhe durch fehlerhafte Software wird dabei immer höher, z.B. bei der Steuerung von medizinischen Geräten oder atomaren Anlagen, bei Flugzeugen oder Raumfahrzeugen. Ein aktuelles Beispiel, bei dem ein hoher Schaden entstanden ist, ist die Einführung der LKW – Maut in der Bundesrepublik Deutschland, bei der unter anderem durch Softwareprobleme monatlich Millionensummen an Schäden entstehen. Viele wissenschaftliche Untersuchungen haben belegt, dass die Beseitigungs- und Schadenskosten eines Software-Fehlers umso niedriger sind, je früher im Softwareentwicklungszyklus der Fehler entdeckt und behoben wird. Eine Zusammenstellung der Ergebnisse solcher Untersuchungen findet man zum Beispiel in [McCo93]. Testaktivitäten sind daher notwendig, um eine hohe Qualität der erzeugten Software zu gewährleisten, und um sicherzustellen dass das erzeugte Produkt seine Spezifikationen und Anforderungen erfüllt. Die Testmaßnahmen sollen durch alle Entwicklungszyklen hindurch angewendet werden, um die möglichst frühzeitige Erkennung von Fehlern abzusichern. Der zeitliche Anteil von Testaktivitäten am Softwareentwicklungsprozess ist erheblich. Ein wichtiges Ziel bei der Planung und Erstellung von Softwaresystemen ist daher, die Aufwände für die Erstellung, Durchführung und Pflege von Tests möglichst gering zu halten. Es liegt auf der Hand, dass durch eine geschickte Strategie beim Entwurf der Gesamtarchitektur in diese Richtung hin gewirkt werden kann. Ein Teil dieser Testmaßnahmen sind Unit-Tests, bei denen einzelne Klassen1 isoliert getestet werden. Diese Tests werden in der Regel von den Softwareentwicklern selbst (und nicht von Qualitätssicherungs-Personal) durchgeführt, und sie sollen dazu dienen, dass die Klassen einzeln gegen ihre jeweilige Spezifikation getestet werden können. Die Testbarkeit von Klassen und von Softwaresystemen ist definiert worden als der Grad, in dem ein Softwareprodukt2 das Testen in einem gegebenen Kontext erleichtert [Jung03]. Bei der Entwicklung von Softwaresystemen sollte stets das Ziel einer guten Testbarkeit angestrebt werden, um optimale Qualität der Testergebnisse zu erreichen und Testaufwände zu minimieren ([Jung02], [Merl03]). In der vorliegenden Diplomarbeit werden einige Richtlinien erarbeitet, mit denen dieses Ziel erreicht werden kann, wobei dem Einfluss von Klassen-Abhängigkeiten auf die Testbarkeit das Haupt-Augenmerk gilt. Es wird sich zeigen, dass diese Richtlinien nicht den Grundsätzen und Prinzipien der Objektorientierten Softwareentwicklung widersprechen. 1 Betrachtet wird ausschließlich die objektorientierte Softwareentwicklung. Diese Aussagen lassen sich aber auch auf andere (z.B. modulbasierte) Softwaresysteme übertragen. 2 übersetzt aus dem englischen („software artifact, a document or product created during software development. [...]“). Dieser Begriff bezieht sich also ausdrücklich auch auf Entwurfsdokumente, doch diese Tatsache läßt sich nur schwer in einer wörtlichen Übersetzung wiedergeben.

Page 7: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Einleitung

6

Das Geheimnisprinzip besagt, dass einzelne Klassen ihre Daten privat (und für andere Klassen unsichtbar) halten, so dass möglichst keine Daten direkt und global zugänglich sind. Wenn aus einer Klasse heraus auf Attribute einer anderen Klasse zugegriffen wird (gemeint ist der Direktzugriff, keine get-Methode), so entsteht eine Abhängigkeit, die das Testen erschwert. Wird an den betroffenen Attributen etwas geändert (z.B. der Name, der Datentyp, oder die relative Position des Attributes innerhalb der anderen Attribute), so muss die zugreifende Klasse neu compiliert werden3. Im günstigsten Fall reicht diese Maßnahme für die abhängige Klasse aus, in aller Regel muss aber jeder Zugriff auf die geänderten Attribute überarbeitet und umcodiert werden. Weil auch der Code für den Klassentest der abhängigen Klasse anschließend angepasst werden muss, wird dabei die Pflege der Testklassen sehr aufwändig. Häufig kommt als zusätzliches Problem noch hinzu, dass dieser Direktzugriff nicht nur von einer einzelnen Klasse durchgeführt wird. Es ist von Vorteil, wenn ein Softwaresystem sich in Klassen aufteilt, welche eindeutig gegeneinander abgegrenzt sind bezüglich Ihrer Aufgaben. In der Objektorientierten Softwareentwicklung nennt man diesen Sachverhalt das Prinzip der losen Kopplung. Kleinere, überschaubare Einheiten lassen sich besser testen als ein großes Gesamtsystem, und durch die klare Definition der Aufgaben und Zuständigkeiten einer Klasse lassen sich leichter Testaktivitäten und Testziele festlegen. Bei der Beachtung der beiden genannten Prinzipien resultiert ein Gesamtsystem, welches relativ wenige Abhängigkeiten zwischen einzelnen Klassen aufweist. Dies ist wichtig für einen isolierten Test der einzelnen Klassen. Hat ein System nämlich relativ viele Klassenabhängigkeiten, so müssen bei Klassentests häufig fremde Dritt-Klassen zwangsweise instantiiert und verwendet werden. Je mehr solcher Fremdklassen in einer Testklasse verwendet werden, desto höher ist die Wahrscheinlichkeit, dass Fehler aus den Fremdklassen den Test stören, oder gar einzelne Fehler aus der zu testenden Klasse aufheben, die dann nicht gefunden werden. Im Verlauf dieser Arbeit werden anhand eines Fallbeispiels viele Störungen aufgezeigt, die durch solche Abhängigkeiten entstehen. Um Herstellungskosten für Softwaresysteme zu reduzieren und Tests möglichst systematisch und häufig durchzuführen, bietet es sich an, Testaktivitäten zu automatisieren und unmittelbar nach einem Buildprozess durchführen zu lassen. Wenn die automatisierten Tests gründlich und umfassend sind, bekommen Entwickler einen sehr guten Überblick, ob Codeänderungen zu Fehlern geführt haben, und sie können bei erfolgreichen Tests auch darauf vertrauen, dass sich durch die letzten Änderungen keine Verletzungen von Spezifikationen in den betroffenen Klassen eingestellt haben. Um Tests automatisieren zu können, bedarf es jedoch mit wachsender Systemgröße bestimmter Voraussetzungen. Dabei spielt der Wartungsaufwand eine große Rolle: Wenn Änderungen an einer Klasse die Neucompilierung oder gar den anschließenden Umbau vieler Testklassen erfordern, dann besteht die Gefahr, dass die Testklassen nicht mehr ausreichend oder gar nicht mehr gepflegt werden, insbesondere wenn die zuständigen Entwickler unter Termindruck stehen. Auch dieser Gefahr kann man mit einer guten Gesamtarchitektur entgegentreten, wie sich später zeigen wird.

3 Dieses Problem entsteht bei jeder Änderung der Klasse, die das Attribut bzw. die Attribute bereitstellt, sofern die abhängige Klasse in einer Programmiersprache geschrieben ist, die compiliert werden muß (also keine Scriptsprache ist).

Page 8: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Einleitung

7

Bei den Abhängigkeiten zwischen einzelnen Klassen gibt es verschiedene Abhängigkeitstypen, die unterschiedliche Folgen für Klassentests nach sich ziehen. Ziel dieser Arbeit ist die Untersuchung einiger dieser Typen. Anhand eines Fallbeispiels wird dargestellt, wie sich diese Abhängigkeiten konkret auswirken, und wie man Probleme bereits beim Entwurf hätte vermeiden können. Es wird jedoch keine umfassende Ausarbeitung von Designrichtlinien zur Verbesserung der Testbarkeit im Allgemeinen erarbeitet werden, dafür soll auf weiterführende Literatur verwiesen werden, z.B. [Link02] oder [Lako96]. 1.2 Aufgabenstellung In dieser Arbeit sollen zwei Fragen untersucht werden, deren Beantwortung zur Erreichung einer besseren Testbarkeit beitragen kann: Zentrale Fragen

1) Wie wirken sich indirekte Abhängigkeiten auf den Test aus (insbesondere auf Testfallerstellung, Test-Setup und Fehler-Lokalisierung)?

2) Ist die Metrik DSTMh gut geeignet, unbeabsichtigte bzw. leicht refaktorisierbare fest-verdrahtete Abhängigkeiten aufzuspüren?

Die erste Aufgabe ist die Untersuchung indirekter Klassenabhängigkeiten (zur Definition siehe Kapitel 2) bezüglich ihrer Auswirkungen auf die Testbarkeit. Dabei soll für verschiedene Abhängigkeiten jeweils eine Bewertung für deren jeweilige Auswirkung auf die Testbarkeit erarbeitet werden:

1) Aufwand zur Erstellung von Testklassen, dabei insbesondere der Aufwand zur Ergreifung aller zusätzlichen Maßnahmen, die durch die jeweilige Abhängigkeit erforderlich werden

2) Aufwand für Codeanalyse und Fehlerbehandlung, der durch eine Abhängigkeit zusätzlich nötig wird

3) Bewertung der Komplexität, die durch diese Abhängigkeit in die beteiligten Klassen eingebracht wird

4) Versteckte Zusatzaufwände und Implikationen, die durch weitere Abhängigkeiten „hinter“ der untersuchten Abhängigkeit hinzukommen (also weitere indirekte Abhängigkeiten hinter der untersuchten indirekten Abhängigkeit).

Die zweite Aufgabe ist die Erprobung einer Hypothese. Es soll untersucht werden, ob es mit Hilfe einer automatisch berechneten Metrik (DSTMh, zur Definition siehe Kapitel 2) möglich ist, Problemstellen bezüglich der Testbarkeit innerhalb eines gegebenen Softwaresystems schnell und effektiv zu finden. Dabei wird bewertet, ob diese Stellen mit relativ geringem Aufwand refaktorisierbar sind, und diese Änderung zu einer wesentlichen Verbesserung bezüglich der Testbarkeit beitragen kann. Die dritte Aufgabe schließlich ist die Beschreibung von Möglichkeiten, wie man bereits in der Entwurfs- und Codierphase erreichen kann, dass ein testfreundliches Gesamtsystem entsteht. Hierbei sollen Richtlinien erarbeitet werden, die vorgeben, wie man die Testbarkeits-Probleme vermeiden kann, die im Laufe dieser Arbeit anhand eines Fallbeispiels aufgezeigt werden.

Page 9: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Einleitung

8

Eine Nebenfrage, die im Rahmen der Untersuchungen ebenfalls beantwortet werden soll, lautet: „Welche Testprobleme hätte man in einem Fallbeispiel (Sopraprojekt, siehe Abschnitt 3.1) schon vor der Implementierung (d.h. aufgrund der Anforderungen und des Designs) identifizieren können?“

Page 10: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

9

� � ���� �� �� �

Einige wichtige Begriffe, Metriken und Werkzeuge, deren Verständnis für die folgenden Kapitel grundlegend ist, werden in diesem Kapitel vorgestellt und erklärt. 2.1 Wichtige Begriffe Analyse-Klassenmodell Im Analyse-Klassenmodell (siehe [Six03], Kurseinheit 5) unterscheidet man drei Arten von Analyseklassen:

• Entitätsklassen (entity classes). Diese Klassen repräsentieren langlebige Informationen im Anwendungssystem. Aus den Anwendungsfällen werden einzelne Objekte sinnvoll isoliert, für diese werden Attribute identifiziert, und damit werden Entitätsklassen geschaffen.

• Schnittstellenklassen (boundary classes). Sie bündeln die Interaktionen zwischen den Akteuren und dem Anwendungssystem, und dienen zum Beispiel zur Erstellung von Benutzeroberflächen oder zur Umsetzung von technischen Kommunikations-protokollen.

• Kontrollklassen (control classes). Die fachliche Logik einzelner Anwendungsfälle wird jeweils in einzelnen Kontrollklassen abgebildet. Diese Klassen dienen zur Steuerung und Kontrolle der an den Anwendungsfällen beteiligten Entitätsklassen.

Abhängigkeit Die Abhängigkeit ist eine gerichtete, transitive Relation zwischen Klassen. Die Klasse A ist abhängig von der Klasse B, falls A nicht compiliert oder gelinkt werden kann ohne die Klasse B4. Die abhängige Klasse A wird dabei Client-Klasse genannt (oder auch dependent class oder client class). Die Klasse B heißt Supplier-Klasse (oder auch dependee class oder supplier class). Eine gute Testbarkeit für Klassentests ist gegeben, wenn sich einzelne Klassen isoliert testen lassen. Es zeigt sich aber, dass durch bestimmte Abhängigkeitsbeziehungen solche isolierten Tests erschwert oder sogar unmöglich werden. Dabei unterscheiden wir drei Arten von Abhängigkeiten: festverdrahtet, semi-festverdrahtet und nicht festverdrahtet.

4 Dieser Satz ist die Definition aus [Lako96], Section 3.3. Diese Definition ist stark an die Sprachen C und C++ angelehnt, bei der Sourcecode compiliert und gelinkt werden muß. Übertragen auf andere Sprachen könnte der Linkprozeß entfallen (z.B. bei Java), oder z.B. bei Scriptsprachen in Microsoft Windows müßte es analog heißen, daß eine Type Library der Klasse B vorliegen muß, oder die Klasse B im System registriert sein muß, damit A zur Laufzeit wie geplant funktioniert und keine Exception erzeugt.

Page 11: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Grundlagen

10

Festverdrahtete Abhängigkeit (physical oder hard-wired dependency)5 Die Abhängigkeit einer Client-Klasse von einer Supplier-Klasse ist festverdrahtet, falls es unmöglich ist,

1) ein Implementationsdetail der Supplier-Klasse zu ersetzen,

und

2) den Wert eines nicht-privaten Attributes der Supplier-Klasse zu setzen, auf das die Client-Klasse zugreift,

ohne danach die Implementierung der Client-Klasse (bzw. eine ihrer Basisklassen) oder der Supplier-Klasse zu verändern. Festverdrahtete Abhängigkeiten machen es unmöglich, die Supplier-Klasse (durch eine andere Klasse) zu ersetzen, oder den Wert von benutzten Attributen zu verändern. Genauer gesagt ist die Abhängigkeit einer Client-Klasse von einer Supplier-Klasse festverdrahtet, wenn einer der folgenden Punkte zutrifft:

1) Die Supplier-Klasse ist eine direkte oder indirekte Basisklasse der Client-Klasse. Die Supplier-Klasse kann nicht durch eine andere Klasse ersetzt werden ohne die Änderung des Sourcecodes der Client-Klasse (oder einer ihrer Basisklassen).

2) Die Client-Klasse instantiiert die Supplier-Klasse selbst. In diesem Fall kann die Supplier-Klasse nicht durch eine andere Klasse ersetzt werden. Um diesen Fall zu identifizieren, verwenden wir folgende Heuristik: Die Client-Klasse instantiiert die Supplier-Klasse durch Aufruf eines Konstruktors oder eines Pseudo-Konstruktors der Supplier-Klasse. Ein Pseudo-Konstruktor einer Klasse A ist eine statische Methode mit einem Rückgabewert vom Typ der Klasse A oder eines Interfaces, welches von A implementiert wird. Beispiele für Pseudo-Konstruktoren sind die Methode getInstance des Singleton-Patterns [Gamm94] oder die Methode erzeuge im Persistenz-Rahmenwerk [Six03].

3) Die Supplier-Klasse ist als final deklariert. In diesem Fall können keine Klassen von der Supplier-Klasse abgeleitet werden.

4) Alle Methoden der Supplier-Klasse, auf die von der Client-Klasse (oder ihrer Instanzen) zugegriffen wird, sind nicht überschreibbar, und alle Attribute der Supplier-Klasse, auf die aus der Client-Klasse (oder ihrer Instanzen) zugegriffen wird, dürfen nicht gesetzt werden. Das heißt, dass alle Methoden der Supplier-Klasse, die aus der Client-Klasse (oder ihrer Instanzen) aufgerufen werden, statisch oder final sind, und alle von der Client-Klasse verwendeten Attribute final sind.

Semi-Festverdrahtete Abhängigkeit (semi-hard-wired dependency) Die Abhängigkeit einer Client-Klasse von einer Supplier-Klasse ist semi-festverdrahtet, wenn es möglich ist, einige (aber nicht alle) Implementierungs-Details der Supplier-Klasse zu ersetzen, auf die die Client-Klasse zugreift, ohne die Implementierung der Client-Klasse (oder eine ihrer Basisklassen) zu verändern. Genauer gesagt ist eine Abhängigkeit einer Client-Klasse von einer Supplier-Klasse semi-festverdrahtet, wenn

5 Diese Definition (und die folgenden bezügl Abhängigkeitstypen) ist entnommen aus [Jung03], und vom Verfasser dieser Diplomarbeit aus dem englischen ins deutsche übersetzt worden.

Page 12: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Grundlagen

11

1) die Abhängigkeit nicht festverdrahtet ist, und 2) einige, aber nicht alle Methoden der Supplier-Klasse, auf die von der Client-Klasse

(bzw. deren Instanzen) zugegriffen wird, nicht überschrieben werden können, oder einige (aber nicht alle) Attribute der Supplier-Klasse, auf die von der Client-Klasse (bzw. von deren Instanzen) zugegriffen wird, dürfen nie im Wert verändert werden. Dies bedeutet, dass einige Methoden der Supplier-Klasse, welche von der Client-Klasse (bzw. deren Instanzen) aufgerufen werden, statisch oder final sind, oder dass einige Attribute der Supplier-Klasse, die von der Client-Klasse verwendet werden, final sind.

Typabhängigkeit (type dependency) Die Abhängigkeit einer Client-Klasse von einer Supplier-Klasse wird typabhängig genannt, wenn es möglich ist, jedes Implementierungsdetail der Supplier-Klasse zu ändern, von dem die Client-Klasse abhängig ist, ohne die Implementierung der Client-Klasse anpassen zu müssen. Genauer gesagt ist eine Abhängigkeit einer Client-Klasse von einer Supplier-Klasse eine Typabhängigkeit, wenn

1) die Abhängigkeit nicht festverdrahtet ist, und 2) alle Methoden der Supplier-Klasse, auf die von der Client-Klasse (bzw. deren

Instanzen) aus zugegriffen wird, überschrieben werden können, und alle Attribute der Supplier-Klasse, auf die von der Client-Klasse (bzw. deren Instanzen) aus zugegriffen wird, wertmäßig änderbar sind. Dies bedeutet, dass keine Methode der Supplier-Klasse, welche von der Client-Klasse (oder deren Instanzen) aus aufgerufen wird, statisch oder final ist, und dass kein von der Client-Klasse verwendetes Attribut final ist.

Eine festverdrahtete Abhängigkeit kann versehentlich entstehen, eine Typabhängigkeit entsteht jedoch meist durch bewusstes Design. Das Systemdesign sollte es beispielsweise unmöglich machen, dass versehentlich eine festverdrahtete Abhängigkeit von außerhalb eines Pakets in ein Paket hinein entstehen kann. Direkte Abhängigkeiten (direct dependency) und Indirekte Abhängigkeiten (implied oder indirect dependency)

Abbildung 1: Direkte und Indirekte Abhängigkeiten

Die Abbildung 1 veranschaulicht den Unterschied zwischen direkter und indirekter Abhängigkeit: Während die Klasse A direkt von B und die Klasse B direkt von C abhängig ist, ist A indirekt von C abhängig.

Page 13: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Grundlagen

12

Gute Testbarkeit Ein leicht testbares (und damit testfreundliches) Softwaresystem zeichnet sich durch folgende Eigenschaften bezüglich der Abhängigkeiten von Klassen aus:

1) Die einzelnen Klassen befolgen das Prinzip der losen Kopplung. Dies bedingt unter anderem, dass möglichst wenige Abhängigkeiten zwischen den beteiligten Klassen bestehen.

2) Wenn eine Abhängigkeit notwendig und implementiert ist, dann ist sie weder festverdrahtet, noch semi-festverdrahtet.

3) Es existieren keine zyklischen Abhängigkeitsbeziehungen. Vererbungsbeziehungen erschweren die Testbarkeit, da einzelne Objektinstanzen nicht mehr eindeutig einer einzelnen Klasse zugeordnet werden können, und dadurch verschiedene Variationsmöglichkeiten getestet werden müssen. Refaktorisierung Die Umgestaltung eines Softwaresystems mit einem bestimmten Ziel, beispielsweise zur strikten Einhaltung von Programmierprinzipien oder zur Verbesserung der Testbarkeit, wird Refaktorisierung (engl. refactoring) genannt. Stub- und Mockobjekte Eine Client-Klasse, für die ein Unit-Test durchgeführt wird, kann verschiedene Supplier-Klassen zwingend benötigen, ohne die eine Instantiierung oder Verwendung der Client-Klasse nicht möglich ist. Daher ist es häufig sinnvoll, für Testzwecke Ersatz-Objekte einzusetzen, welche exakt die gleiche Signatur (öffentliche Methoden und Attribute) wie die zu ersetzende Supplier-Klasse hat. Folgende Möglichkeiten für solche Ersatz-Objekte stehen zur Auswahl (vergl. [Link02]):

1) Stub-Objekte: Sie sind Platzhalter, in der Regel ohne richtige Funktionalität, die nur dazu dienen, die Client-Klasse fehlerfrei compilieren zu können, ohne die echte Supplier-Klasse einsetzen zu müssen.

2) Mock-Objekte: Diese Objekte enthalten eine nennenswerte Code-Implementierung, die entweder an die Funktionalität der echten Supplier-Klasse angelehnt ist, oder speziell für die Aufgaben der Client-Klassentests zugeschnitten ist.

Das Entwurfsmuster Abstrakte Fabrik (abstract factory oder class factory) Eine Abstrakte Fabrik ist eine Klasse, die Methoden zur Instantiierung von Objekten enthält [Gamm94]. Dadurch wird vermieden, dass Client-Klassen direkt den Konstruktor ihrer Supplier-Klasse verwenden, und damit bereits zwingend eine festverdrahtete Abhängigkeit aufbauen.6 6 In Microsoft COM z.B. ist die Erzeugung von Klasseninstanzen grundsätzlich nur über class factories möglich.

Page 14: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Grundlagen

13

Laufzeit-Ausnahmen (Exceptions) Verschiedene Programmiersprachen, unter anderem Java, unterstützen die Technik des so genannten structured exception handling, einer systematischen Vorgehensweise, um Ausnahmesituationen während der Laufzeit eines Programms zu behandeln. Dabei werden in bestimmten Situationen so genannte Exceptions erzeugt, welche die normale Abfolge der Code-Instruktionen unterbrechen, weil die weitere Abarbeitung nicht mehr möglich ist, zum Beispiel bei Speichermangel, Division durch Null oder ähnlichem. Es können im Code an bestimmten Punkten (so genannte catch-Blöcke) Behandlungen für solche Ausnahmen programmiert werden, bei denen eine aufgetretene Exception aufgefangen und analysiert werden kann anhand einiger Merkmale (z.B. Fehlernummer und Fehlertext). Abkürzungen Eine zu Testende Klasse, auf die von einer zugehörigen Testklasse aus Bezug genommen wird, nennt man class unter test, abgekürzt CUT. Eine zu Testende Methode heißt dabei method unter test, kurz MUT. 2.2 Metriken In der folgenden Tabelle werden einige Metriken vorgestellt, auf die im weiteren Verlauf dieser Diplomarbeit immer wieder Bezug genommen wird7.

Metrik Beschreibung ACD Average component dependency, durchschnittliche Anzahl der Abhängigkeiten je

Klasse. Maß für das Gesamtsystem. NDC Number of dependency cycles, Gesamtanzahl der Abhängigkeits-Zyklen. Maß für das

Gesamtsystem. NFD Number of feedback dependencies. Maß für das Gesamtsystem. Je niedriger der

Wert, desto weniger Aufwand muss in das Durchbrechen von Abhängigkeits-Zyklen investiert werden.

NSBC Number of stubs needed to break cycles. Maß für das Gesamtsystem. Je niedriger der Wert, desto weniger Aufwand muss in das Durchbrechen von Abhängigkeits-Zyklen investiert werden.

NCDC Number of components within dependency cycles. Maß für das Gesamtsystem. Je niedriger der Wert, desto weniger Klassen sind an Zyklen beteiligt.

CBO Coupling between objects. Maß für eine Klasse. Anzahl der Klassen, von denen die bewertete Klasse direkt abhängig ist.

CBOi Wie CBO, nur werden die direkten und die indirekten Abhängigkeiten gemessen. DSTMh8 Dependencies on statement level, hardwired. Maß für eine konkrete Abhängigkeit.

Anteil der festverdrahteten und semi-festverdrahteten Verwendungen der supplier class im Verhältnis zu allen Verwendungen der supplier class (semi-/festverdrahtet und nicht-festverdrahtet). Die Mehrfach-Verwendung von Objekten der Supplier-Klasse derselben Kategorie durch eine Methode der Client-Klasse wird dabei nur einfach gezählt.

Tabelle 1: Die verwendeten Metriken

7 Diese Metriken sowie deren Bedeutung wurden entnommen aus [Jung02] sowie aus der Dokumentation zu ImproveT, welches im Anschluß noch beschrieben wird. Es wird an dieser Stelle keine mathematische Definition angegeben, dies ist related work. 8 Diese Metrik wurde früher sub_d_hw genannt, beispielsweise in der Diplomarbeit von Merl [Merl03]

Page 15: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Grundlagen

14

2.3 Werkzeuge, die im Rahmen dieser Arbeit verwendet werden Zur Quantifizierung von Metriken wird das Tool ImproveT verwendet, welches von Dipl-Ing. Stefan Jungmayr an der FernUniversität in Hagen entwickelt worden ist. Edgar Merl, ein Student an der FernUniversität, hat im Rahmen seiner Diplomarbeit [Merl03] mit Design2Test eine Einbindung dieses Tools in die kommerzielle Entwicklungsumgebung Together ControlCenter® realisiert. Die beiden genannten Tools werden im Rahmen dieser Diplomarbeit zur Messung der Metriken aus Tabelle 1 verwendet. Das hier verwendete Fallbeispiel, welches im folgenden Abschnitt vorgestellt wird, wurde in der Programmiersprache Java erstellt. Daher wurde ausschließlich diese Sprache durchgehend innerhalb der vorliegenden Arbeit verwendet. Java wurde von der Firma Sun Microsystems [URL: Sun] entwickelt, und es gibt eine kostenlose und ausführliche Online-Dokumentation im Internet [URL: Java]. Klassentests werden im Rahmen dieser Diplomarbeit mit JUnit [URL: JUnit] implementiert. JUnit ist ein kostenlos verfügbares Framework von Klassen, welches die Erstellung von Testklassen speziell für die Sprache Java unterstützt (es gibt auch Derivate von JUnit für andere Sprachen, z.B. CppUnit [URL: CppUnit] für die Sprache C++). Diese Unit-Test-Tools bieten eine Unterstützung für die Zusammenstellung von so genannten Test-Suites, einer Kombination von Testfällen, die in definierter Abfolge verarbeitet werden. Dabei kann man durch Konstrukte des Frameworks sämtliche Anforderungen und Erwartungen an die zu testende Klasse formulieren. Es können dabei gezielt Exceptions abgefangen oder deren Erzeugung durch die CUT (class under test, siehe Abschnitt 2.1) sichergestellt werden. JUnit bietet Visualisierungs- und Protokollierungsmöglichkeiten für die Ergebnisse der Testläufe, bei denen schnell erkennbar wird, ob sämtliche Erwartungen an die CUT durch den Test verifiziert werden konnten. Ein wichtiger Mechanismus von JUnit ist die Bereitstellung der beiden Methoden setUp() und tearDown(). Die erste Methode wird vom Framework automatisch vor jeder einzelnen Testmethode aufgerufen, und die zweite Methode automatisch nach jeder Testmethode. Zusammen heißen die beiden Methoden Testfixtures, und sie vereinfachen die immer wiederkehrende Notwendigkeit, benötigte Hilfsvariablen oder -klassen neu zu initialisieren bzw. zu instantiieren. Das JUnit-Framework wird von der Entwicklungsumgebung Together ControlCenter® [URL: Together] unterstützt, so dass damit ein effizientes Vorgehen für die hier verwendeten Java-basierte Testfälle möglich ist. Together ControlCenter wird im Rahmen dieser Arbeit für die Erstellung von Testklassen und die Weiterentwicklung des Sourcecodes des verwendeten Fallbeispiels (siehe nächster Abschnitt) verwendet.

Page 16: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

15

� � ��� � � �� ! � �� � � � �" � � �� �� � ���

An dieser Stelle wird das verwendete Fallbeispiel SeminarIS vorgestellt. Des Weiteren wird die grundsätzliche Vorgehensweise zur Erstellung von Testklassen, und zur Untersuchung von indirekten Abhängigkeiten und einer Hypothese, vorgestellt. Eine Beschreibung der Gesamtstrategie des Vorgehens schließt dieses Kapitel ab. 3.1 Fallbeispiel SeminarIS Als Fallbeispiel wird ein Software-Projekt verwendet, welches im Rahmen eines Praktikums im Sommersemester 2001 an der FernUniversität in Hagen, Lehrgebiet Praktische Informatik III, durchgeführt worden ist. Dabei geht es um ein Seminar-Informationssystem, abgekürzt SeminarIS, mit dem Seminare, Dozenten und Seminarteilnehmer verwaltet werden können. Das Projekt wurde von acht Studenten in der Sprache Java erstellt. Als Datenbank wurde das System InstantDB verwendet, welches über ein Persistenz-Rahmenwerk angesprochen wird. InstantDB ist ein Relationales Datenbankmanagementsystem, welches über die Datenbankschnittstelle JDBC [URL: JDBC] angesprochen werden kann und zum damaligen Zeitpunkt als Open Source zur Verfügung stand. Mittlerweile sind die Rechte an die Firma Lutris [URL: Lutris] veräußert worden, welche das Rahmenwerk nicht mehr zum Download anbietet. Als Entwicklungsoberfläche diente dem Entwicklerteam das bereits genannte Together ControlCenter. Die Abbildung 2 veranschaulicht das Domänenmodell des Fallbeispiels. In SeminarIS geht es darum, Seminarveranstaltungen zu verwalten. Dies können öffentliche Seminarveranstaltungen oder Firmen-Seminarveranstaltungen sein. An Seminaren teilnehmen können natürliche Personen, und speziell Firmen-Seminarveranstaltungen werden von einer Firma gebucht. Eine Seminarveranstaltung hat immer einen bestimmten Seminartyp, und hat in der Regel einen Seminarleiter. Seminartypen können zu Dozenten zugeordnet werden. Typische Anwendungsfälle sind die Bearbeitung von Personen- und Firmendaten durch Kundensachbearbeiter, außerdem die Bearbeitung von Seminarbelegungen und die Seminarverwaltung, also z.B. die Zuordnung von Dozenten zu Seminartypen. Speziellere Fälle sind Anfragefunktionen für Abrechnungs- oder statistische Zwecke oder das Ausdrucken bestimmter Formulare und Briefe. Die Arbeit von Edgar Merl [Merl03], auf die bereits oben Bezug genommen wurde, handelte ebenso wie die vorliegende Arbeit von diesem Fallbeispiel. Im Gegensatz zu Herrn Merl war der Verfasser dieser Arbeit jedoch nicht an der früheren Entwicklung im Rahmen des Praktikums beteiligt. Bereits in [Merl03] wurde der Versuch unternommen, die Testbarkeit des Systems spürbar zu verbessern. Merl hat dabei die Projektmetriken betrachtet, und ist dabei nach folgendem Schema vorgegangen: Suche die Abhängigkeiten mit dem höchsten rACD9-Wert, betrachte das Umfeld dieser Abhängigkeit und versuche, diese Stelle zu optimieren.

9 rACD ist die Reduktionsmetrik zu der bereits genannten Metrik ACD. Je höher der rACD – Wert, desto höher ist die Verringerung (und damit Verbesserung) des ACD-Wertes bei Entfernung der zugehörigen Abhängigkeit.

Page 17: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Vorgehensweise der Untersuchung

16

Dabei hat Merl jedoch keine durchgängige Refaktorisierung über das ganze System durchgeführt, sondern hat stets einzelne Problemstellen herausgearbeitet und jeweils gezeigt, inwieweit sich die jeweilige Einzel-Refaktorisierung auf die Testbarkeit auswirkt.

Abbildung 2: Domänenmodell von SeminarIS

Die erste Aufgabe im Rahmen der vorliegenden Arbeit bestand also zunächst darin, sämtliche Einzel-Refaktorisierungen von Merl zu analysieren und nacheinander auf das Gesamtsystem anzuwenden (es waren insgesamt neun Refaktorisierungs-Maßnahmen). Dabei entstand ein neues, stark verändertes Gesamtsystem S’, welches die in Tabelle 2 dargestellten Projektmetriken aufwies.

Page 18: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Vorgehensweise der Untersuchung

17

Metrik Wert vorher Wert nachher ACD 89,1 35,6 NFD 81,0 23,0 NSBC 39,0 19,0 NCDC 106,0 39,0 NDC 5,0 10,0

Tabelle 2: Projektmetriken des Gesamtsystems, vor und nach den Merl'schen Refaktorisierungen

Es fällt dabei besonders auf, dass es durch die Merl’schen Einzelmaßnahmen gelungen ist, die durchschnittliche Zahl der Abhängigkeiten je Klasse (ACD) im System von etwa 89 auf etwa 36 zu verringern, das entspricht fast dem Faktor 3. Auch bei den anderen Metriken, bei denen ausnahmslos ein kleinerer Wert für eine bessere Testbarkeit steht, konnten erhebliche Verbesserungen erreichen werden. Der Anstieg des Wertes NDC ist dadurch bedingt, dass größere Abhängigkeits-Zyklen in kleinere Zyklen aufgebrochen worden sind, wobei sich die Gesamtanzahl der Zyklen etwas vergrößert hat. Dieses resultierende System wurde zum Basissystem S’ aller folgenden Untersuchungen. Wenn im Verlauf dieser Arbeit Refaktorisierungsmaßnahmen vorgestellt werden, so beziehen sich diese stets auf den jetzt erreichten Systemzustand S’. Die einzelnen Refaktorisierungen wurden nicht inkrementell vorgenommen. In den folgenden Absätzen wird die Strategie erläutert, mit der im weiteren Verlauf vorgegangen wird, um Unit-Tests für einzelne Klassen dieses Fallbeispiels zu erstellen. 3.2 Ansatz zur Erstellung von Testklassen Es werden im Rahmen dieser Diplomarbeit Testklassen implementiert, die jeweils den kompletten Test einer korrespondierenden Klasse aus dem Fallbeispiel abdecken sollen. Die Testklassen sollen folgende Bereiche abdecken:

1) Instantiierung der Klasse: Abdecken aller Instantiierungsmöglichkeiten, anschließend Test auf Integrität (Klasseninvarianten).

2) Testen der Zuweisung und Abfrage von Attributen: Belegung mit Grenzwerten10, Integrität der Klasse prüfen.

3) Testen der Zustände der Klasseninstanz: vollständiges Durchlaufen aller wesentlichen Pfade von Zustandsänderungen. Testen, ob unerlaubte oder undefinierte Zustände erreicht werden. Integrität der Instanz prüfen.

4) Testen der Aufgaben der Klasse: Nutzung aller Methoden, die von einer Klasse angeboten werden. Übergabe von Parametern mit Grenzwerten, Belegung von methodenlokalen Variablen mit Grenzwerten anstreben (sofern von außen möglich). Prüfung auf korrekte Dienstausführung. Prüfung der Rückgabewerte. Prüfung auf korrekte Fehlerbehandlung.

5) Testen der Nutzung von Supplier-Klassen: Wenn die CUT die Dienste anderer Klassen nutzt, auch diese Dienstnutzung mit Grenzwerten Testen. Beachten der korrekten Fehlerbehandlung innerhalb der Supplier-Klassen sofern möglich.

10 Im Bereich von Grenzwerten sollte stets mit einer Umgebung von ± 1 um jeden Grenzwert herum getestet werden. Ein Beispiel: Eine Methode erwartet einen Parameter vom Typ unsigned integer, der nur im Intervall [0, .., 10] sinnvolle Werte enthält. Getestet werden muß daher mindestens mit den Parameterwerten 0, 1, 9, 10, 11, und 65535 (bei einer 16-Bit-Repräsentation des Datentyps).

Page 19: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Vorgehensweise der Untersuchung

18

3.3 Ansatz zur Untersuchung von indirekten Abhängigkeiten Durch die Untersuchung von exemplarischen Fällen sollen die Auswirkungen von indirekten Abhängigkeiten auf die Erstellung und Durchführung von Tests aufgezeigt werden. Indirekte Abhängigkeiten zeichnen sich dadurch aus, dass die Client-Klasse A nicht direkt abhängig ist von der Supplier-Klasse C, sondern sie ist direkt abhängig von einer anderen Klasse B, welche wiederum (direkt oder indirekt) von der Supplier-Klasse C abhängt. Das bedeutet auch, dass die Supplier-Klasse C ausgetauscht werden könnte, ohne dass A direkt davon betroffen wäre, obwohl A indirekt über mindestens eine andere Klasse von B abhängig ist (siehe Abschnitt 2.1). Ein Beispiel soll dies noch einmal verdeutlichen: Eine Klasse Protokoll zum Erstellen einer Protokoll-Datei sei direkt abhängig von der Klasse Datei. Die Klasse Datei, welche Dateioperationen bereitstellt, sei abgeleitet von der abstrakten Basisklasse FileSystemObject, von welcher alle Arten von Dateisystem-Objekten (Ordner, Dateien, Access-Control-Lists etc.) abgeleitet seien. Protokoll greift auf keine Methoden oder Attribute von FileSystemObject zu, dies wird alles von Datei gekapselt. Dann ist Protokoll indirekt abhängig von FileSystemObject. Ablauf der Untersuchung:

1) Vorbereitung

Klassenauswahl: Für den Test werden Klassen ausgewählt, die besonders viele indirekten Abhängigkeiten haben, um mit dem Test ein möglichst breites Spektrum an (möglicherweise verschiedenartigen) indirekten Abhängigkeiten abzudecken. Daher wird die Auswahl der Klassen anhand hoher Werte der Metrikdifferenz

CBOi – CBO

getroffen, welche als Selektionskriterium brauchbar erscheint11.

2) Test und Analyse

a) Testvorbereitung: Es soll eine Testklasse erstellt werden pro Klasse, die zu testen ist. Dabei werden die Aufwände für Instantiierungen und Initialisierungen („I&I“) von Objekten beobachtet, die vor den eigentlichen Testmethoden notwendig werden, um die Tests durchführen zu können. Es sollen die Aufwände und Komplexitäten in drei Abstufungen bewertet werden: einfach (z.B. keine I&I notwendig), mittel (wenig Zusatzaufwand, unkomplizierte I&I), und schwer (komplizierte I&I). Hierbei fallen insbesondere Analyseaufwände für die zu instantiierenden und initialisierenden Objekte ins Gewicht, falls dieser Vorgang nicht trivial ist.

b) Testfallerstellung: Dabei soll zunächst mit dem so genannten black-box-Ansatz

gearbeitet werden, das heißt dass die Testmethoden die funktionale Spezifikation12 der CUT abdecken müssen. Gibt es dann innerhalb der zu testenden Methoden verschiedene Verzweigungsmöglichkeiten, soll dabei jeder Ast der Verzweigungen abgedeckt werden, wodurch gegebenenfalls zusätzliche

11 CBOi – CBO = (Summe der direkten und indirekten Abhängigkeiten) – (Summe der direkten Abhängigkeiten). Zu den Metriken CBO und CBOi siehe Abschnitt 2.2. 12 Dies setzt voraus, daß die funktionale Spezifikation der Methode bekannt, dokumentiert oder wenigstens erkennbar ist, was nicht immer vorausgesetzt werden kann (wie es mehrfach im Fallbeispiel festgestellt werden mußte).

Page 20: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Vorgehensweise der Untersuchung

19

Testmethoden erstellt werden müssen. Wegen des Fokus auf die indirekten Abhängigkeiten ist darauf zu achten, dass das Wissen über die indirekten Abhängigkeiten genutzt wird, um alle möglichen Varianten oder Kombinationen von Aufrufmöglichkeiten abzudecken (d.h. ggfs. Objektparameter durch abgeleitete Objektinstanzen ersetzen, alle Variationen polymorpher Methoden aufrufen, alle Objekttypvarianten abdecken, die zwischen der CUT (= Client-Klasse) und der Supplier-Klasse liegen können). Dies kann ggfs. durch geschickte Wahl der Aufrufparameter der zu testenden Methode erreicht werden.

c) Der Erstellungsaufwand und die Komplexität der Testmethoden sollen bewertet

werden in den Abstufungen „einfach / mittel / schwer“. Die Kriterien für die Einstufung sind:

1) die eigentliche Codierungsdauer der Testmethode 2) die Anzahl der Testmethoden, die für einen vollständigen Test nötig sind 3) die Anzahl der notwendigen Aufruf-Kombinationen zur vollständigen

Abdeckung einer zu testenden Methode

d) Testdurchführung: Hierbei sollen möglichst alle Fehler in der zu testenden Klasse gefunden werden. Dies soll dadurch erreicht werden, dass bereits bei der Erstellung der Testfälle alle Annahmen, die bezüglich des Verhaltens und der Zustände der zu testenden Klasseninstanzen gemacht werden, im Testcode dokumentiert und gegebenenfalls durch gezielte Anweisungen abgesichert werden. Dadurch wird jede Abweichung von einer Annahme im Protokoll des Testlaufes festgehalten. Auch jede Laufzeit-Ausnahme, die nicht abgefangen wurde, ist dem Protokoll zu entnehmen. Laufzeit-Ausnahmen, die bewusst ausgelöst werden, werden im Testcode abgefangen. In diesem Fall ist insbesondere das Auftreten von Fehlern innerhalb von Dritt-Objekten interessant, also Objekte, die nicht Gegenstand des Tests sind, aber aufgrund von indirekten Abhängigkeiten benutzt werden müssen. Aufwände, die durch die Analyse und ggfs. Behebung solcher Fehler entstehen, sollen genau herausgearbeitet werden. Auch Fehler in Schnittstellen von Drittobjekten oder bei deren Nutzung gehören hierzu.

6) Analyse Nachdem die verschiedenen Problemstellen mit indirekten Abhängigkeiten identifiziert und bewertet worden sind, werden die Probleme in Kategorien zusammengefasst und tabellarisch ausgewertet.

3.4 Ansatz zur Untersuchung einer Hypothese bezüglich der

Metrik DSTMh Folgende Hypothese soll überprüft werden: „Bei der Suche nach semi-festverdrahteten Abhängigkeiten, die mit geringem Aufwand durch Refaktorisierung entfernt werden können, weisen niedrige Werte der Metrik DSTMh � 0 eine höhere Trefferquote aus als bei der zufälligen Auswahl von Abhängigkeiten“.

Page 21: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Vorgehensweise der Untersuchung

20

Vorgehensweise zur Prüfung der Hypothese:

1) Untersuchung von fünfzig Abhängigkeiten mit den niedrigsten Werten der Metrik DSTMh � 0. Bewertung der Abhängigkeiten bezüglich des Kriteriums „ist leicht refaktorisierbar“, sowie ggfs. der Gründe, warum die jeweilige Abhängigkeit nicht leicht zu Refaktorisieren ist (tabellarische Aufstellung)

2) Untersuchung von fünfzig Abhängigkeiten, die willkürlich nach dem Zufallsprinzip ausgewählt werden sollen. Als Auswahlkriterium werden Zufallszahlen verwendet. Auch hier erfolgt eine tabellarische Übersicht mit den Bewertungen wie unter Punkt 1.

3) Untersuchung der Hypothese anhand der beiden Tabellen aus den Punkten 1 und 2. Erwartet wird eine signifikant höhere Zahl an Fällen mit „ist leicht refaktorisierbar“ in der Tabelle Nr. 1, womit die Hypothese angenommen würde.

3.5 Gesamtstrategie Für die Untersuchung der Auswirkungen von Entwurfs- und Architekturentscheidungen auf die Testbarkeit für objektorientierte Systeme wurden zwei zentrale Fragen sowie eine Nebenfrage formuliert, die weiter oben genannt worden sind (siehe Abschnitt 1.2). Mit dem oben genannten Ansatz sollen Testbarkeits-Probleme herausgearbeitet werden, die Infolge von Abhängigkeiten entstanden sind. Es sollen Bewertungen für diese Fälle durchgeführt werden, um Problem-Bereiche zu identifizieren, die bereits aufgrund des Designs oder bei der Implementierung mit wenig Aufwand hätten vermieden werden können. Anhand der untersuchten Fragen sollen Design-Richtlinien herausgearbeitet werden, die eine testfreundliche Architektur gewährleisten. Es sollen möglichst einige Richtlinien beschrieben werden, wie bestehende Architekturen schrittweise testfreundlicher refaktorisiert werden können. Außerdem soll stets untersucht werden, inwieweit sich die allgemeine Entkopplung von Objekten auf die Testbarkeit auswirkt.

Page 22: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

21

# " � � �� �� � ���� ���� ��� � � � � � ������� � � �

Dass Klassen-Abhängigkeiten die Testbarkeit negativ beeinträchtigen können, wurde bereits erläutert. Doch wie genau sich das Vorhandensein indirekter Abhängigkeiten auf die Testaufwände auswirkt, soll in diesem Kapitel untersucht werden. 4.1 Auswahl von Klassen Zunächst sollen Klassen aus dem Fallbeispiel systematisch ausgewählt werden, um für diese Klassen anschließend Testklassen zu entwickeln, und die Auswirkungen indirekter Abhängigkeiten auf die Testbarkeit zu untersuchen. Die Tabelle 3 listet die 25 Klassen mit der höchsten absoluten Anzahl an indirekten Abhängigkeiten auf, berechnet mittels der Metrik

CBOi – CBO Dies ist die Anzahl aller Abhängigkeiten abzüglich der Anzahl der direkten Abhängigkeiten. Die folgende Abbildung 3 zeigt die Verteilung der Werte dieser Metrikdifferenz über alle Klassen des Fallbeispiels. Von insgesamt 238 Klassen haben 18 Klassen den Wert „0“, dies ist im Balkendiagramm nicht erkennbar.

0

50

100

150

200

250

CB

Oi -

CB

O

Abbildung 3: Verteilung der Metrikwerte CBOi - CBO im Fallbeispiel

Page 23: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

22

Klasse CBOi – CBO Klassifizierung SeminarisH 217 Instantiierung und Initialisierung AAKlassenFabrik 201 Instantiierung und Initialisierung SVAuswaehlenAA 106 Akteur-Anwendungsfall GeschaeftspartnerAuswaehlenAA 99 Akteur-Anwendungsfall FSVErfassenAA 98 Akteur-Anwendungsfall BelegungAuswaehlenAA 94 Akteur-Anwendungsfall FSVAendernAA 94 Akteur-Anwendungsfall PersonAuswaehlenAA 90 Akteur-Anwendungsfall BelegungAendernAA 86 Akteur-Anwendungsfall PersonAendernAA 84 Akteur-Anwendungsfall TeilnehmerAnmeldenAA 84 Akteur-Anwendungsfall LeitungsauftragAuswaehlenAA 81 Akteur-Anwendungsfall OeSVAendernAA 80 Akteur-Anwendungsfall BelegungStornierenAA 79 Akteur-Anwendungsfall FSVAendernErfassenAA 78 Akteur-Anwendungsfall OeSVAendernErfassenAA 77 Akteur-Anwendungsfall LeitungsauftragErfassenAA 76 Akteur-Anwendungsfall LeitungsauftragAendernAA 75 Akteur-Anwendungsfall OeSVErfassenAA 75 Akteur-Anwendungsfall PersonBeschreibung 75 Hilfsklasse für grafische Darstellung STBeschreibung 75 Hilfsklasse für grafische Darstellung Auswahlliste 74 Hilfsklasse für grafische Darstellung EckPanel 74 Hilfsklasse für grafische Darstellung FirmaAuswaehlenAA 72 Akteur-Anwendungsfall DozentenvereinbarungAuswaehlenAA 70 Akteur-Anwendungsfall PersonListe 70 Hilfsklasse für grafische Darstellung STListe 70 Hilfsklasse für grafische Darstellung FSVErfassenK 68 Kontrollklasse

Tabelle 3: Klassen des Fallbeispiels mit der höchsten absoluten Zahl an indirekten Abhängigkeiten

Klassen für die Instantiierung und Initialisierung („I&I“) Wenn eine abstrakte Klassenfabrik („class factory“) in einem Softwaresystem verwendet wird, so dient diese zur Instantiierung verschiedener Klassen, und ist dadurch von Natur aus von allen diesen zu Instantiierenden Klassen direkt abhängig, sofern die Konstruktoren oder Pseudo-Konstruktoren (wie in diesem Fall) direkt aufgerufen werden. Die Refaktorisierung einer class factory bedeutet eine gravierende Änderung der Klassenarchitektur, und sie würde für die Testbarkeit keine Verbesserungen bringen, weil die entsprechenden Instantiierungen dann eben bei anderen Klassentests entsprechend berücksichtigt werden müssen13. Die Klasse SeminarisH ist das erste Objekt des Fallbeispiels, welches nach dem Programmstart erzeugt wird und wichtige globale Klassen instantiiert, unter anderem die Klasse AAKlassenFabrik. Eine genauere Analyse zeigt, dass SeminarisH ihren hohen Metrikwert (CBOi – CBO = 217) zum überwiegenden Teil indirekt aus den Abhängigkeiten zu dieser Klasse AAKlassenFabrik (und den damit verwandten Klassen AAKlassenFabrikProvider und IAAKlassenfabrik) bezieht. Lediglich zehn indirekte

13 Eine Ausnahme stellt die Instantiierung von Objekten über einen Mechanismus dar, der weder den Konstruktor noch den Pseudo-Konstruktor direkt aufruft, also z.B. der Reflection-Mechanismus in Java oder die Instantiierung von COM-Komponenten in Windows-Betriebssystemen. Hier wird jedoch ausschließlich vom konkreten Fallbeispiel ausgegangen, welches keiner tiefgreifenden Refaktorisierung unterzogen werden soll.

Page 24: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

23

Abhängigkeiten werden aus einer anderen direkten Abhängigkeit in SeminarisH eingebracht (nämlich aus der Klasse SachbearbeiterA). Die AAKlassenFabrik ist ganz bewusst angelegt worden als abstrakte Klassenfabrik. Durch die Instantiierung einer AAKlassenFabrik wird SeminarisH automatisch festverdrahtet abhängig von dieser class factory, und damit indirekt abhängig von allen direkten und indirekten Abhängigkeiten der AAKlassenFabrik. Mit Ausnahme von sehr einfachen Anwendungen ist es nicht zu vermeiden, dass in einem Softwaresystem beim Start bestimmte I&I-Aufgaben durchgeführt werden müssen. Die Klassen, die im Fallbeispiel für diese Aufgaben implementiert werden, sind durch die direkten Konstruktoraufrufe auf ganz natürliche Art und Weise also in hohem Maße von den anderen Klassen des Softwaresystems abhängig. Auch Refaktorisierungen durch weitere Abstraktionen über Interfaces oder zusätzliche class factories bringen aus Sicht der Testbarkeit keine spürbaren Vorteile, da die Instantiierungen letztlich von irgendeiner Klasse durchgeführt werden müssen. Auf der anderen Seite ist aber zu sagen, dass diese I&I-Klassen in aller Regel keine anderen Aufgaben haben, als Kontrollklassen zu instantiieren und zu initialisieren, um anschließend den Kontrollfluss an diese Klassen zu übergeben. Diese simple Funktionalität ist auch hier in der Klasse SeminarisH gegeben, deren Aufgabe lediglich in der Erzeugung von vier Klassen besteht, sowie der Einstellung des User Interfaces. Selbst wenn also die schiere Größe der Zahl der Abhängigkeiten den Eindruck eines erschwerten Tests anzeigt, so ist durch die simple Struktur der I&I-Klassen ein relativ einfaches Testen möglich (sofern nicht bestimmte Klassen ausgetauscht werden sollen durch Stubs oder Mocks). Dieser simple Aufbau trifft auch auf class factories zu, in unserem Beispiel in der Klasse AAKlassenFabrik: Diese besteht lediglich aus „einzeiligen“ Methoden, nämlich Methoden zur Erzeugung von Objektinstanzen (im Beispielfall genau 9 ähnlich implementierte Methoden). Auch hier ist der Test sehr einfach möglich, sofern kein Austausch von Klassen durchgeführt werden soll. Aufgrund dieser Sonderstellung erscheint die Auswahl einer der Klassen SeminarisH oder AAKlassenFabrik zur Untersuchung der Auswirkungen von indirekten Abhängigkeiten nicht besonders interessant. Akteur-Anwendungsfall-Klassen Das folgende UML-Diagramm (Abbildung 4) zeigt die Vererbungshierarchie aller Akteur-Anwendungsfall-Schnittstellenklassen des Fallbeispiels. Dabei ist für jede Klasse auch der Wert der Metrik CBOi – CBO eingetragen14.

14 Aus technischen Gründen (wegen der verwendeten UML-Darstellungsapplikation) ist jedem dieser Werte ein Gedankenstrich („-“) vorangestellt, der jedoch keine spezielle Bedeutung hat.

Page 25: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

24

Abbildung 4: UML - Diagramm aller Akteur - Anwendungsfallklassen im Fallbeispiel Die Akteur-Anwendungsfall-Klassen bieten eine Art Ausgangspunkt für die Kontroll- und Domänenklassen. Eine Akteur-Anwendungsfall-Schnittstellenklasse (AAS) entspricht stets einem Dialogfenster, in dem der Anwender Abfragen tätigen und Aktionen starten kann. Dadurch werden die AAS-Klassen auch zu Vätern der Kontrollklassen des Systems (Väter in Bezug auf die Instantiierung), und die Kontrollklassen sind natürliche Väter der Domänenklassen. Aufgrund dieses Designs ist es plausibel, dass AAS-Klassen direkt nach den I&I-Klassen die Spitzenpositionen in der Rangliste der Werte CBOi – CBO einnehmen. Die Verwendung von AAS-Klassen als Testobjekte zur Untersuchung der Aufgabenstellung erscheint also ebenfalls wenig erstrebenswert, weil die Zahl der indirekten Abhängigkeiten architekturell bedingt sehr hoch und die Mehrheit dieser Abhängigkeiten sehr ähnlich ausgeprägt sind. Hilfsklasse für grafische Darstellung Einige Akteur-Anwendungsfälle benutzen Hilfsklassen zur grafischen Darstellung. Die Klasse DozentenvereinbarungAuswaehlenAA zum Beispiel benutzt das folgende System von Hilfsklassen zur Darstellung bestimmter Teilbereiche des Anwendungsdialogs (Abbildung 5, Metrikwerte sind wieder angegeben)15:

15 Auch hier ist der Metrik (wie in Abbildung 4) ein Gedankenstrich vorangestellt, der jedoch keine spezielle Bedeutung hat.

Page 26: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

25

Abbildung 5: Hilfsklassen für grafische Darstellungen von Teilbereichen

Weil die Basisklasse dieser Hilfsklassen, EckPanel, von den AAS-Klassen DVAktionenAA und DVAuswaehlenAA direkt abhängig ist, erklärt sich auch der hohe Wert der Metrik CBOi – CBO. Die Abhängigkeit resultiert aus der statischen Nutzung von UI-Controls16 aus der Supplier-Klasse durch die Klasse EckPanel. Für diese Hilfsklassen erscheint das Erstellen einer Testklasse ebenfalls nicht erstrebenswert, da durch die enge Verzahnung mit den zugehörigen AAS-Klassen die gleichen Argumente gegen die Auswahl der Klassen sprechen wie bei den AAS-Klassen. 4.2 Untersuchung der Klasse FSVErfassenK Die Kontrollklasse FSVErfassenK ist die Kontrollklasse mit dem höchsten Wert der Metrik CBOi – CBOn nämlich 68. Für diese Klasse wurde eine Testklasse erstellt. Die Beobachtungen, die bei der Erstellung der Testklasse auftraten, sind detailliert im Anhang A.2 wiedergegeben.

16 UI = User Interface, in diesem Fall Java Swing

Page 27: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

26

Im Folgenden werden noch einmal die Testprobleme zusammengefasst und nummeriert, die bei der Entwicklung der Testklasse aufgetreten sind:

Testvorbereitung

1) Die Tatsache, dass ein bestimmtes Datenbank-Objekt vorhanden sein muss (Seminartyp), bereitete erhebliche Probleme. Wegen eines Programmfehlers konnte es nicht permanent in der Datenbank hinterlegt werden. Es musste in der Testklasse eine zusätzliche Methode implementiert werden, die das Datenbank-Objekt vor dem entsprechenden Test neu erzeugt. Dieses Objekt muss nach dem Testlauf wieder zerstört werden, damit Folge-Tests keine duplicate-key-Fehler bei der Erzeugung des Objekts erhalten. Der Aufwand alleine für die Implementierung und den Test der genannten Zusatzmethode betrug etwa 30 Minuten.

Testfallerstellung

2) Weil die CUT keine get-Methoden für ihre Attribute anbietet, wird ein gründlicher Test (in dem auch der Zustand der Instanz zu bestimmten Zeitpunkten geprüft werden soll) erschwert. Dies gilt auch für die Basisklassen. Nur durch aufwändige Implementierung mittels Reflection-API wurden diese Tests möglich. Die Implementierung einer Methode, die die vier Attribute der Basisklasse der CUT nach der Instantiierung auf korrekte Initialisierung prüft, hat 32 uncommented lines of code und erforderte alleine etwa 30 Minuten Aufwand für Implementierung und Test.

Testdurchführung

3) Unachtsamkeit der Entwickler der CUT führte zu verstärktem Aufwand bei der Suche nach der Ursache einer Exception. Dieser Fehler hat nichts mit indirekten Abhängigkeiten zu tun, es hätte mit jedem Attribut passieren können, dessen Typ von Object abgeleitet ist.

4) Es wurde oftmals nötig, den Code der Basisklasse der CUT (und deren Basisklasse) bis in tiefe Details hinein zu analysieren, um Exceptions oder Fehler zu interpretieren, die beim Aufruf der Methoden der CUT auftraten

5) Es hat sich gezeigt, dass sehr viele Attribute aus den Basisklassen der CUT zunächst initialisiert werden müssen, bevor eine Methode ausgeführt werden kann. In einigen Fällen wurde diese notwendige Initialisierung nicht geprüft, sondern davon ausgegangen, dass die Attribute gültig sind. In vielen Fällen wird auch kein Hinweis gegeben, welches Attribut konkret ungültig ist, oder was unternommen werden muss, damit die jeweilige Methode eine sinnvolle Aufgabe verrichten kann.

6) Des Weiteren hat sich gezeigt, dass die Voraussetzung für einen erfolgreichen Test das Vorhandensein einer Datenbank mit einem gültigen Eintrag für einen Seminartyp ist. Darüber hinaus muss sich der Entwickler der Testklasse auch noch die Mühe machen, den Schlüssel eines solchen Seminartyps herauszufinden; er wird nämlich in keinem Anwendungsdialog angezeigt. Später hat sich durch genaue Analyse der Klasse Seminartyp herausgestellt, dass dieser Schlüssel identisch ist mit dem Titel des Seminars.

Aus der Sicht des Verfassers dieser Arbeit scheinen nur die Probleme zu 4) und 5) unmittelbar damit zusammenzuhängen, dass die CUT eine sehr hohe Zahl an indirekten Abhängigkeiten besitzt. Die Wahrscheinlichkeit, auf Probleme des Typs 1) und 6) zu stoßen, ist dabei in Klassen mit vielen indirekten Abhängigkeiten wesentlich höher als in reinen Serverklassen, die einen Service anbieten, ohne von anderen Klassen abhängig zu sein. In langen Vererbungs-Ketten tritt auch das Problem 2 verstärkt zutage, da alle Vorgängerklassen außer der direkten Basisklasse zu indirekten Abhängigkeiten der CUT führen, wie auch in diesem speziellen Fall.

Page 28: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

27

Die folgenden beiden Tabellen (Tabelle 4 und Tabelle 5) listen die einzelnen Abhängigkeiten der Klasse FSVErfassenK auf, sowie deren Auswirkungen auf die Testbarkeit. Wegen der Menge der Abhängigkeiten wurde jeweils eine Tabelle für die direkten und eine Tabelle für die indirekten Abhängigkeiten erstellt. Dabei bedeuten die Tabellenüberschriften

- Distanz: Die kürzeste Entfernung der Supplier-Klasse von der untersuchten Client-Klasse in der Kette der Abhängigkeiten, wobei „Distanz = 1“ bedeutet direkt abhängig, und Distanz > 1 bedeutet entsprechend indirekt abhängig.

- fachlich relevant: Der Entwickler der Testklasse muss analysieren, was die jeweilige Supplier-Klasse an Funktionalitäten bereitstellt, und wie diese zu Nutzen ist.

- Testsetup / Eingabeparameter: Die Supplier-Klasse wird benötigt für das Testsetup, das TearDown oder als Eingabeparameter der CUT.

- Assertions: Die Supplier-Klasse wird in Assert – Statements der Testmethoden verwendet

- Exceptions: Die Supplier-Klasse wird als Exception-Typ in einem try/catch – Block der Testmethoden verwendet.

- Auswirkung auf den Testaufwand: Falls die betreffende Abhängigkeit zusätzlichen Aufwand für das Testen einbringt, wird hier eine Bewertung dieses Aufwands angegeben („gering“, „mittel“ und „hoch“).

Verwendg. im Test Auswirkung auf d. Testaufwand

Supplier-Klasse Dis

tanz

fach

lich

rele

vant

Test

setu

p /

Ein

gabe

para

met

er

Ass

ertio

ns

Exc

eptio

ns

Fehl

er in

Sup

plie

rkla

sse

?

Problembeschreibung Test

vorb

erei

tung

Test

falle

rste

llung

Test

durc

hfüh

rung

DruckerK 1 - - - - -FirmenSeminarveranstaltung 1 - - - - -FSVAendernErfassenK 1 x - Probleme Nr. 2, 4, und 5

(Basiskl. der CUT)- hoch hoch

FSVAuftragsbestaetigungDK 1 - - - -FSVRechnungDK 1 - - - - -HinweisNachricht 1 - - - - -IFirmenSeminarveranstaltung 1 - - - - -ISeminarisDatenbank 1 x x - - - - -SeminarisDatenbankProvider 1 x - - - - -SeminarisMeldung 1 x x - - - - -Seminarveranstaltung 1 x x - - - - -SVAendernErfassenK 1 x - Probleme Nr. 2, 4 und 5

(Basiskl. der Basiskl.)- hoch hoch

Tabelle 4: Direkte Abhängigkeiten der Klasse FSVErfassenK

Page 29: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

28

Verwendg. im Test Auswirkung auf d. Testaufwand

Supplier-Klasse Dis

tanz

fach

lich

rele

vant

Test

setu

p /

Ein

gabe

para

met

er

Ass

ertio

ns

Exc

eptio

ns

Fehl

er in

S

uppl

ierk

lass

e ?

Problembeschreibung Test

vorb

erei

tung

Test

falle

rste

llung

Test

durc

hfüh

rung

BelegungRelation 2 - - - - -BriefDK 2 - - - - -BuchtRelation 2 - - - - -DatumUhrzeit 2 - - - - -DruckDokumentDK 2 - - - - -ErweitertPersistent 2 x - - - - -Firma 2 - - - - -FirmenAnsprechpartnerRelation 2 - - - - -Geld 2 - - - - -GeschaeftspartnerOrdner 2 - - - - -IErweitertPersistent 2 - - - - -IFirma 2 - - - - -IGeschaeftspartner 2 - - - - -IPerson 2 - - - - -ISeminartyp 2 x x - - - - -ISeminarveranstaltung 2 - - - - -ISynchronisationsManager 2 - - - - -KeineNachricht 2 - - - - -LeitungsauftragRelation 2 - - - - -Person 2 - - - - -SeminarisDatenbank 2 - Problem Nr. 6: Instanz

muß vorhanden sein- - hoch

Seminartyp 2 x x x - Probleme Nr. 1 und 6 mittel - hochSeminartypOrdner 2 - - - - -SeminarveranstaltungOrdner 2 - - - - -SVAnsprechpartnerRelation 2 - - - - -SVSeminartypRelation 2 - - - - -SynchronisationProvider 2 - - - - -TeilnahmeFirmenSVRelation 2 - - - - -UngueltigerSchluesselException 2 x - - - - -AssoziationsException 3 - - - - -Belegung 3 - - - - -BelegungTripel 3 - - - - -BuchtPaar 3 - - - - -CommitOrRollbackWithoutStartTransactionException3 - - - - -ErweitertPersistentOrdner 3 - - - - -ErweitertPersistentRelation 3 - - - - -FirmenAnsprechpartnerPaar 3 - - - - -Geschaeftspartner 3 - - - - -IBelegung 3 - - - - -IBelegungTripel 3 - - - - -IBuchtPaar 3 - - - - -IFirmenAnsprechpartnerPaar 3 - - - - -IGeld 3 - - - - -IKonfiguration 3 - - - - -ILeitungsauftrag 3 - - - - -ILeitungsauftragTripel 3 - - - - -IOeffentlicheSeminarveranstaltung 3 - - - - -IProtokollManager 3 - - - - -ISVAnsprechpartnerPaar 3 - - - - -ISVSeminartypPaar 3 - - - - -ISynchronisationsKunde 3 - - - - -ITeilnahmeFirmenSVPaar 3 - - - - -KonfigurationException 3 - - - - -KonfigurationProvider 3 - - - - -Leitungsauftrag 3 - - - - -LeitungsauftragTripel 3 - - - - -OeffentlicheSeminarveranstaltung 3 - - - - -ProtokollProvider 3 - - - - -Seitenbeschreibung 3 - - - - -SeminarisException 3 x - - - - -SVAnsprechpartnerPaar 3 - - - - -SVSeminartypPaar 3 - - - - -SynchronisationsManager 3 - - - - -TeilnahmeFirmenSVPaar 3 - - - - -IProtokollAusgabeKunde 4 - - - - -Konfiguration 4 - - - - -ProtokollManager 4 - - - - -SeminarisTextLayout 4 - - - - -

Tabelle 5: Indirekte Abhängigkeiten der Klasse FSVErfassenK

Page 30: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

29

Die folgende Tabelle 6 fasst die Aufwände für die verschiedenen Phasen von der Testvorbereitung bis zur Testdurchführung zusammen. Dabei gilt als Einstufung folgende Richtlinie:

- gering: Es fällt kein unerwarteter Aufwand an, und der Schritt kann in einem Zug durchgeführt werden, z.B. die Programmierung einer Methode. Maximal 15 Minuten Aufwand

- mittel: Es wurden zusätzliche Aufwände nötig, z.B. das Erstellen von Hilfsmethoden oder die Analyse von anderen Klassen infolge von Fehlern (Laufzeit- oder Compilierfehler). Aufwand maximal 30 Minuten

- hoch: Es wurden zusätzliche Aufwände (wie oben genannt) nötig, die dazu führten, dass der aktuelle Schritt länger als 30 Minuten gedauert hat.

Phase Beschreibung Einstufung Testvorbereitung Erstellen der Methode erzeugeSeminartyp zum Sicherstellen

des Zustands der SeminarisDatenbank vor dem Test (Problem 1)

mittel

Testfallerstellung Absichern des Initialzustands einer Instanz der CUT, inklusive Hilfsmethode InitialzustandSicherstellen() (Problem 2)

hoch

Testdurchführung Analyse von etlichen Laufzeitfehlern beim Aufruf von testMitteilungenErstellen(), inklusive Untersuchung verschiedener Fremdklassen, etwa 100 Minuten (Probleme 3 und 4)

hoch

Analyse von Fehlern, die daraus resultieren, dass für bestimmte Attribute der CUT Voraussetzungen gelten, die nicht immer gegeben sind (Problem 5)

mittel

Ursachenanalyse und Behebung des Problems 6: Es muss eine Datenbank mit einem bestimmten Seminartyp präpariert vorliegen zur erfolgreichen Durchführung des Tests.

hoch

Tabelle 6: Aufwände für Testvorbereitung, Testfallerstellung und -durchführung der CUT FSVErfassenK

4.3 Untersuchung der Klasse RechnungsempfaengerRelation Als nächste zu Testende Klasse wurde nach FSVErfassenK (Abschnitt 4.2), entsprechend der Auflistung in Tabelle 3 die nächste Klasse von oben ausgewählt, die keine Kontrollklasse ist. Diese Auswahl wurde getroffen, um nicht dieselben Auffälligkeiten anzutreffen wie im vorherigen Abschnitt. Die ausgewählte Klasse ist die RechnungsempfaengerRelation mit einem Wert

CBOi – CBO = 33.

Die ausführliche Dokumentation zu Testfallerstellung, Testvorbereitung und Testdurchführung ist im Anhang A.3 enthalten. Die folgende Aufstellung fasst noch einmal kurz alle aufgetretenen Testprobleme zusammen. Testvorbereitung:

1) Das Attribut der Basisklasse ist privat, der Zugriff für Prüfungen gelingt nur über den Reflection-Mechanismus.

Page 31: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

30

2) Es müssen Objekte vom Typ IBelegung, IGeschaeftspartner und IRechnungsempfaengerRelation im Testfixture verwaltet werden, da diese Objekte in jeder Testmethode benötigt werden. Weil die Geschaeftspartner-Klasse eine abstrakte Klasse ist, muss eine weitere (abgeleitete) Klasse verwendet werden, in diesem Fall Firma.

3) Um Objekte des Typs Belegung und Firma in die Datenbank einfügen zu können, müssen diese Objekte einen eindeutigen Schlüssel bekommen, über den sie identifiziert werden; für die Zwecke des Tests muss es eindeutig identifizierbare Objekte geben, deren Identität überprüfbar ist. Mangels Methoden wie etwa getSchluessel oder setSchluessel müssen die entsprechenden Klassen analysiert werden, um festzustellen, welche Attribute bzw. welche Attributkombinationen als Schlüssel verwendet werden.

Testfallerstellung:

4) Es wurden verschiedene Methoden der CUT entdeckt, die systemweit nicht

verwendet werden. Für diese Methoden sind bereits Testmethoden erstellt worden, was natürlich den Testaufwand negativ beeinträchtigt. Die Methoden der Testklasse waren teilweise wegen schwerwiegender Fehler unbrauchbar, so dass sie ganz auskommentiert wurden, und auf den Test verzichtet wurde.

5) Zu testende Methoden waren teilweise nicht-öffentlich, so dass nur über die Methode invoke() der Java Reflection API entsprechende Tests durchgeführt werden konnten. Diese Methode „verschluckt“ jedoch Exceptions, die während der Ausführung auftreten, das heißt anstatt der Original-Exception wird eine neue InvocationTargetException erzeugt, die wiederum keine Rückschlüsse mehr auf die ursprüngliche Exception aus der zu testenden Methode beinhaltet. Diese Tatsache erschwert die Fehlersuche.

6) Selbst einfachste Methoden der CUT, die einfach nur das Vorhandensein bestimmter Paar-Objekte prüfen, oder zu einer Hälfte des Paares das zugehörige Gegenstück ermitteln, verlangen den vorherigen Start einer Datenbank - Transaktion. Dies ist bei Aktionen unnötig, die potentiell keinerlei Änderung des Datenbank-Zustands herbeiführen können. Und selbst wenn es nötig wäre (wie z.B. in der Methode verbinde()), so erwartet der Nutzer eines High-Level Objects (welches in der so genannten business-logic-Schicht angesiedelt ist), dass er sich über keinerlei Low-Level-Operationen Gedanken machen muss, welches ja der Start einer Transaktion darstellt. Der Grund, warum bei jeder Datenbankabfrage eine Transaktion nötig ist, liegt in der Methode Datenbank.abfrageAusfuehren, welche nicht unterscheidet zwischen reinen read-only und potential-change - Operationen. Diese Transaktionsverarbeitung verschlechtert die Testbarkeit erheblich, da in jeder Testmethode ca. 30 Zeilen Code für die Transaktionsverarbeitung inklusive Fehlerbehandlung enthalten sind.

7) Die beiden Ausprägungen der Methode loese in der CUT weisen ein unterschiedliches Verhalten auf: Wenn kein erfolgreiches Lösen möglich ist, erzeugt die erste Ausprägung der Methode Exceptions, während die zweite einfach gar nichts macht. Eine Erfolgskontrolle ist mit der zweiten Ausprägung der Methode nicht möglich, wäre aber aus Testsicht zu Erwarten gewesen.

Testdurchführung:

8) Die Fehlerbehandlung der Pseudo-Konstruktoren (Methode erzeuge) mehrerer Klassen (Belegung, Firma, RechnungsempfaengerPaar) ist mangelhaft: Die aussagekräftige Exception aus einer unteren Schicht wird verworfen, und eine neue, leere Exception wird stattdessen erzeugt. Dies führt zu Analyseaufwänden der betreffenden Methoden.

Page 32: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

31

9) Der Test - Mechanismus Fixture / TearDown bedingt, dass vor jedem Aufruf einer Testmethode die Erzeugung der benötigten Objekte (Fixture) erfolgt, und nach dem Methodenaufruf der Abbau derselben (TearDown). Es gab jedoch ein Problem mit dem Objekt RechnungsempfaengerPaar: Obwohl im TearDown die Methode zerstoere des Objekts aufgerufen wurde, wurde das Objekt nicht aus der Datenbank entfernt, so dass beim folgenden Fixture ein duplicate-key-Fehler auftrat. Erst nach Analyse der Klasse RechnungsempfaengerPaar wurde festgestellt, dass für die beiden darin beteiligten Objekte vorher explizit eine Methode loese() aufgerufen werden muss, und erst dann zerstoere() ausgeführt werden darf. Es war bei der Testfallerstellung davon ausgegangen worden, dass das zerstoere() auch implizit ein Doppeltes loese() ausführen würde (Dies wurde erst bei der Testdurchführung bemerkt).

10) Die Methode Belegung.equals(), welche zur Absicherung von Zuständen verwendet wird, ist nicht robust gegenüber Initialen Zuständen der Attribute Anmeldedatum und Stornodatum. Unerwartete Exceptions wurden erzeugt.

11) Obwohl in der CUT jede Methode mindestens einen Parameter vom Typ IBelegung oder IGeschaeftspartner bekommt, ist bis auf die Methode verbinde() keine Methode gegen NULL-Objekte abgesichert: Es werden Nullpointer-Exceptions erzeugt.

Aus der Sicht des Verfassers dieser Arbeit hängen die Probleme zu 3, 6, 8 und 10 unmittelbar damit zusammenzuhängen, dass die CUT eine sehr hohe Zahl an indirekten Abhängigkeiten besitzt. Die folgende Tabelle 7enthält alle Abhängigkeiten der CUT mitsamt dem Distanzwert17 (analog zu Abschnitt 4.2) und einer Beurteilung bezüglich des Einflusses auf den Test.

17 Zur Beachtung: Nur die Zeilen mit Distanz > 1 sind indirekte Abhängigkeiten.

Page 33: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

32

Verwendg. im Test Auswirkung auf d. Testaufwand

Supplier-Klasse Dis

tanz

fach

lich

rele

vant

Test

setu

p /

Ein

gabe

para

met

er

Ass

ertio

ns

Exc

eptio

ns

Fehl

er in

S

uppl

ierk

lass

e ?

Problembeschreibung Test

vorb

erei

tung

Test

falle

rste

llung

Test

durc

hfüh

rung

AssoziationsException 1 - - - - -BelegungRelation 1 - - - - -ErweitertPersistentRelation 1 - Problem 1

Basiskl. Der CUTmittel - -

GeschaeftspartnerOrdner 1 - - - - -IBelegung 1 x x - Problem 2 (Testfixture) mittel - -IGeschaeftspartner 1 x x - Problem 2 (Testfixture) mittel - -IRechnungsempfaengerPaar 1 x x x Problem 2 (Testfixture) mittel - -ISeminarisDatenbank 1 - - - - -RechnungsempfaengerPaar 1 x x - Probl. 7 und 8: mangelh.

Fehlerbehandl., Inkonsist.- gering gering

UngueltigerSchluesselException 1 - - - - -Belegung 2 x x x Probleme 3, 5, 8, 9, 10

verwendetes Hilfs-Objektmittel mittel hoch

BelegungTripel 2 - - - - -DatumUhrzeit 2 - - - - -ErweitertPersistent 2 x - - - - -ErweitertPersistentOrdner 2 - - - - -Firma 2 x x - Probleme 3 und 8

verwendetes Hilfs-Objektmittel - gering

Geschaeftspartner 2 x - - - - -IBelegungTripel 2 - - - - -IErweitertPersistent 2 - - - - -IFirma 2 - - - - -IGeld 2 - - - - -IOeffentlicheSeminarveranstaltung 2 - - - - -IPerson 2 - - - - -ISeminarveranstaltung 2 - - - - -Person 2 - - - - -SeminarisDatenbankProvider 2 x - - - - -SeminarisException 2 - - - - -SeminarveranstaltungOrdner 2 - - - - -FirmenSeminarveranstaltung 3 - - - - -Geld 3 - - - - -IFirmenSeminarveranstaltung 3 - - - - -IKonfiguration 3 - - - - -IProtokollManager 3 - - - - -KonfigurationProvider 3 - - - - -OeffentlicheSeminarveranstaltung 3 - - - - -ProtokollProvider 3 - - - - -SeminarisDatenbank 3 x - Problem 6: Instanz

u. Transaktionen nötig- gering -

mittelmittel

Seminarveranstaltung 3 - - - - -CommitOrRollbackWithoutStartTransactionException4 - - - - -IProtokollAusgabeKunde 4 - - - - -Konfiguration 4 - - - - -KonfigurationException 4 - - - - -ProtokollManager 4 - - - - - Tabelle 7: Alle Abhängigkeiten der Klasse RechnungsempfaengerRelation

Die folgende Tabelle 8 fasst die Probleme im Zusammenhang mit der Testbarkeit noch einmal zusammen, analog zum Abschnitt 4.2. Phase Beschreibung Einstufung Testvorbereitung Test des Konstruktors, Zustandskontrolle nur über

Reflection API (Problem Nr. 1) mittel

Verwaltung von Objekten der Typen Belegung, Geschaeftspartner / Firma und RechnungsempfaengerPaar (Problem Nr. 2)

mittel

Analyseaufwände von Drittklassen (Problem Nr. 3) mittel Testfallerstellung Eine der drei Methoden wird gar nicht verwendet,

unnötiger Testfall erstellt (Problem Nr. 4) mittel

Page 34: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

33

Test der drei privaten Methoden der CUT. Aufruf nur über Java Reflection API, dabei „Verschlucken“ von ursprünglichen Exceptions und komplizierte Fehlersuche (Problem Nr. 5).

mittel (Testerstellung), hoch (Testdurchf.)

Für jede erstellte Methode musste Code zur Transaktionsverwaltung drumherum erstellt werden (Problem Nr. 6)

mittel (bei erstem Fall), dann leicht

Inkonsequentes Verhalten der loese-Methoden bei unsinnigen Eingaben (Problem Nr. 7)

gering

Testdurchführung Fehlerbehandlung von Pseudo-Konstruktoren von Hilfsklassen ist mangelhaft und erfordert Fehleranalyse (Problem 8)

gering

Methoden zerstoere und loese der Drittklasse RechnungsempfaengerPaar unlogisch (Analyse- und Korrekturaufwände, Problem Nr. 9)

mittel

Unerwartete Exceptions aus der Drittklasse Belegung (Problem Nr. 10)

mittel

Robustheit gegen NULL-Parameter nicht gegeben, nachträgliche Anpassung (Problem Nr. 11).

gering

Tabelle 8: Die vorgefundenen Testbarkeits - Probleme der CUT RechnungsempfaengerRelation

4.4 Untersuchung der Klasse SpaltenModel Es soll eine weitere Testklasse zu einer Klasse des Fallbeispiels erstellt werden. Es wird wieder eine Klasse mit einem hohem Wert CBOi – CBO ausgewählt (siehe Abschnitt 4.2), es soll jedoch keine Kontrollklasse sein, und auch keine Relationenklasse, weil diese beiden Typen in den beiden vorherigen Abschnitten behandelt worden sind. Die ausgewählte Klasse ist die Klasse SpaltenModel mit einem Wert

CBOi – CBO = 25.

Auch für diesen Testfall wird die ausführliche Dokumentation im Anhang zur Verfügung gestellt (siehe Anhang A.4). Die gefundenen Testprobleme in der Zusammenfassung: Testvorbereitung:

1) Weil die CUT Objekte vom Typ IAnfrageSpalte in einem Iterator verwaltet, mussten die Klasse AnfrageSpalte und das Interface IAnfrageSpalte analysiert werden, um Instanzen der CUT für Testzwecke geeignet präparieren zu können.

Testfallerstellung:

2) Es gab keine Robustheit gegen Typverkehrte Object-Parameter: Auch hier wurde stets eine Exception ausgelöst.

Testdurchführung:

3) Bei int - Parametern, die als Tabellenindex interpretiert werden, sind negative Werte nicht erlaubt. Dies führt zu Exceptions, die aus einer dritten Klasse heraus

Page 35: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

34

(java.lang.Vector) resultieren. Die Klasse java.lang.Vector musste gründlich analysiert werden, um geeignete Testfälle für Grenzfälle zu erstellen (also Fälle, bei denen mit Parameter x noch ein anderes Verhalten der CUT erfolgt als mit „x+1“).

4) In vielen Fällen wird auch bei der Verwendung von Indexwerten als Parameter, die wertmäßig höher als die aktuellen Dimensionen der konkreten Tabelle sind, eine Exception erzeugt (Verursacher war wieder java.lang.Vector).

5) In einem Fall wurde ein Nachrichtenfenster erzeugt, welches hinter der Entwicklungsumgebung angezeigt wurde, so dass die Analyse des Testzustands erschwert wurde. Außerdem verhindert dieser Umstand einen automatischen Testbetrieb.

6) Die Robustheit gegen NULL-Objekte als Parameter war nirgendwo gegeben, es wurde stets eine Exception erzeugt.

Die Punkte 2, 3, 4 und 6 können unter der Kategorie „mangelhafte Robustheit gegen unerwartete Parameter“ gewertet werden. Natürlich dauerte die Testdurchführung infolge überraschender Exceptions (und deren nachträglicher Behandlung) merklich länger. Das ist aber auch der Sinn des Testens, die Probleme wurden aufgedeckt, und nun sind die Schwächen bekannt und dokumentiert. Der Punkt 5 ist ein ernsthaftes Problem für die Testdurchführung. Ein wichtiger Aspekt von Unit Tests ist die Möglichkeit, die Tests automatisiert unmittelbar nach dem Build durchführen zu können, um für das aktuelle Release sofort die Gewissheit zu haben, ob der Code fehlerfrei ist und die im Test codierten Spezifikationsprüfungen erfolgreich bestanden wurden. Sobald Nachrichtenfenster zur Benutzerkommunikation geöffnet werden, bei denen die Testdurchführung so lange stoppt, bis ein Benutzer die Nachricht bestätigt hat, ist die Möglichkeit der Test-Automation nicht mehr gegeben. Nach [Link02] (Kapitel 13) existiert ein so genannter AWT-Roboter, der aus dem Testcode heraus gezielt Mausklicks und Tastatureingaben simulieren kann, um das Nachrichtenfenster zu akzeptieren. Dazu muss jedoch die genaue Bildschirmposition des OK-Buttons bekannt sein. Die Tatsache, dass dann auf unterschiedlichen Display-Auflösungen der Testcode verändert werden muss, und der Arbeits - Overhead für das Kennen lernen und die Implementierung von AWT-Roboter-Code dürfte den Entwickler von Testklassen tendenziell dazu bewegen, einen Kompromiss bezüglich des Testumfangs einzugehen: Verzweigungen der CUT, die ein Nachrichtenfenster öffnen, werden einfach ausgelassen. Zum Zusammenhang zwischen Testproblemen und Abhängigkeiten ist zu sagen, dass nur die Probleme 1, 3, und 4 aus Abhängigkeiten heraus resultieren. Es handelt sich aber ausschließlich um direkte Abhängigkeiten der CUT zu den Klassen java.lang.Vector, AnfrageSpalte und IAnfrageSpalte. Das Problem 5 könnte man ebenfalls als „durch Abhängigkeiten entstanden“ interpretieren, wenn man sich die Möglichkeit vor Augen hält, die Klasse MeldungsAnzeigeA für Testzwecke durch ein Stub- oder Mockobjekt zu ersetzen. Diese Klasse MeldungsAnzeigeA wird im Fallbeispiel als zentrales Objekt für die Anzeige von Benutzernachrichten verwendet, und könnte für Testzwecke durch eine reine Protokollierungsfunktionalität ersetzt werden. Unter diesem Aspekt betrachtet, entsteht das Problem Nr. 5 durch die Verwendung einer ungeeigneten MeldungsAnzeigeA-Implementierung. Es handelt sich aber wiederum um eine direkte Abhängigkeit der CUT.

Page 36: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

35

Im Folgenden (Tabelle 9) wird angeführt, welche Abhängigkeiten die CUT hat, mitsamt dem jeweiligen Distanzwert (analog zu Abschnitt 4.2) und einer Bemerkung bezüglich des Einflusses auf den Test.

Verwendg. im Test Auswirkung auf d. Testaufwand

Supplier-Klasse Dis

tanz

fach

lich

rele

vant

Test

setu

p /

Ein

gabe

para

met

er

Ass

ertio

ns

Exc

eptio

ns

Fehl

er in

S

uppl

ierk

lass

e ?

Problembeschreibung Test

vorb

erei

tung

Test

falle

rste

llung

Test

durc

hfüh

rung

AnfrageSpalte 1 x x - Problem 1: Klassen-analyse notwendig

mittel - -

HinweisNachricht 1 - - - - -IAnfrageSpalte 1 x x - Problem 1: Klassen-

analyse notwendigmittel - -

MeldungsAnzeigeA 1 - Problem 5: verstecktesNachrichtenfenster

- - hoch

TableEingabeModel 1 - - - - -ErweitertPersistent 2 - - - - -ExceptionNachricht 2 - - - - -ExceptionNachrichtA 2 - - - - -IAnfrage 2 - - - - -IErweitertPersistent 2 - - - - -IProtokollManager 2 - - - - -ISeminarisDatenbank 2 - - - - -ProtokollProvider 2 - - - - -SeminarisDatenbankProvider 2 - - - - -SeminarisMeldung 2 - - - - -Anfrage 3 - - - - -IProtokollAusgabeKunde 3 - - - - -ProtokollManager 3 - - - - -SeminarisDatenbank 3 - - - - -AnfrageOrdner 4 - - - - -CommitOrRollbackWithoutStartTransactionException4 - - - - -IKonfiguration 4 - - - - -KonfigurationException 4 - - - - -KonfigurationProvider 4 - - - - -UngueltigerSchluesselException 4 - - - - -AnfrageParameter 5 - - - - -ErweitertPersistentOrdner 5 - - - - -IAnfrageParameter 5 - - - - -Konfiguration 5 - - - - -SeminarisException 5 - - - - - Tabelle 9: Abhängigkeiten der Klasse SpaltenModel

Die folgende Tabelle fasst die Probleme im Zusammenhang mit der Testbarkeit noch einmal zusammen, analog zum Abschnitt 4.2. Phase Beschreibung Einstufung Testvorbereitung Analyse des Dritt-Objekts AnfrageSpalte notwendig (Problem 1) mittel Testfallerstellung Keine nennenswerten Probleme - Testdurchführung Keine Robustheit gegen Typverkehrte Object-Parameter

(Problem 2) gering

Analyse der Klasse java.lang.Vector nötig (Problem 3) hoch Fehler in der CUT führen zu Exceptions beim Test der

Verwendung ungültiger Indexwerte (Problem 4) gering

Eine während des Tests angezeigte Benutzernachricht, die dazu noch versteckt (hinter der Entwicklungsumgebung) angezeigt wurde, unterbrach stets den Test und führte zu erheblichem Analyseaufwand bei der Testdurchführung (Problem Nr. 5).

hoch

Die Robustheit gegen NULL-Objekte als Parameter war nirgendwo gegeben

gering

Tabelle 10: Testbarkeitsprobleme bei der CUT SpaltenModel

Page 37: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

36

4.5 Zusammenfassung und Auswertung Die folgende Tabelle (Tabelle 11) listet noch einmal zusammenfassend auf, welche Problemarten in den vorhergehenden Abschnitten bezüglich der Testbarkeit aufgetreten sind, die aus indirekten Abhängigkeiten der jeweiligen CUT resultieren. Die Tabelle ist absteigend sortiert nach der Problemschwere (zuerst wird das gravierendste Problem genannt). Problembeschreibung a) Objektinstantiierungen müssen häufig in Transaktionen eingebettet werden, das heißt dass eine Transaktion gestartet und anschließend entweder bestätigt (commit) oder abgebrochen (rollback) wird. Es wurden mehrere Stellen gefunden, in denen normale Methoden auf der Ebene der Geschäftslogik-Schicht diesen Transaktionsrahmen erforderten, und ihre eigenen Objektbedürfnisse nicht durch Transaktionen umrahmt haben. b) Eine Einarbeitung in das Persistenzrahmenwerk zur Datenhaltung war notwendig. Dieser Umstand ist eine Folge aus dem Problem a) (Transaktionsverwaltung in den Testmethoden nötig) und c) (häufiges Auftreten von unkommentierten Exceptions aus der Datenhaltungsschicht). c) Oft wurde eine mangelhafte Fehlerbehandlung vorgefunden. An dieser Stelle sind nicht die Fälle gemeint, die in der CUT selbst enthalten waren, sondern in Klassen, zu denen eine indirekte Abhängigkeit besteht, und die infolge mangelhafter Robustheit oder nicht vorhandener Fehlerbehandlung während der Tests Exceptions erzeugten. Dies verursachte erhebliche Analyse - Aufwände bei der Testdurchführung. d) Wenn die CUT von einer anderen Klasse abgeleitet ist, und die Attribute der Basisklasse zwar als private deklariert sind, im Rahmen des Tests aber überprüft werden müssen, dann werden besondere Tricks der jeweiligen Implementierungssprache benötigt. Im Fallbeispiel wurde die Einarbeitung in und die Verwendung von Java-Reflection-Mechanismen notwendig. e) Mehrfach war es erforderlich, dass in der zugrunde liegenden zentralen Datenbank bestimmte Objekte mit bestimmten Schlüsseln vorhanden sein mussten, um einen Test durchführen zu können. Dies ist beispielsweise der Fall, wenn die CUT selbst Objekte aus dem Domänenmodell verwaltet (z.B. direkt als Attribute, oder indirekt als Array, Queue etc). Dann ist eine Analyse der zugehörigen Klassen nötig, um das Persistenzhandling und die Schlüsselverwaltung zu ergründen. f) Falls mit Objekten des Domänenmodells gearbeitet wird (und dies war bei allen Tests dieses Kapitels der Fall), muss stets eine Instanz der SeminarisDatenbank zur Verfügung stehen. Dies ist bedingt durch den Zwang zur Persistierung aller Entitätsobjekte, wodurch auch die Abhängigkeit dieser Objekte zur Klasse ErweitertPersistent herrührt. Diese Tatsache führte oft zu überraschenden Exceptions während der Testausführung, weil dieser Punkt nicht bedacht wurde, oder auch zu Zusatzaufwänden bei der Testvorbereitung für die Instantiierung der SeminarisDatenbank

Tabelle 11: Die vorgefundenen Problemarten, die aus indirekten Abhängigkeiten der CUT resultieren

Der zu Anfang vorgestellte Indikator CBOi – CBO hat sich als nicht uneingeschränkt nutzbar erwiesen (bezüglich der Suche nach Klassen mit Auffälligkeiten aufgrund indirekter Abhängigkeiten), weil er aufgrund seiner Natur die class factories bevorzugt und entsprechend hoch bewertet. Dabei wurden auch alle Klassen, die von class factories abhängen, ebenfalls sehr hoch eingestuft (siehe z.B. den Abschnitt „grafische Hilfsklassen“). Wie zu erwarten war, haben die tatsächlich vorgefundenen Probleme im Bereich der indirekten Abhängigkeiten gezeigt, dass die reine Anzahl der indirekten Abhängigkeiten weniger eine Rolle spielt, als vielmehr die besonderen Aufgaben und Eigenschaften einzelner Klassen, von denen indirekte Abhängigkeiten bestehen. Im vorliegenden Fallbeispiel wurde an dieser Stelle eine wichtige Regel guten Programmierstils nicht eingehalten, nämlich Abhängigkeiten möglichst nur von Interfaces, und nicht direkt von Klassen zu realisieren. Ein Refactoring des Fallbeispiels, das zur Austauschbarkeit des Persistenzrahmenwerks beitragen würde, indem es die häufig

Page 38: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung von indirekten Abhängigkeiten

37

angetroffenen direkten Abhängigkeiten zu ErweitertPersistent ersetzen würde durch Abhängigkeiten vom Interface IErweitertPersistent, würde die Testbarkeit spürbar verbessern helfen. Dadurch würde z.B. ein Stub- oder Mockobjekt für ErweitertPersistent möglich, welches in simpler Art und Weise eine echte Datenbank simuliert, und in allen denkbaren Fehlerfällen aussagekräftige und hilfreiche Exceptions erzeugt. Nach Auswertung der Tabellen 4, 5, 7 und 9, welche sowohl die direkten wie auch die indirekten Abhängigkeiten der Klassen des Fallbeispiels enthalten, kann man nicht sagen, dass in den untersuchten Fällen die indirekten Abhängigkeiten mehr Probleme für die Testbarkeit bereitet hätten, als die direkten Abhängigkeiten. Tendenziell ist sogar eher das Gegenteil der Fall. Spezielle Auffälligkeiten, die gezielt mit indirekten Abhängigkeiten zu tun haben, konnten keine gefunden werden, obwohl die Klassen mit der Strategie ausgewählt worden sind, möglichst viele indirekte Abhängigkeiten zu besitzen. Um allgemeine und gesicherte Aussagen bezüglich der Testprobleme aus direkten und indirekten Abhängigkeiten zu machen, war der Umfang der Stichprobe noch zu gering.

Page 39: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

38

� " � � �� �� � ���� ��� �$ % & � � � � � � � ' ���� � � � �( � ��������

Es gibt verschiedene Strategien, um ein gegebenes Softwaresystem in einer Art und Weise zu refaktorisieren, dass mit wenig Aufwand eine deutlich bessere Testbarkeit erreicht werden kann, zum Beispiel durch Betrachtung von Reduktionsmetriken (siehe [Merl03]). Da festverdrahtete Abhängigkeiten die Testbarkeit erheblich verschlechtern, soll ein neuer Ansatz über die Metrik DSTMh geprüft werden, welche das Verhältnis „festverdrahtet zu nicht-festverdrahtet“ ausdrückt (vergl. Abschnitt 2.2). 5.1 Hypothese Es soll folgende Hypothese untersucht werden: „Bei der Suche nach semi-festverdrahteten Abhängigkeiten, die mit geringem Aufwand durch Refaktorisierung entfernt werden können, weisen niedrige Werte der Metrik DSTMh � 0 eine höhere Trefferquote aus als bei der zufälligen Auswahl von Abhängigkeiten“ 5.2 Untersuchung von Abhängigkeiten anhand der Metrik DSTMh Bei einem niedrigen Wert von DSTMh besteht eine Abhängigkeitsbeziehung nur zu einem geringen Anteil aus festverdrahteten oder semi-festverdrahteten Verwendungen. Dies trifft umso stärker zu, je niedriger DSTMh ist. Die folgende Tabelle 12 gibt die semi-festverdrahteten Abhängigkeiten des Fallbeispiels wieder, die einen Metrikwert DSTMh ungleich 0 haben. Die Liste ist nach DSTMh sortiert und enthält die fünfzig niedrigsten Werte von DSTMh. Der Übersichtlichkeit halber wurde eine Identifikator-Spalte eingefügt, die identisch ist mit dem Identifikator aus dem Design2Test - Werkzeug. Es wurden einige zusätzliche Spalten eingefügt, um eine Bewertung der folgenden Kriterien angeben zu können: Statistik

- Abhängigkeiten von Supplier-Klasse / gesamt und / hardwired: Die Anzahl der direkten Client-Klassen von der jeweiligen Supplier-Klasse im gesamten System, jeweils als Gesamtanzahl und als Anzahl der (semi-) festverdrahteten Abhängigkeiten (hardwired).

Refaktorisierungs-Kandidaten

- leicht refaktorisierbar: Die Zugriffe, die zu einer (semi-) festverdrahteten Abhängigkeit führen, sind mit geringem Aufwand durch Refaktorisierung zu beseitigen

Page 40: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

39

- versehentlich / unbeabsichtigt: Die Tatsache, dass eine (semi-) festverdrahtete Verwendung der Supplier-Klasse durch die Client-Klasse erfolgt, erscheint dem Verfasser dieser Arbeit als versehentlich oder unbeabsichtigt.

schlechtes Design

- schlechtes Design: Die (semi-) festverdrahtete Verwendung der Supplier-Klasse durch die Client-Klasse ist auf Verstöße gegen die Grundlagen und Prinzipien des guten Objektorientierten Designs zurückzuführen, also zum Beispiel fehlende Kapselung, schlechtes Information Hiding, keine minimalen Schnittstellenbeziehungen und ähnliches. In den untersuchten Fällen kam die Verwendung statischer Konstanten vor, außerdem wurden Methoden unnötigerweise in statischer Ausprägung implementiert.

Keine einfache Refaktorisierung möglich (oder Refak. nicht sinnvoll)

- bewusst festverdrahtet: Eine Abhängigkeit ist ganz bewusst festverdrahtet, und eine Refaktorisierung würde die Testbarkeit nicht verbessern. Dies kann z.B. daran liegen, dass die Programmierer in einer konkreten Situation keine bessere Alternative zur Festverdrahtung gefunden haben, oder eine Alternative als zu aufwändig erschien. Dies ist kein Reziprok-Fall zum genannten Fall „versehentlich / unbeabsichtigt“, denn für sehr viele (semi-) festverdrahtete Abhängigkeiten gibt es durchaus nachvollziehbare Gründe, warum die Implementierung in der vorgefundenen Art und Weise durchgeführt worden ist, ohne dass erkennbar wird, dass sich die Entwickler nennenswert Gedanken gemacht haben über besonders gutes Objektorientiertes Design und die Reduzierung festverdrahteter Abhängigkeiten. Diese Fälle werden weder als versehentliche Verwendung festverdrahteter Abhängigkeiten angesehen, noch als bewusste (im Sinne von designmäßig ausdrücklich benötigte) festverdrahtete Verwendung.

- statischer Methodenaufruf, begründet: Hierbei handelt es sich um statische Methodenaufrufe, die als „notwendiges Übel“ betrachtet werden können, und nicht leicht refaktorisierbar sind. Ein Beispiel ist DatumUhrzeit. getAktuelleSystemzeit(): Wenn diese Methode nicht statisch implementiert wäre, so müsste eine Instantiierung eines Datum/Uhrzeit-Suppliers stattfinden, und es wäre dadurch trotzdem wieder eine festverdrahtete Abhängigkeit vorhanden.

- statische Instantiierung: Hier wird die supplier class entweder über die Konstruktormethode (die grundsätzlich statisch ist), oder über einen Pseudo-Konstruktor instantiiert. Beides führt zu einer festverdrahteten Abhängigkeit.

- throw: Die Client-Klasse verwendet die Supplier-Klasse als Exception-Objekt. Diese Beziehung beeinträchtigt die Testbarkeit nicht negativ.

Alle Fälle, in denen die Testbarkeit durch eine Refaktorisierung verbessert würde, welche mit geringem Aufwand realisierbar ist, wurden der Übersichtlichkeit halber in hellblauer Farbe hervorgehoben, bis auf einen Sonderfall (siehe Tabelle 12, ID 12181): Diese eine Abhängigkeit hat zwar einen statischen Methodenaufruf, der leicht refaktorisiert werden kann, aber aufgrund der statischen Instantiierung der Supplier-Klasse ist die Abhängigkeit in ihrer Gesamtheit nicht leicht refaktorisierbar.

Page 41: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

40

schlechtes Design

ID client class supplier class ��

��

��

��

��

����

���

���

��

��

��

��

��

��

����

���

���

��

��

����

��

����

���

���

����

��

���

��

�����

��

��

��

���

����

���

���

��

��

��

���

���

���

���

���

���

��

���

���

��

��

��

���

��

��

�!

���

��

��

���

��

��

"�

���

��

� ��

���

���

���

��

��

��

���

��

��

�!

��

�#�

���

���

��

�$

����

�����

� �

����

14203 SVAbsagenAA SVAbsagenK 5,56 1 1 x14484 SVAuswaehlenAA SVAuswaehlenK 6,25 1 1 x13375 SVKalkulierenAA SVKalkulierenK 6,25 1 1 x10251 PersonAendernAA PersonAendernK 7,14 1 1 x

9343 AnfrageStellenAA AnfrageStellenS 7,69 1 1 x9340 AnfrageStellenAA AnfrageStellenK 8,33 1 1 x

10362 PersonAendernErfassenAA SeminarisMeldung 8,33 95 58 x x607 ParameterModel AnfrageParameter 9,09 6 3 x634 SpaltenModel AnfrageSpalte 9,09 5 2 x

14882 SVVorbereitenAA SVVorbereitenK 10,00 1 1 x5396 SVAendernErfassenK Seminarveranstaltung 10,53 23 4 x x9153 AnfrageDefinierenAA AnfrageDefinierenS 11,11 1 1 x9534 BilanzErstellenAA BilanzErstellenK 11,11 1 1 x

12530 LeitungsauftragAendernAA LeitungsauftragAendernK 11,11 1 1 x12921 LeitungsauftragErfassenAA LeitungsauftragErfassenK 11,11 1 1 x12181 DVAuswaehlenAA DozentenvereinbarungAuswaehlenK 11,76 1 1 x x x

208 AnfrageStellenK ErgebnistabellenModel 12,50 1 1 x11970 DVAktionenAA DozentenvereinbarungErfassenK 12,50 5 5 x x12820 LeitungsauftragAuswaehlenAA LeitungsauftragAuswaehlenK 12,50 1 1 x

9098 AnfrageAendernAA AnfrageAendernK 14,29 1 1 x10977 BelegungGesamtS SeminarisMeldung 14,29 95 58 x x13959 FSVTeilnehmerZuordnenAA FSVTeilnehmerZuordnenK 14,29 1 1 x

8830 Seitenbeschreibung SeminarisTextLayout 14,29 1 1 x13054 SeminartypAendernAA SeminartypAendernK 14,29 1 1 x

5402 SVAendernErfassenK SeminarisMeldung 14,29 95 58 x x14623 SVDurchfuehrenAA SVDurchfuehrenK 14,29 1 1 x

43 AnfrageDefinierenK ParameterModel 16,67 4 2 x45 AnfrageDefinierenK SpaltenModel 16,67 4 2 x

7547 BelegungRelation BelegungTripel 16,67 1 1 x x9825 FirmaAendernErfassenAA SeminarisMeldung 16,67 95 58 x x6742 FirmenSeminarveranstaltung Geld 16,67 31 17 x

11139 GeschaeftspartnerAuswaehlenAA GeschaeftspartnerAuswaehlenK 16,67 1 1 x12621 LeitungsauftragAendernErfassenAA LeitungsauftragGesamtS 16,67 1 1 x

7119 OeffentlicheSeminarveranstaltung Geld 16,67 31 17 x5078 OeSVAendernK Seminarveranstaltung 16,67 23 4 x x

10346 PersonAendernErfassenAA GeschaeftspartnerGesamtS 16,67 2 2 x10342 PersonAendernErfassenAA PersonGesamtS 16,67 1 1 x10458 PersonAuswaehlenAA PersonAuswaehlenK 16,67 1 1 x11266 SeminarbelegungAA BelegungGesamtS 16,67 4 4 x11261 SeminarbelegungAA FirmaKurzS 16,67 2 2 x

3985 SeminartypAendernErfassenK SeminarisMeldung 16,67 95 58 x x7401 Seminarveranstaltung Geld 16,67 31 17 x7490 AnstellungRelation AnstellungPaar 20,00 1 1 x x1768 BelegungLoeschenK SeminarisMeldung 20,00 95 58 x x7790 BuchtRelation BuchtPaar 20,00 1 1 x x

12179 DVAuswaehlenAA DVAktionenAA 20,00 8 3 x12328 DVAuswaehlenAA DVInfo 20,00 1 1 x

9793 FirmaAendernErfassenAA FirmaGesamtS 20,00 1 1 x9789 FirmaAendernErfassenAA GeschaeftspartnerGesamtS 20,00 2 2 x8087 FirmenAnsprechpartnerRelation FirmenAnsprechpartnerPaar 20,00 1 1 x x

Tabelle 12: Fünfzig Abhängigkeiten mit niedrigem Wert der Metrik DSTMh

Page 42: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

41

Bei der Untersuchung der Abhängigkeiten der Tabelle 12 hat sich gezeigt, dass viele dieser Abhängigkeiten aus einem bestimmten Grund einen Wert von DSTMh < 0,5 haben: Der Grund für die meisten Fälle ist die statische Instantiierung der Supplier-Klasse (dies ist in der entsprechenden Spalte angegeben), wobei die eigentliche Nutzung (Methoden-Aufrufe) nicht-festverdrahtet realisiert ist.

Die Supplier-Klasse wird dabei entweder direkt durch den Standardkonstruktor oder indirekt über einen Pseudo-Konstruktor instantiiert; beides zählt als festverdrahteter Zugriff. Alle anderen Zugriffe der Client-Klasse auf die Supplier-Klasse sind dabei nicht festverdrahtet. Dieses Zugriffsmuster ist kein Fall von unbeabsichtigter Verwendung festverdrahteter Methoden. Eine Refaktorisierung wäre am ehesten über die Nutzung einer abstrakten Fabrik möglich und würde in jedem der oben dargestellten Fälle zur Verbesserung der Testbarkeit beitragen. Der Refaktorisierungs-Aufwand ist jedoch nicht gering, sondern eher als mittel einzustufen. Ein Beispiel mit relativ überschaubarem Refaktorisierungsaufwand wird im Abschnitt 5.4 angegeben. Eine besondere Häufung ist auffällig: Von den 40 Fällen in dieser Liste haben 16 Fälle ein bestimmtes ähnliches Muster. Es handelt sich dabei um die Akteur-Anwendungsfall-Schnittstellenklassen (kurz AAS), die von der Klasse SeminarisFrame abhängig sind. Diese Klasse SeminarisFrame hat eine Methode kontrollKlasseErzeugen, die jede der abgeleiteten Klassen implementieren muss. Diese abgeleiteten Klassen sind allesamt AAS-Klassen, siehe dazu das UML-Diagramm in Abschnitt 4.1 (Abbildung 4). Durch die zwangsweise Implementierung der genannten Methode kontrollKlasseErzeugen in den von SeminarisFrame abgeleiteten AAS-Klassen wird eine statische Instantiierung der zur jeweiligen Klasse gehörigen Kontrollklasse bewirkt, siehe dazu folgendes Codebeispiel aus der Klasse SVAbsagenAA: /** * Erzeugt Kontrollklasse. * Ueberschriebene Methode von SeminarisFrame */ protected void kontrollKlasseErzeugen() { svAbsagenK = SVAbsagenK.erzeuge(); };

Die Einführung einer class factory speziell für alle diese Kontrollklassen, verbunden mit der Übergabe des Interfaces dieser class-factory an alle kontrollKlasseErzeugen-Methoden (z.B. per Parameter) würde also erhebliche Verbesserungen der Testbarkeit bringen: Kein Pseudo-Konstruktor würde mehr aufgerufen, und aus allen genannten 16 Abhängigkeiten würden die (semi-) festverdrahteten Zugriffe entfallen, womit die Abhängigkeiten nicht-festverdrahtet werden. Dies könnte im genannten Beispiel so aussehen: /** * Erzeugt Kontrollklasse. * Ueberschriebene Methode von SeminarisFrame */ protected void kontrollKlasseErzeugen(IKontrollklassenFactory objFactory) { svAbsagenK = objFactory.SVAbsagenKInstanzErstellen(); };

Diese Refaktorisierung wäre mit mittlerem Aufwand zu bewältigen.

Page 43: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

42

Interessante Auffälligkeiten 1) Die statischen Methoden Seminarveranstaltung.areEqual In der Tabelle 12 sind die Abhängigkeiten Nummer 5396 (SVAendernErfassenK � Seminarveranstaltung) und 5078 (OeSVAendernK � Seminarveranstaltung) aus einem relativ unscheinbaren Anlass heraus festverdrahtet. Die Klasse Seminarveranstaltung bietet drei polymorphe Methoden namens areEqual an, welche alle drei statisch sind. Dieser Grund alleine führt dazu, dass die genannten Abhängigkeiten festverdrahtet sind. Der Code der drei polymorphen Methoden ist sehr kompakt, daher wird er hier komplett angegeben: public static boolean areEqual(Object obj1, Object obj2) { if (obj1 == obj2) return true; if ((obj1 == null) || (obj2 == null)) return false; return (obj1.equals(obj2)); } public static boolean areEqual(int int1, int int2) { return int1 == int2; } public static boolean areEqual(boolean bool1, boolean bool2) { return bool1 == bool2; }

Der Grund für die statische Implementierung dürfte wohl folgender sein: Der Entwickler war der Ansicht, dass diese Methoden aufgerufen werden können, ohne dass dem Aufrufer eine konkrete Instanz einer Seminarveranstaltung vorliegt. Bei der Analyse des Gesamtsystems stellt sich dies jedoch als nicht zutreffend heraus. Aus Sicht der Testbarkeit sind zwei Probleme auffällig:

1. Die Tatsache, dass die Methoden statisch sind, macht die Abhängigkeiten von Seminarveranstaltung festverdrahtet (dies gilt für die Client-Klassen SVAendernErfassenK, OeSVAendernK, OeffentlicheSeminarveranstal-tung und FirmenSeminarveranstaltung). Die Festverdrahtung sorgt dafür, dass beim Test der Client-Klassen die Supplier-Klassen nicht ausgetauscht werden, und dies beeinträchtigt die Testbarkeit negativ.

2. Die erste der drei gleichnamigen Varianten ist nur äußerst schwierig zu testen; die Testmethode, die die Funktionalität abdecken soll, muss sämtliche Objekttypen kennen (und die Methode damit aufrufen), die jemals an einen Aufruf der Methode übergeben werden könnten. Hier besteht die große Gefahr der Unterlassung bzw. des Vergessens bestimmter Objektarten, insbesondere wenn später Erweiterungen an der Klasse vorgenommen werden. Bei der erstmaligen Erstellung von Testmethoden für die bestehende Klasse entstehen besonders hohe Aufwände, weil dabei das gesamte Projekt auf die möglichen Kombinationen von Objekttypen hin analysiert werden muss.

Aus Sicht des sauberen objektorientierten Designs ist zum einen zu bemängeln, dass diese Methoden keine Aufgabe erfüllen, die in irgendeiner Weise fachlich mit der Klasse zu tun haben, in der sie implementiert sind. Des Weiteren ist auch die Tatsache, dass die Methoden offenbar grundlos statisch implementiert sind, als ungünstig zu bewerten.

Page 44: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

43

Der Vorschlag für eine Refaktorisierung wäre die Entfernung der drei oben genannten Methoden, und die Schaffung von vier neuen Methoden der Klasse Seminarveranstaltung, die nicht statisch sind:

1) public boolean hatIdentischesZeitfenster(Seminarveranstaltung objAndereSV) zum Aufruf aus SVAendernErfassenK und OeSVAndernK. Prüfung von Start und Beginn, Uhrzeit und Datum.

2) public boolean hatIdentischeKonditionen(Seminarveranstaltung objAndereSV) zum Aufruf aus OeffentlicheSeminarveranstaltung. equals. Vergleich von Preis, Max- und Min-Teilnehmerzahl.

3) public boolean hatIdentischeTagesadresse(Seminarveranstaltung objAndereSV) zum Aufruf aus SVAendernErfassenK. Vergleich aller Adressbestandteile der Seminarveranstaltung.

4) public boolean istBuchhalterischIdentisch(Seminarveranstaltung objAndereSV) zum Aufruf aus FirmenSeminarveranstaltung. Vergleich von Rechnungsdatum und -nummer, Auftragsdatum und -nummer, Preis und Teilnehmerzahl.

Die Methode Seminarveranstaltung.equals müsste ebenfalls auf die Nutzung von areEqual verzichten, was aber mit sehr geringem Aufwand realisierbar ist, weil areEqual ohnehin nur die Objektidentität (this) abfragt und ansonsten die Methode equals des zu prüfenden Objekts aufruft. Mit diesen Änderungen ist die Refaktorisierung getan, und folgende Vorteile werden erreicht:

1) Die genannten vier festverdrahteten Abhängigkeiten werden zu nicht-festverdrahteten Abhängigkeiten, mit dem Vorteil der Ersetzbarkeit der Supplier-Klassen für die Tests der Client-Klassen.

2) Die genannten Nachteile des unsauberen Designs werden eliminiert. Zusätzlich werden folgende Vorteile erzielt: Die neuen Methoden haben eindeutig spezifizierte Aufgaben, und sie passen fachlich in die Klasse Seminarveranstaltung hinein.

3) Die Client-Klassen müssen deutlich weniger fachliches Wissen über die Supplier-Klasse haben: Anstatt viele einzelne Attribute mit get-Methoden abzurufen und einzeln zu vergleichen, kann nun ein einzelner fachlicher Vergleich aufgerufen werden. Dies fördert das Prinzip des information hiding.

4) Das Testproblem mit den unendlichen Aufrufvarianten des Object-Parameters ist beseitigt.

Der Aufwand für die Refaktorisierung ist mit ca. zwei Stunden anzusetzen. Die Bewertung des Testaufwandes und der Testzuverlässigkeit für den Fall, dass die Refaktorisierung nicht gemacht wird, lässt sich nicht eindeutig quantifizieren. Die beiden oben aufgeführten Probleme sind eher als schwerwiegend einzustufen (wegen des Problems Nr. 2), und sie wirken sich besonders negativ auf die Testbarkeit aus, weil mehrere Klassentests betroffen sind.

Page 45: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

44

2) unnötige statische Methodenaufrufe Die Abhängigkeit Nummer 12181 betrifft die Klasse DVAuswaehlenAA als Client-Klasse und DozentenvereinbarungAuswaehlenK als Supplier-Klasse. Bei dieser Abhängigkeits-beziehung gibt es zwei einzelne festverdrahtete Abhängigkeiten:

1) Der Konstruktor-Aufruf. 2) Die Methode getST aus der Supplier-Klasse ist statisch definiert, und wird von der

Client-Klasse aufgerufen. Die zweite festverdrahtete Abhängigkeit ist unnötig, und sie ist leicht zu refaktorisieren. Wenn man dieser Methode getST einen String mit einem Seminartyp-Identifier hereingibt, dann gibt die Methode ein Objekt zurück, welches das Interface ISeminartyp implementiert. Der Entwickler hatte wohl ursprünglich beabsichtigt, durch die statische Gestalt der Methode auch ohne konkretes Objekt vom Typ der Supplier-Klasse die Beschaffung eines ISeminartyp-Objekts zu ermöglichen. Die Analyse des gesamten Systems hat ergeben, dass die Methode oeffnenSTSchl der Client-Klasse die einzige Stelle ist, an der die Methode getST aufgerufen wird: void oeffnenSTSchl(String sTSchl) { try { dVAuswaehlenK.setST (DozentenvereinbarungAuswaehlenK.getST(sTSchl)); } catch (Exception e) { MeldungsAnzeigeA.anzeigen(e, HauptFensterProvider.getHauptFenster()); } anzeigenModal(); }

Es wird dabei sofort eines deutlich: Eine konkrete Instanz eines Objektes der Supplier-Klasse ist vorhanden, sogar in derselben Zeile. Mit einer minimalen Refaktorisierung kann also erreicht werden, dass die Methode getST der supplier class nicht mehr statisch sein muss. Es gibt zwei Möglichkeiten:

1) Ersetzen des statischen Aufrufs (DozentenvereinbarungAuswaehlenK. getST(sTSchl)) durch einen nicht-statischen Aufruf (dVAuswaehlenK. getST(sTSchl))

2) Schaffung einer neuen Methode setST in der Supplier-Klasse, welche als Parameter einen String annimmt, und sich das ISeminartyp-Objekt selbst heraussucht.

Analoge Fälle trifft man auch in anderen Abhängigkeiten. Die folgende Tabelle führt alle drei gefundenen Stellen auf: ID Client-Klasse Supplier-Klasse Name der Methode 12181 DVAuswaehlenAA DozentenvereinbarungAuswaehlenK getST 11970 DVAktionenA DozentenvereinbarungErfassenK getST 11970 DVAktionenA DozentenvereinbarungErfassenK getPerson

Tabelle 13: unnötige statische Methodenaufrufe

Page 46: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

45

Es handelt in diesen Fällen nicht um systematische Gründe für die statischen Methodenaufrufe, z.B. weil verwandte Klassen ebenfalls statische get-Methoden nach ähnlichem Muster implementieren. Andere Kontrollklassen mit ähnlichen Aufgaben, die ebenfalls im übergeordneten Paket SeminarverwaltungP liegen, haben zwar auch get-Methoden zum Zugriff auf bestimmte Objekte über deren Schlüssel, jedoch sind diese Methoden nicht statisch (Beispiele: SeminartypErfassenK.getObjektST(), SeminartypAendernK.getObjektST(String einSchluessel), FSVAendernK. getObjektSV(String einSchluessel)). 3) statische Konstanten ID Client-Klasse Supplier-Klasse verwendete Konstante 10362 PersonAendernErfassenAA SeminarisMeldung QUESTION 10977 BelegungGesamtS SeminarisMeldung WARNING 5402 SVAendernErfassenK SeminarisMeldung ERROR 9825 FirmaAendernErfassenAA SeminarisMeldung QUESTION 3985 SeminartypAendernErfassenK SeminarisMeldung ERROR 1768 BelegungLoeschenK SeminarisMeldung WARNING

Tabelle 14: Verwendung von statischen Konstanten

Bei insgesamt sechs Abhängigkeiten (siehe Tabelle 14) fällt auf, dass es festverdrahtete Zugriffe auf eine bestimmte Supplier-Klasse gibt, die alle eines gemeinsam haben: Der Zugriff erfolgt ausschließlich über statische Konstanten, es gibt keine anderen Zugriffe der Client-Klassen auf die Supplier-Klasse. Die Supplier-Klasse SeminarisMeldung definiert dafür die folgenden fünf statischen Konstanten: public static final int PLAIN = -1; public static final int ERROR = 0; public static final int INFORMATION = 1; public static final int WARNING = 2; public static final int QUESTION = 3;

Diese Konstanten stellen eine Art globale Variablen dar, denn sie unterscheiden sich von globalen Variablen nur durch die Notwendigkeit der Angabe des Klassennamens, vergleichbar einer Art Namespace. Schon diese Tatsache deutet auf ein schlechtes Design hin. Wenn man sich die Codezeilen, mit denen der Zugriff auf diese Konstanten erfolgt (und die die festverdrahtete Abhängigkeit bewirken) einmal genauer ansieht, bekommt man schnell eine Idee für eine einfache Refaktorisierung: meldung = HinweisNachricht.erzeuge("Bitte einen gültigen Geldbetrag eingeben.", "Stornogebühr ungültig", SeminarisMeldung.WARNING); [...] meldung = HinweisNachricht.erzeuge("Bitte eine Auftragsnummer angeben.", "Auftragsnummer fehlt", SeminarisMeldung.WARNING); [...] meldung = HinweisNachricht.erzeuge("Bitte einen gültigen Geldbetrag eingeben.", "Rechnungssumme ungültig", SeminarisMeldung.WARNING); [...]

Page 47: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

46

Die Klasse HinweisNachricht erbt von der Klasse SeminarisMeldung, daher bietet sich für die drei genannten Codestellen eine Methode an, die HinweisNachricht. erzeugeWarnung heißen könnte. Diese Methode nimmt nur noch einen Meldungstext und eine Überschrift entgegen, und die Konstante WARNING entfällt. Angesichts der Tatsache, dass die reine Anzahl der genannten Konstanten sehr klein und überschaubar ist (siehe Tabelle 14), könnte man diese Vorgehensweise analog auf die übrigen Konstanten anwenden, zum Beispiel für Informationsmeldungen und Fehlermeldungen. Der Refaktorisierungsaufwand alleine für den Fall WARNING beträgt etwa 10 Minuten, und dadurch würde die Abhängigkeit Nummer 10977 völlig frei von festverdrahteten Zugriffen. Diese Beobachtung führte zur Refaktorisierung der Behandlung von Benutzernachrichten, welche im übernächsten Abschnitt 5.4 beschrieben wird, und im Anhang B vollständig dokumentiert ist. 4) Verwendung von .class In insgesamt vier Fällen wird auf das Attribut .class der Supplier-Klasse zugegriffen: ID Client-Klasse Supplier-Klasse 7547 BelegungRelation BelegungTripel 7490 AnstellungRelation AnstellungPaar 7790 BuchtRelation BuchtPaar 8087 FirmenAnsprechpartnerRelation FirmenAnsprechpartnerPaar

Tabelle 15: Verwendung des Schlüsselworts .class

Das Attribut .class wird von der Basisklasse Object zur Verfügung gestellt, und ist als public final deklariert. Die Verwendung von .class in der Supplier-Klasse behindert das Testen erheblich, da die Supplier-Klasse dann nicht mehr austauschbar ist. Im konkreten Fall wird dem Persistenzrahmenwerk ein Parameter übergeben, der die Identifikation der Klasse der jeweiligen Parameterinstanz ermöglichen soll. Würde die Supplier-Klasse für Testzwecke durch ein Stub- oder Mockobjekt ausgetauscht, so würde der entsprechende Aufruf in das Persistenzrahmenwerk nicht mehr das Gewünschte leisten. Zumindest solange nicht, bis die aufgerufene Methode auf alle möglichen Ersatzobjekte vorbereitet worden ist (siehe dazu Absatz 2.1, Stub- und Mockobjekte). Die Verwendung von .class stellt eine Verletzung des guten objektorientierten Programmierstils dar, weil damit grundsätzlich eine starke Kopplung verschiedener Klassen erzwungen wird. Die entsprechenden Abhängigkeiten bekamen daher in der Tabelle 12 eine Markierung unter der Spalte schlechtes Design / Zugriff auf .class. Der Refaktorisierungsaufwand für die Fälle mit .class-Zugriff ist als hoch zu bewerten. Diese vier genannten Stellen sind auch die einzigen Fälle aus der Auswahl der fünfzig Abhängigkeiten, die mit einem hohen Refaktorisierungsaufwand eingestuft werden.

Page 48: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

47

5.3 Untersuchung von zufällig ausgewählten Abhängigkeiten Im Folgenden werden 50 Abhängigkeiten mit einer Zufallsfunktion ausgewählt18, anhand der gleichen Kriterien wie im vorigen Abschnitt bewertet. Auch hier erfolgt eine tabellarische Übersicht mit den Bewertungen wie im vorigen Abschnitt. Die Identifikator-Spalte (ID) wurde dabei direkt übernommen aus Design2Test. Eine dieser zufälligen Abhängigkeiten, AnfrageStellenAA � AnfrageStellenS, kam auch bereits in der ersten, systematisch ausgewählten Liste vor (ID-Nr. 9343). In dieser Tabelle ist die überwiegende Mehrzahl an Fällen eine bewusste Festverdrahtung. Dies gilt zum Beispiel, wenn die festverdrahtete Abhängigkeit von einer Client-Klasse zu einer class factory besteht, die mit einer statischen Methode instantiiert oder referenziert wird. In den untersuchten Fällen sind dies beispielsweise Aufrufe der Art SeminarisDatenbankProvider.getDatenbank, bei denen eine globale class factory zur Beschaffung der Instanz der Datenbank verwendet wird. Diese Methode instantiiert notfalls einen SeminarisDatenbankProvider, falls noch keine Instanz vorhanden ist. Es sind zwölf Aufrufe der Kategorie „GlobalerOrdner / GlobalerService.getEinzigeInstanz“ dabei; hierbei könnte zwar eine einzige class factory für globale Services leichte Verbesserungen bringen (für die Austauschbarkeit der Services zu Testzwecken), der Refaktorisierungs-Aufwand wäre jedoch wegen der breiten Streuung dieser Aufrufe sehr hoch (alleine in den zwölf genannten Fällen sind sieben verschiedene globale Ordner bzw. Services enthalten).

18 Diese Zufallsfunktion wählte aus der Grundgesamtheit aller Abhängigkeiten des Systems aus, anhand der Identifikationsnummer (ID).

Page 49: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

48

schlechtes Design

ID Client-Klasse Supplier-Klasse DS

TMh

Abh

. von

Sup

plie

r-K

lass

e / g

es.

Abh

. von

Sup

plie

r-K

lass

e / h

w

leic

ht re

fakt

oris

ierb

ar

vers

ehen

tlich

/ un

beab

sich

tigt

stat

isch

e K

onst

ante

n

Zugr

iff a

uf "

.cla

ss"

stat

isch

er M

etho

dena

ufru

f, ni

cht

notw

endi

g

bew

ußt f

estv

erdr

ahte

t

stat

isch

er M

etho

dena

ufru

f, be

grün

det

stat

isch

e In

stan

tiier

ung

thro

w

116 AnfrageDefinierenK AnfrageSpalte 100,00 5 2 x137 AnfrageDefinierenK HinweisNachricht 100,00 58 58 x x x183 AnfrageLoeschenK AnfrageOrdner 33,33 5 5 x x235 AnfrageStellenK AnfrageOrdner 50,00 5 5 x x277 AnfrageStellenK ListenDruckDK 100,00 1 1 x x293 BilanzDK DatumUhrzeit 100,00 19 19 x x320 BilanzErstellenK SeminarisDatenbankProvider 100,00 74 74 x x997 PersonAendernErfassenK SeminarisMeldung 30,00 95 58 x x x x

1107 PersonAendernK GeschaeftspartnerOrdner 50,00 18 18 x x1220 PersonAendernK KeineNachricht 100,00 38 38 x x x1570 BelegungAendernK ProtokollProvider 100,00 20 20 x1625 BelegungAendernK SVSeminartypRelation 50,00 11 11 x x1761 BelegungBearbeitenK SynchronisationProvider 100,00 34 34 x1837 BelegungLoeschenK RechnungsempfaengerRelation 50,00 6 6 x x2001 BelegungStornierenK GutschriftStornierungDK 50,00 1 1 x2005 BelegungStornierenK MitteilungStornierungDK 50,00 1 1 x2214 MitteilungBelegungsaenderungDK BelegungRelation 50,00 23 23 x x2577 OeSVRechnungDK BelegungRelation 33,33 23 23 x x3411 DozentenvereinbarungLoeschenK IDozentenvereinbarungK 100,00 8 6 x x x3427 DozentenvereinbarungLoeschenK DatumUhrzeit 100,00 19 19 x x3612 LeitungsauftragAendernErfassenK GeschaeftspartnerOrdner 50,00 18 18 x x3659 LeitungsauftragAendernErfassenK LeitungsauftragRelation 33,33 22 22 x x4231 SVKalkulierenK GeschaeftspartnerOrdner 50,00 18 18 x x4799 FSVErfassenK FSVAuftragsbestaetigungDK 50,00 1 1 x5107 OeSVAendernK SynchronisationProvider 100,00 34 34 x5156 OeSVAendernK DatumUhrzeit 100,00 19 19 x5217 OeSVErfassenK KeineNachricht 100,00 38 38 x x x5233 SVAbsagenK SeminarveranstaltungOrdner 50,00 17 17 x x5275 SVAbsagenK BelegungRelation 50,00 23 23 x x5433 SVAendernErfassenK SVSeminartypRelation 33,33 11 11 x x5597 SVAuswaehlenK SeminarisDatenbankProvider 100,00 74 74 x x5788 SVLoeschenK SynchronisationProvider 100,00 34 34 x5918 SVVorbereitenK TeilnahmebescheinigungDK 50,00 1 1 x6027 ErweitertPersistent SeminarisDatenbankProvider 100,00 74 74 x x7609 BelegungRelation AssoziationsException 42,86 13 8 x x7683 BelegungRelation DatumUhrzeit 100,00 19 19 x x7961 DozentenvereinbarungTripel DVSchluessel 71,43 1 1 x x x8538 SVAnsprechpartnerRelation SVAnsprechpartnerPaar 25,00 1 1 x x x x8751 DruckDokumentDK KonfigurationProvider 100,00 8 8 x x8868 SeminarisStandard SeminarisClassWriter 100,00 1 1 x x9343 AnfrageStellenAA AnfrageStellenS 7,69 1 1 x9444 AnfrageStellenAA SynchronisationProvider 100,00 34 34 x9464 AnfrageStellenS ParameterS 25,00 1 1 x9984 FirmaErfassenAA FirmaErfassenK 33,33 1 1 x

10740 BelegungAendernAA KeineNachricht 100,00 38 38 x x x12415 PersonListe IDozentenvereinbarungK 33,33 8 6 x x12435 PersonListe MeldungsAnzeigeA 100,00 49 49 x x12510 DozentenvereinbarungErfassenAA AAKlassenFabrikProvider 100,00 13 13 x x14368 SVAendernErfassenAA MeldungsAnzeigeA 100,00 49 49 x x14415 SVAendernErfassenAA AAKlassenFabrikProvider 100,00 13 13 x x

Tabelle 16: Fünfzig zufällig ausgewählte Abhängigkeiten

Page 50: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

49

Interessante Auffälligkeiten 1) Notwendige Festverdrahtung In den systematisch ausgewählten Fällen (Tabelle 12) war weder ein Fall mit „bewusster Festverdrahtung“ dabei, noch ein Fall mit begründeter Verwendung eines statischen Methodenaufrufs19. In den zufällig ausgewählten Fällen (Tabelle 16) haben 34 von 50 Fällen mindestens eine dieser beiden Eigenschaften. Die beiden Eigenschaften werden im Folgenden unter den gemeinsamen Oberbegriff notwendige Festverdrahtung gestellt, um zu verdeutlichen, dass selbst unter dem Betrachtungsaspekt der guten Testbarkeit in diesen Fällen keine Verbesserung erreicht werden kann (ohne Änderungen der Systemarchitektur). 2) Die Abhängigkeit DozentenvereinbarungLoeschenK � IDozentenvereinbarungK

(ID 3411) Die Abhängigkeit DozentenvereinbarungLoeschenK � IDozentenvereinbarungK tritt nur aufgrund eines einzigen Aufrufs auf. Die Schnittstelle IDozentenvereinbarungK stellt eine Konstante bereit: public interface IDozentenvereinbarungK { (...) public static final int protokollpuffergroesse = 200; (...) }

Die Klasse DozentenvereinbarungLoeschenK greift auf diesen Wert zu, und wird wegen des statischen Charakters der Variable zwangsweise festverdrahtet. Auffällig ist dabei die Konstellation der Implements-Beziehungen des Interfaces IDozentenvereinbarungK.

Abbildung 6: Implements-Beziehungen des Interfaces IDozentenvereinbarungK

Die Kontrollklassen DozentenvereinbarungErfassenK und Dozentenvereinbarung-AendernK, die ähnlich strukturiert sind wie die untersuchten Klasse Dozentenverein-barungLoeschenK, implementieren das Interface IDozentenvereinbarungK. Bei dieser Konstruktion handelt es sich nach Auffassung des Verfassers dieser Arbeit um ein Versehen; dies wird im Folgenden belegt (es ist jedoch in der Systemdokumentation nicht eindeutig nachweisbar).

19 Die vier Fälle mit Zugriff auf das statische Konstrukt .class fallen nicht unter diese Kategorien.

Page 51: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

50

Im Folgenden ist der Codeausschnitt mit der statischen Verwendung wiedergegeben: if (synchroManager.istErlaubtLoeschen(dV, this)) { ProtokollProvider.getProtokollManager().anmelden(10, this); StringBuffer protokolleintrag = new StringBuffer (IDozentenvereinbarungK.protokollpuffergroesse); protokolleintrag.append("Löschen:\n"); protokolleintrag.append(dV.toString()); ProtokollProvider.getProtokollManager().anwendungsLog( protokolleintrag.toString()); dV.loescheDV(); ProtokollProvider.getProtokollManager().anwendungsLog( "Das Löschen wurde abgeschlossen.\n\n"); ProtokollProvider.getProtokollManager().abmelden(this); }

Beim Betrachten des Codeausschnitts wird klar, dass die Konstante protokollpuffergroesse alleine dazu da ist, die Größe des zu reservierenden String-Speichers für die textliche Serialisierung einer Dozentenvereinbarung (mit der Methode Dozentenvereinbarung.toString) anzugeben. Der ProtokollManager nimmt jeden String, unabhängig von der Länge, entgegen. Daher ist die String-Puffergröße vom fachlichen Standpunkt eine Eigenschaft, die in die Klasse Dozentenvereinbarung selbst hinein gehört, da nur diese Klasse über die Implementierungsdetails und Hilfsobjekt-Anforderungen ihrer Methode toString Bescheid wissen sollte. Durch Schaffung einer Methode getProtokollPuffergroesse innerhalb der Klasse Dozentenvereinbarung könnte also diese statische Abhängigkeit (ID 3411) komplett entfernt werden. Der Refaktorisierungsaufwand (inklusive der zu Ändernden Klassen DozentenvereinbarungErfassenK und DozentenvereinbarungAendernK) beträgt nur etwa 15 Minuten. 3) statische Konstanten Wie bereits im vorhergehenden Abschnitt ausführlich erläutert, finden auch in dieser Auswahlliste viele Zugriffe auf die statischen Konstanten der SeminarisMeldung und deren Derivatobjekte (HinweisNachricht, KeineNachricht) statt, in insgesamt fünf Fällen. 5.4 Exemplarische Refaktorisierung In den beiden vorangegangenen Abschnitten hat sich gezeigt, dass eine Refaktorisierung der Benutzernachrichten-Behandlung, welche die statischen Abhängigkeiten zu den Klassen HinweisNachricht, KeineNachricht und SeminarisMeldung aufheben würde, und am besten gleichzeitig noch die Nutzung der statischen globalen Variablen in SeminarisMeldung unterbinden könnte, eine erhebliche Verbesserung der Testbarkeit mit sich bringen würde. Von den 18 blau markierten festverdrahteten Abhängigkeiten der beiden Tabellen (Tabelle 12 und Tabelle 16) könnten nach dieser Refaktorisierung 11 komplett entfallen (genauer gesagt würden diese Abhängigkeiten dann nicht mehr festverdrahtet sein). Eine exemplarische Refaktorisierung wird im Anhang Anhang B detailliert beschrieben.

Page 52: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

51

Zusammenfassung der Ergebnisse der Refaktorisierung:

1) Durch Schaffung einer class factory zusammen mit einem zugehörigen Interface konnten alle statischen Instantiierungen der Klassen FrageNachricht, HinweisNachricht, ExceptionNachricht und KeineNachricht alternativ über diese neue class factory umgangen werden.

2) Auch die Zugriffe auf die öffentlichen statischen Attribute der Klasse SeminarisMeldung konnten auf diesem Weg vermieden werden.

3) Durch geschickte Anordnung der Instantiierung der class factory, und Weitergabe der Instanz der class factory an die Verwender - Methoden, konnte in vielen Fällen sogar darauf verzichtet werden, die festverdrahteten Abhängigkeiten durch eine andere festverdrahtete Abhängigkeit zu ersetzen; in den meisten Fällen liegt jetzt nur noch eine nicht-festverdrahtete Abhängigkeit von dem neuen Interface vor.

4) Die Gesamtzahl der festverdrahteten Abhängigkeiten im System konnte von 1085 auf 965 reduziert werden, die Projektmetriken blieben (bis auf eine minimale Änderung des Wertes ACD) unverändert.

5) Wegen des hohen Aufwands wurde die konsequente Umsetzung der Refaktorisierung nach etwa einem Tag Aufwand abgebrochen. Der Weg der Umsetzung ist damit aufgezeigt, und die bisherigen Ergebnisse zeigen bereits den Erfolg der Maßnahme.

5.5 Vergleich und Auswertung Bei der gezielten Auswertung von Abhängigkeiten anhand der Metrik DSTMh wurden bedeutend mehr Fälle gefunden, bei denen mit wenig Refaktorisierungsaufwand eine Verbesserung der Testbarkeit erreicht werden kann, als bei der zufälligen Auswahl. Im ersten Fall waren es 9 von 50 untersuchten Fällen, im zweiten Fall nur 7 von 50. Für die Fälle bewusst festverdrahtet und statischer Methodenaufruf, begründet, welche unter dem Begriff notwendige Festverdrahtung zusammengefasst werden, wird ein hoher Aufwand für eine Refaktorisierung geschätzt. Unter diese Kategorie fallen bei der zufälligen Auswahl (Tabelle 16) 34 von 50 Fällen. Bei der systematischen Auswahl (Tabelle 12) ist kein Fall mit notwendiger Festverdrahtung gefunden worden, jedoch vier Fälle mit Zugriff auf das Konstrukt .class, wodurch diese vier Fälle in der Kategorie „hoher Refaktorisierungsaufwand“ einzustufen sind. Für die restlichen Fälle (weder mit niedrigem, noch mit hohem Aufwand), wird ein mittlerer Refaktorisierungsaufwand angenommen, weil dort mit der Schaffung einer class factory Verbesserungen erreicht werden können. Bei der ersten Auswahl betrifft dies 37 von 50 Fällen, bei der zweiten Auswahl 9 von 50. systematische Auswahl zufällige Auswahl Niedriger Refaktorisierungsaufwand 18,0 % 14,0 % Mittlerer Refaktorisierungsaufwand 74,0 % 18,0 % Hoher Refaktorisierungsaufwand 8,0 % 68,0 %

Tabelle 17: statistische Auswertung der untersuchten Fälle

Nach Auswertung dieser Zahlen (Tabelle 17) kann die eingangs aufgestellte Hypothese als bestätigt angenommen werden. Die Werte für die Fälle mit niedrigem Refaktorisierungsaufwand sind nicht wesentlich besser im Fall der systematischen Auswahl, jedoch sind die Werte für die Fälle mit hohem Refaktorisierungsaufwand ganz erheblich höher bei der zufälligen Auswahl von Abhängigkeiten: Man kann als Resultat festhalten, dass durch systematische Auswahl 92 % der gewählten Abhängigkeiten keinen hohen Refaktorisierungsaufwand erfordern.

Page 53: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Untersuchung einer Hypothese bezüglich der Metrik DSTMh

52

Es ist dabei noch zu beachten, dass sich einige der oben genannten Schätzungen der Kategorie „mittel“ nur auf die Erstellung einer class factory bezieht. Wenn die nötigen Ersetzungen bei der Verwendung der Supplier-Klasse nicht automatisch durch Verwendung der „Suchen und Ersetzen“-Funktionalität der Entwicklungsumgebung bewerkstelligt werden kann, wie in der exemplarischen Refaktorisierung (Anhang B) aufgezeigt, dann hängt der gesamte Refaktorisierungsaufwand noch von der Anzahl der Client-Klassen ab, die die Supplier-Klasse verwenden. Die Spalte Abh. von der supplier class / gesamt, die in beiden Tabellen (Tabelle 12 und Tabelle 16) angegeben ist, enthält diesen Wert für die jeweilige Abhängigkeit. Ein hoher Wert in dieser Spalte bedeutet eine breite Streuung der Verwendung der Supplier-Klasse, und dies kann zu einem wesentlich höheren tatsächlichen Refaktorisierungsaufwand führen. Wenn sehr viele (semi-) festverdrahtete Abhängigkeiten zu einer bestimmten supplier class bestehen, dann ist die positive Auswirkung einer Refaktorisierung auf das Gesamtsystem natürlich höher als im Falle niedrigerer Werte. Die exemplarische Refaktorisierung in Abschnitt 5.4 wurde daher ganz bewusst mit der Gruppe von supplier classes durchgeführt, die hier die höchsten Werte aufwiesen. Die Gesamtzahl der (semi-) festverdrahteten Abhängigkeiten im System konnte mit dieser Maßnahme von 1085 auf 965 reduziert werden (siehe Anhang B).

Page 54: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

53

* + � � � �� , � � & � � �� - �

Ein interessantes Testproblem, welches nicht zur eigentlichen Aufgabenstellung gehört, wird in diesem Kapitel beschrieben. 6.1 Die zyklische Abhängigkeit zwischen einer Klasse und ihrem

zugehörigen Interface Im Rahmen der Vorbereitungen zu den eigentlichen Fragestellungen dieser Diplomarbeit wurde ein Unit-Test für die Klasse Geschaeftspartner erstellt, welche eine Basisklasse für die bedeutenden Domänenklassen Firma und Person darstellt und eine abstrakte Klasse ist. Die Interfaces aller drei genannten Klassen besitzen jeweils Abhängigkeiten zu den implementierenden Klassen selbst, welches gravierende Folgen für die Testbarkeit hat, und daher nicht unerwähnt bleiben soll. Im Anhang A.1 ist das UML-Diagramm für den genannten Klassenkomplex (Geschaeftspartner, Firma und Person, jeweils mit zugehörigem Interface) abgebildet. Da die Klasse Geschaeftspartner abstrakt ist, und keine eigene Logik und Funktionalität enthält, ist die Erstellung von Testmethoden für diese Klasse nicht sinnvoll, da Geschaeftspartner ohnehin nicht instantiiert werden kann. Die Methoden der Klasse werden aus den abgeleiteten Klassen heraus aufgerufen, und können nur auf diesem Weg getestet werden. Die Anzahl und Komplexität der Methoden von Geschaeftspartner ist darüber hinaus äußerst niedrig, nur eine einzige Methode (die weiter unten beschrieben wird) enthält eigenständige funktionale Logik. Getestet wurden daher ausschließlich die beiden abgeleiteten Klassen Firma und Person. Die folgende Tabelle 18 gibt einen Überblick über die Abhängigkeiten der Klasse Firma, und dazu eine Bewertung der jeweiligen Abhängigkeit. Die Tabellenüberschriften sind analog den Tabellen aus dem Kapitel 3, außer dass statt der Distanz hier nur die Tatsache „direkt oder indirekt abhängig“ interessant ist.

Page 55: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Weitere Testprobleme

54

Verwendg. im Test Auswirkung auf d. Testaufwand

Supplier-Klasse indi

rekt

e A

bhän

gigk

eit

fach

lich

rele

vant

Tes

tset

up /

Ein

gabe

para

met

er

Ass

ertio

ns

Exc

eptio

ns

Feh

ler i

n S

uppl

ierk

lass

e ?

Problembeschreibung Tes

tvor

bere

itung

Tes

tfalle

rste

llung

Tes

tdur

chfü

hrun

g

IGeschaeftspartner x x - - - - -IFirma - x - - - - -IPerson x - - - - -ErweitertPersistent - x x - Transaktion erforderl. - hoch hochGeschaeftspartner - x x x - - - - -Person x - - - - -IPerson x - - - - -IErweitertPersistent x - - - - -ISeminarisDatenbank - x - - - - -SeminarisDatenbankProvider x x x - - - - -SeminarisDatenbank x - - - - -ProtokollProvider x - - - - -IProtokollManager x - - - - -ProtokollManager x - - - - -IProtokollAusgabeKunde x - - - - -IKonfiguration x - - - - -KonfigurationProvider x - - - - -Konfiguration x - - - - -KonfigurationException x - - - - -SeminarisException x - - - - -CommitOrRollbackWithoutStartTransactionExceptionx - - - - -

Tabelle 18: Abhängigkeiten der Klasse Firma

Beobachtungen bei der Erstellung der Testklassen und der Testdurchführung Das Instantiieren eines Firma-Objekts hat sich als nicht trivial herausgestellt. Die Klasse Firma sieht persistierbare Instanzen vor, so dass eine Instantiierung immer mit Datenbankzugriffen verbunden ist. Die Klasse Firma erbt von der Klasse ErweitertPersistent, welche für alle persistierbaren Klassen des Fallbeispiels Methoden wie getDb(), getSchluessel() und zerstoere() bereitstellt, um eine einheitliche Schnittstelle zur Speicherung und Verwaltung der einzelnen Instanzen zu gewährleisten. Da es keinen öffentlichen Konstruktor für Firma gibt, muss die Objektinstantiierung über die statische Methode erzeuge() erfolgen, welche einen Pseudo-Konstruktor darstellt (siehe Abschnitt 2.1) und über das Persistenzrahmenwerk einen persistenten Datensatz erzeugt. Diese Methode erfordert den vorherigen Start einer Datenbank-Transaktion, andernfalls wird eine Exception vom Typ DatenbankAusnahme ausgelöst. Ein Testklassen-Autor ist also gezwungen, sich vorher mit den Gegebenheiten des Persistenzrahmenwerks vertraut zu machen, bevor er einzelne Domänenklassen (welche allesamt analoge Pseudo-Konstruktoren haben) instantiieren kann, um den Grund für diese Laufzeit-Ausnahme herauszufinden; der Inhalt der Exception ist nämlich leer und damit für die Behandlung des Problems wertlos.

Page 56: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Weitere Testprobleme

55

Ursprünglich stammt diese Exception aus der Klasse Seminarisdatenbank, und die war auch mit einem ordentlichen Fehlertext versehen („Es ist keine Transaktion gestartet.“), doch in der Methode Firma.erzeuge() wurde sie nicht weitergeworfen, stattdessen wurde eine neue, leere DatenbankAusnahme erzeugt. Dadurch wird natürlich die Fehlersuche während des Testlaufs erschwert. Die Methode Firma.erzeuge musste daher entsprechend korrigiert werden, und das Transaktionshandling wurde beim Test-Setup mit eingefügt. Die Tatsache, dass die Verwendung von Methoden des Persistenzrahmenwerks (PRW) für das Testsetup notwendig ist, und dass sogar ganz bestimmte Methoden aufgerufen werden müssen, damit das Instantiieren der CUT gelingt, erschwert das Testen

- durch die Notwendigkeit zusätzlicher Codeerzeugung (der so genannte Transaktions-rahmen)

- durch die zwangsweise Einarbeitung in das PRW durch den Ersteller der Testmethoden

- durch zusätzliche potentielle Fehlerquellen (zum Beispiel das Verfälschen der Exception, siehe oben, oder im Fall, dass gar keine Datenbank zur Verfügung steht)

Alle drei genannten Punkte haben den Aufwand für die Erstellung der Testklassen erheblich erhöht. Als Verstoß gegen gutes Objektorientiertes Design ist zu werten, dass die Klasse Firma, welche ein Objekt des Domänenmodells ist, von einer Klasse festverdrahtet abhängig ist (nämlich durch Vererbung von ErweitertPersistent), die wiederum technische Details bezüglich der Persistierung enthält. Besonders auffällig im UML-Diagramm (Anhang A.1) erweisen sich die vielen Abhängigkeiten von Interfaces auf ihre implementierenden Klassen, wie bereits oben erwähnt. Diese sind im UML-Diagramm rot eingezeichnet und sind alle an Abhängigkeitszyklen beteiligt. Die Ursache wird klar, wenn man sich die Methode Geschaftspartner.erzeugeTransienteKopie() ansieht: /** * Gibt eine transiente Kopie als Geschaeftspartner-Typ zurück. * @return Die transiente Kopie. */ public Geschaeftspartner erzeugeTransienteKopieGp() { Geschaeftspartner kopie = null; // Hier sind leichte Verrenkungen noetig, da Person und Firma jeweils // gibTransienteKopie mit dem entsprechenden Rueckgabewert implementieren. // Fuer den Rechnungsempfaenger einer Belegung wird aber ein gibTransienteKopie // auf der abstrakten Oberklassen-Ebene mit Rueckgabetyp Geschaeftspartner // benoetigt. Hierbei laesst Java dann aber nicht mehr den gleichen Operations- // namen zu. if (istFirma()) { kopie = ((IFirma)this).erzeugeTransienteKopie(); } if (istPerson()) { kopie = ((IPerson)this).erzeugeTransienteKopie(); } return kopie; }

Page 57: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Weitere Testprobleme

56

Bei der genauen Betrachtung dieser Methode erzeugeTransienteKopie fallen mehrere Dinge auf:

1) Die Klasse Geschaeftspartner ist abstrakt. Das Erzeugen einer transienten Kopie einer abstrakten Klasse erscheint unsinnig, Instanz-Kopien kann man nur von anderen Instanzen erzeugen. Daher sollte diese Methode eigentlich ebenfalls abstrakt sein, und von den abgeleiteten Klassen überschrieben werden. Der Kommentar im Sourcecode deutet es an: Im Grunde sollte diese Methode abstrakt realisiert werden, aber es gab wohl Schwierigkeiten dabei.

2) Der Test dieser Methode gestaltet sich als äußerst fragwürdig: Soll für die Methode

auch eine Testmethode implementiert werden? Falls ja, dann würde diese Testmethode nur aus den zugehörigen Testmethoden der abgeleiteten Klassen aufgerufen. Doch dies bringt keinen Nutzen für den Test: Der eigentliche Code für erzeugeTransienteKopie ist ohnehin in der abgeleiteten Klasse implementiert, und von der Methode der Basisklasse wird lediglich dorthin zurück verzweigt. In der Methode der Basisklasse geschieht nichts weiter, als das Treffen der Entscheidung, von welchem Typ die abgeleitete Klasse ist. Diese Entscheidung wird aber bereits an anderer Stelle getestet, weil es als eine eigene Methode der abgeleiteten Klasse implementiert ist (istFirma() / istPerson()). Es gibt eigentlich nur einen Grund, die Testmethode dennoch zu schreiben: Diese Methode wird von außerhalb aufgerufen (aus den Klassen BelegungBearbeitenK und SeminarbelegungK) und muss daher der Vollständigkeit halber mitgetestet werden. Dies stünde jedoch im Widerspruch zu der oben getroffenen Aussage, dass für die abstrakte Klasse keine Testmethode erstellt werden soll, weil die abstrakte Klasse ohnehin nicht instantiierbar ist.

3) Ein weiteres Problem würde sich ergeben, wenn eine Testmethode

testErzeugeTransienteKopie erstellt würde: Sie müsste alle Interfaces der abgeleiteten Klassen einbinden, wegen der nach Typen getrennten Unterscheidung in der Implementierung. Die Testmethode erhält unnötigerweise einer eine hohe Zahl an festverdrahteten Abhängigkeiten, die die Testbarkeit vermindern.

4) Mit jeder weiteren Klasse, die später noch implementiert wird und die von

Geschaeftspartner erbt, müssen sowohl die Implementierung der Methode erzeugeTransienteKopie noch einmal angepasst werden, wie auch die zugehörige Testmethode. Dies erschwert die Wartbarkeit und Pflege der Testimplementierung erheblich.

Es gibt jedoch einen sehr einfachen Weg für ein Refactoring, der weniger Arbeit bedeutet, als der oben genannte Zusatzaufwand für die Erstellung der Testmethoden. Das Problem, das der Entwickler der Methode erzeugeTransienteKopieGp dokumentiert hat, lässt sich sehr einfach lösen, indem anstatt mit dem Rückgabewert zu arbeiten, einfach ein Parameter in die Methode hereingereicht wird, welchem dann die transiente Kopie zugewiesen wird. Dieser Code könnte so aussehen: In IGeschaeftspartner Änderung der Signatur von erzeugeTransienteKopieGp: public abstract void erzeugeTransienteKopieGp(IGeschaeftspartner objGP);

In Geschaeftspartner entfällt die Methode erzeugeTransienteKopieGp() völlig.

Page 58: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Weitere Testprobleme

57

Einfügung In der Klasse Firma: public void erzeugeTransienteKopieGp(IGeschaeftspartner objGP) { objGP = (IGeschaeftspartner) this.erzeugeTransienteKopie(); }

Analog kann in der Klasse Person vorgegangen werden. Wenn zusätzlich noch die Methoden erzeugeTransienteKopie der Interfaces IFirma und IPerson statt einem Klassenobjekt das zugehörige Interfaceobjekt zurückgeben, dann sind damit alle rot eingezeichneten Abhängigkeiten zwischen Geschaeftspartner, Firma, Person und den zugehörigen Interfaces entfernt. Damit wurden vier wichtige Verbesserungen erreicht:

1) Man erspart sich die oben unter Punkt 4 genannten, zusätzlichen Testmethoden, und muss nur noch die jeweiligen Methoden erzeugeTransienteKopie testen.

2) Das Design ist sauber: Die Methode erzeugeTransienteKopieGp, von der Spezifikation her abstrakt, ist nun auch wirklich abstrakt implementiert.

3) Viele Abhängigkeiten, die entgegen der Spezifikation zusätzlich in die Implementierung gelangten, und sogar einen Zyklus ausgebildet hatten, sind nun entfallen

4) Diese kleine, recht lokale Änderung hat messbare Verbesserungen der Projektmetriken zur Folge, wie die Tabelle 19 verdeutlicht.

Metrik Wert vorher Wert nachher ACD 35,8 34,9 NFD 23,0 20,0 NSBC 19,0 17,0 NCDC 39,0 33,0 NDC 10,0 09,0

Tabelle 19: Links die Projektmetriken vor, rechts nach der Refaktorisierung

Diese Werte lassen sich leicht erklären: Die eben entfernten, „roten“ Abhängigkeiten hatten dafür gesorgt, dass die sechs Klassen im weißen Rechteck des UML-Diagramms einen Abhängigkeits-Zyklus gebildet hatten. Da dieser Zyklus restlos durchbrochen worden ist, nahm die Anzahl der Klassen, die an einem Zyklus beteiligt sind (NCDC) um sechs ab (39 � 33). Die gesamte Anzahl der Zyklen (NDC) hat sich nur um eins verringert von 10 auf 9. Die feedback dependencies waren in diesem Fall die drei Abhängigkeiten von den Interfaces auf ihre implementierenden Klassen; durch deren Entfall hat sich der Wert NFD um drei verringert (von 23 auf 20).

Page 59: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

58

. / �� � - - � �0� � � ���

Abschließend werden die Ergebnisse der vorhergehenden Untersuchungen noch einmal zusammengefasst, und es werden die herausgearbeiteten Richtlinien noch einmal übersichtlich zusammengestellt, deren Einhaltung eine bessere Testbarkeit verspricht. 7.1 Die Ergebnisse der untersuchten Fragen Bei der Untersuchung von Klassen mit besonders vielen indirekten Abhängigkeiten konnte nicht festgestellt werden, dass indirekte Abhängigkeiten alleine aufgrund ihrer Anzahl oder der Tatsache, dass es indirekte Abhängigkeiten sind, besondere Testprobleme bereiten. Die Umstände, die die Testbarkeit negativ beeinträchtigen, stammten (neben allgemeinen Programmierfehlern) vorwiegend aus dem Bereich von Datenbank- und Persistenz-Verwaltungsklassen, und aus der Erbschaft von Attributen und Methoden aus Basisklassen. Die Probleme im Bereich der Datenbank- und Persistenzklassen stammten im Wesentlichen immer aus zwei Klassen (SeminarisDatenbank und ErweitertPersistent) und betrafen einen benötigten Datenbankzustand und notwendige Transaktionsprogrammierung. Im Bereich der Vererbung hat es sich häufig als schwierig herausgestellt, den Zustand einer Objektinstanz in seiner Gesamtheit zu prüfen, also einschließlich der geerbten privaten Attribute aus Basisklassen. Es konnte bestätigt werden, dass Abhängigkeiten mit besonders niedrigen DSTMh-Werten besonders häufig mit wenig oder mittlerem Aufwand refaktorisierbar sind im Gegensatz zu zufällig ausgewählten Abhängigkeiten. Im untersuchten Fallbeispiel wurden dabei aufgrund einer auffälligen Häufung sogar ganze Gruppen von Abhängigkeiten identifiziert20, deren Refaktorisierung mit vertretbarem Aufwand durchführbar war und als Ergebnis die Reduzierung der Gesamtzahl der (semi-) festverdrahteten Abhängigkeiten des Systems um gut 10% ermöglichte. 7.2 Wege zu testfreundlicher Architektur Im Folgenden werden Punkte aufgeführt, die sich im Rahmen der vorangegangenen Untersuchungen als problematisch in Bezug auf die Testbarkeit erwiesen haben, und die bereits in der Entwurfsphase, spätestens jedoch bei der Implementierung hätten erkannt und vermieden werden können. Nicht alle Punkte kann man als bereits allgemein bekannte Richtlinien für gute Qualität Objektorientierter Programmierung ansehen. 1) Abhängigkeits - Zyklen vermeiden Basisklassen sollten niemals abhängig von ihren abgeleiteten Klassen sein, so wie in Abschnitt 6.1 beschrieben. Dadurch entstehen zwangsweise Abhängigkeits-Zyklen, und dies ist nicht förderlich aus Sicht der Testbarkeit, weil die beteiligten Klassen nicht isoliert getestet

20 Die Benutzernachrichtenklassen, siehe Abschnitt 5.4.

Page 60: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

59

werden können (siehe [Lako96], Kapitel 5). In diesem speziellen Fall könnte die Basisklasse nicht separat getestet werden. Die Problematik der Abhängigkeitszyklen ist auch beispielsweise in [Jung02] schon einmal erörtert worden. 2) Validierungsmethoden für geerbte, nicht-öffentliche Attribute und Methoden Es entspricht gutem Programmierstil, Attribute immer dann nicht-öffentlich zu realisieren, wenn sie nur für die Verwaltung interner Features der jeweiligen Klasse bzw. deren abgeleiteter Klassen verwendet werden. Diese Aussage trifft natürlich auch auf get- und set-Methoden für die Attribute zu, deren Implementierung in diesem Fall unnötig ist. In einer abgeleiteten Klasse bereiten nicht-öffentliche Attribute der Basisklasse erhebliche Testprobleme, da nur über aufwändige Umwege (in diesem Fall Java Reflection) auf die entsprechenden Objekte zugegriffen werden kann21. Dieser Mechanismus schränkt die Testmöglichkeiten auch ein, z.B. werden alle Exceptions, die innerhalb einer mit der Reflection-Methode invoke aufgerufenen Methoden aus der CUT abgefangen und durch eine standardisierte Exception ersetzt. Dadurch wird die Fehleranalyse zusätzlich erschwert. Dem Verfasser dieser Arbeit erscheint es daher sinnvoll, in abgeleiteten Klassen speziell für Test- und Validierungszwecke eigene, öffentliche Methoden einzubauen, die den Zustand von geerbten Attributen prüfen können. Ein Beispiel könnte etwa so aussehen: /* Prüfung der Datenbankverbindung, nur für Test- und Validierungszwecke */ public void IsDatenbankConnectionOK() { assertTrue(objDatenbankInstanz != null); assertTrue(objDatenbankInstanz.IsConnected()); assertTrue(objDatenbankInstanz.getLastError() == NO_ERROR); }

In diesem Beispiel wird gezeigt, wie ein nicht-öffentliches Attribut, welches von einer Basisklasse geerbt wurde (objDatenbankInstanz), auf seine Korrektheit überprüft werden kann. Mit solchen Prüfmethoden würde die Testbarkeit erheblich verbessert, weil die CUT die Prüfung der geerbten nicht-öffentlichen Attribute von sich aus anbietet, und der Umweg über den Reflection-Mechanismus nicht mehr nötig ist. Der Implementierungsaufwand ist gering, und es erfolgt keine Einschränkung beim Geheimnisprinzip der internen Realisierung. Diese Idee wurde inspiriert durch die Methode

void IsValid();

im Rahmenwerk MFC (Microsoft Foundation Classes, [URL: MFC]), welche von jedem Objekt des Rahmenwerks automatisch geerbt wird und die Integrität des internen Zustands sicherstellt. Beim Compilieren einer sogenannten Debug-Version (für Testzwecke) steht diese Methode zur Verfügung, in einer Release-Version werden die Methoden automatisch entfernt.

21 Zu Beachten: Die Testklasse, die die CUT testen soll, hat keine Beziehung zur CUT.

Page 61: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

60

3) Statische Methoden und Attribute vermeiden Wenn Methoden oder Attribute nicht zwingend statisch implementiert werden müssen (und dies ist bei gutem Design fast nie der Fall), dann sollte die Implementierung auch möglichst nicht statisch gemacht werden, wie die besprochenen Beispiele gezeigt haben (die Methoden areEqual, getST, getPerson, und die Attribute protokollpuffergröße und WARNING, ERROR, etc.). Durch das Schlüsselwort static wird eine Abhängigkeit automatisch festverdrahtet, und dies wirkt sich negativ auf die Testbarkeit aus, weil dann die Austauschbarkeit der Supplier-Klasse nicht mehr möglich ist. Dasselbe gilt für .class (siehe Abschnitt 5.2): Auch dieses Konstrukt ist in der Implementierung möglichst zu vermeiden, denn es macht eine Abhängigkeit ebenfalls festverdrahtet (wegen der statischen Implementierung von .class), und ist ein Signal für problematisches Design. 4) Nachrichtenfenster vermeiden Wenn eine Klasse von sich aus potentiell Benutzernachrichten auf dem Bildschirm anzeigt, so ist dies für die Zwecke von automatisierten Tests sehr hinderlich, weil die Beantwortung von Benutzernachrichten in aller Regel auch einen Benutzer am jeweiligen Rechner erfordert. Dies trifft im Allgemeinen auf alle Aktionen zu, bei denen auf eine Reaktion oder ein Event von außen gewartet wird, bevor die zu testende Methode fortfährt. Abhilfe für die Automatisierung der Beantwortung schaffen entweder Simulations-Tools (wie der bereits erwähnte AWT-Roboter), doch sind diese teilweise aufwändig bezüglich der Einarbeitung und Wartung. Eine alternative Entwurfsvariante wäre eine Nachrichtenklasse, die entweder durch ein Stub- oder Mock-Objekt ersetzt werden kann, welches ausschließlich protokolliert, oder wenn die Original-Nachrichtenklasse eine Einstellmöglichkeit hat, um Nachrichten nicht wirklich anzuzeigen. 5) Einsatz einer class factory Klassen, die ähnliche Aufgaben haben und im System breit gestreut sind, sollten möglichst mithilfe einer class factory verwaltet werden, anstatt überall mit einem Konstruktor erzeugt zu werden. Jede Instantiierung einer Klasse über den Konstruktor oder den Pseudo-Konstruktor bringt eine festverdrahtete Abhängigkeit zu dieser Supplier-Klasse mit sich. Eine class factory kann über einen Parameter von außen in eine Client-Klasse hereingereicht werden, und ermöglicht die nicht-festverdrahtete Instantiierung von benötigten Supplier-Klassen. Die exemplarische Refaktorisierung der Nachrichtenklassen (siehe Anhang B) hat gezeigt, dass mit dieser Technik wesentliche Verbesserungen für die Testbarkeit erreicht werden können. 6) Robustheit, Stabilität, Fehlerbehandlung Häufig werden Annahmen bezüglich der Typen oder der Werte von Attributen und Parametern gemacht, bei deren Nichteinhaltung die Ausführung der Methode scheitert (und zum Beispiel mit der Erzeugung einer Exception abbricht). Wenn solche Exceptions nur an wenigen Stellen vorgesehen sind und auch abgefangen werden, wirkt sich diese mangelhafte Robustheit häufig auch durch viele Stufen eines verschachtelten Aufrufes

Page 62: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

61

hindurch aus; dies zeigte sich im Fallbeispiel insbesondere bei der Untersuchung der Auswirkungen von indirekten Abhängigkeiten. Teilweise wurden Exceptions mit aussagekräftigen Fehlerbeschreibungen behandelt, und einfach ersetzt durch kommentarlose, neue Exceptions. Nun kann man einwenden, dass es ja gerade der Sinn des Testens ist, diese Stellen zu finden. Wäre jedoch von Anfang an nach dem Test-First-Ansatz vorgegangen worden [Link02], so wären diese Probleme gleich zu Anfang entdeckt worden. Das Späte Erstellen von Testklassen ist daher tendenziell mit höherem Aufwand verbunden, falls die Fehlerbehandlung nicht fehlerfrei implementiert worden ist. Neben dem Test-First-Ansatz hätte auch die Anwendung des Prinzips Design by Contract diese Probleme vollständig vermeiden können, weil dabei jeder Parameter einer Methode und jedes verwendete Attribut zunächst auf seine Plausibilität hin überprüft wird, bevor mit der eigentlichen Ausführung der Methode begonnen wird. In Design by Contract [Meye92] ist jede Methode vergleichbar mit einer vertraglichen Vereinbarung für die Durchführung einer Dienstleistung. Für diesen Vertrag sind zunächst Voraussetzungen nötig (sinnvolle Parameter und definierte Attributzustände), und nach der Durchführung der Methode werden wieder bestimmte Zustände von Attributen und Parametern garantiert. Die Absicherung der Voraussetzungen und Abschlußgarantieen wird dabei in Codeform implementiert, zum Beispiel mit Assert-Statements. 7) Saubere Schichtentrennung (Transaktionen nur in Low-Level-Klassen) Die ursprüngliche Designentscheidung, dass alle Objekte des Domänenmodells grundsätzlich in einer Datenbank persistiert werden sollen, ist durchaus akzeptabel und in sich nicht zu bemängeln. Auch die Tatsache, dass zur Absicherung der Datenbankintegrität Transaktionen benutzt werden sollen, ist legitim und vernünftig. Zwei vermeidbare Umstände haben aber dazu geführt, dass die Transaktionsverwaltung fast in jeder Testklasse eingebaut werden musste, und dieser Overhead zum größten Problem für die Testbarkeit wurde:

1) Es ist nirgendwo ersichtlich geworden, inwieweit hier durch eine klar umrissene Schicht irgendwann eine Abstraktion von der zugrunde liegenden Datenbanktechnik erreicht wird. Üblicherweise verwendet man mehrschichtige Architekturen, um damit das Prinzip des information hiding durchzusetzen. Es mussten aber in praktisch allen CUT’s (ganz gleich ob es sich um Tests für Domänenklassen oder Kontrollklassen handelte) vom Entwickler der Testklasse gute Kenntnisse in Bezug auf das verwendete Persistenzrahmenwerk (PRW) vorausgesetzt werden. Der Tester muss dazu noch häufig den Weg von Exceptions Nachverfolgen, die aus den Klassen des PRW heraus erzeugt wurden.

2) Das verwendete PRW unterschied bei der Forderung nach Transaktionen nicht nach

simplen Lesezugriffen und anderen Zugriffen, die potentiell eine Änderung des Datenbankzustands herbeiführen könnten. Daher mussten in den Testklassen sogar für reine SELECT-Anweisungen, die lediglich Daten lesen, Transaktionsrahmen programmiert werden.

Hier hätten die Entwickler besser eine entsprechende Kapselung der Erzeugungstechnik realisiert, etwa Methoden für die transaktionsgesicherte Erzeugung von Domänenklassen. Alle Domänenklassen rufen in ihrem Pseudo-Konstruktor (erzeuge) die folgende Methode der Klasse Datenbank auf:

Page 63: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

62

public synchronized IPersistent erzeugeObjekt(IPersistent objekt) throws DatenbankAusnahme { if (gibTransaktionsMonitor().istTransaktionGestartet()) return gibBroker().erzeugeObjekt(objekt); else throw new DatenbankAusnahme("Es ist keine Transaktion gestartet."); }

Der Grund, warum für die Pseudo-Konstruktoren vorher eine Datenbanktransaktion gestartet werden muss, wird hier deutlich erkennbar (die Methode istTransaktionGestartet). Eine einfache Verbesserung könnte darin bestehen, in jedem Pseudo-Konstruktor eine Transaktion zu starten und auch zu beenden, so dass der Nutzer der Domänenklasse von diesem Mechanismus nichts mitbekommt. Der Pseudo-Konstruktor kann im Fehlerfall eine Exception erzeugen, oder einen Fehlercode zurückgeben. Eine noch bessere Lösung bestünde darin, auch die Domänenklassen von diesen datenbankspezifischen Details zu befreien, und beispielsweise innerhalb der Methode erzeugeObjekt eine Datenbanktransaktion zu starten und zu beenden, sofern noch keine Transaktion gestartet worden ist. 8) Schlüssel von Domänenklassen über Schnittstellen zugänglich machen Viele Domänenklassen wurden in der Fallbeispiel-Applikation in sogenannten Ordner-Klassen verwaltet und dort über eindeutige Schlüssel adressiert. Die Testmethoden erforderten bisweilen, dass man solche Objekte als Hilfsklassen instantiiert, und gezielt über ihren eindeutigen Schlüssel anspricht (siehe Anhang A.2). Wenn die Domänenklassen dabei keine Methodenkombination get/setSchluessel anbieten, so wird es zwingend notwendig, die jeweiligen Klassen zu analysieren, um ihnen für Testzwecke einen Schlüssel zuweisen zu können. Es kam mehrfach vor, dass als Schlüssel bestimmte interne Attribute verwendet wurden (z.B. Seminartitel für Seminar-Objekte, Firmenname konkateniert mit Kundennummer für Firmenobjekte, etc.), die aber nicht über eine entsprechende Schnittstelle (z.B. setSchluessel(String Seminartitel)) von außen belegt werden können. So wurde für viele Hilfsobjekte zunächst die Analyse der zugrunde liegenden Klasse nötig, was die Testvorbereitung (Testfixture) unnötig erschwerte. Der optimale Weg ist ohnehin die Verwendung einer eindeutigen numerischen Identifikationsnummer als Schlüssel, die identisch ist mit dem Datenbank-internen Identifikations-Key, welcher üblicherweise in Datenbanken verwaltet wird. Dies verhindert z.B. das Problem, dass ein Seminar über seinen Titel eindeutig adressiert wird wie im Fallbeispiel, und später ein anderes Seminar mit leichten Änderungen am thematischen Inhalt den gleichen Titel bekommen soll (dies wird aufgrund der vorgefundenen Schlüssel-Technik dann unmöglich). 9) Konsistente Logik einhalten Am Beispiel der Klasse RechnungsempfaengerPaar zeigte sich, dass ein einfacher Aufruf der Methode zerstoere nicht dafür ausgereicht hat, ein (Relations-) Objekt wirklich zu zerstören. Vielmehr musste vorher eine Methode loese für jedes beteiligte Objekt der Relation aufgerufen werden. Ein Außenstehender Benutzer der Klasse kann jedoch (ohne Analyse des Codes) nach Auffassung des Verfassers dieser Arbeit durchaus davon ausgehen, dass die Relationenklasse wissen muss, dass für die Selbst-Zerstörung vorher

Page 64: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

63

gegebenenfalls die beteiligten Objekte gelöst werden müssen, falls dies noch nicht geschehen ist. Des Weiteren wurden innerhalb derselben Klasse gleichnamige Methoden gefunden, die ein unterschiedliches Verhalten im Fehlerfall aufwiesen: Die eine Methode wirft eine Exception, die andere macht einfach gar nichts und gibt dem Aufrufer auch keinerlei Hinweis, ob eine erfolgreiche Ausführung stattgefunden hat oder nicht. Beide Probleme hätten schon im Entwurf vermieden werden können (durch entsprechende Vorbereitung). 10) Ungenutzen Code vermeiden Es wurden mehrere Methoden in CUT’s vorgefunden, für die Testmethoden erzeugt worden sind, die aber nicht benutzt werden und fragwürdigen oder gar funktional ganz falschen Code enthielten. Durch den Einsatz von Tools zur Code-Abdeckungsanalyse könnten solche Methoden identifiziert und von vornherein auskommentiert werden. Dies dient auch der Wartbarkeit, der Übersichtlichkeit und letztendlich der Performance (weniger Code wird schneller ausgeführt aufgrund von Speicher-Caching-Vorteilen). 7.3 Ausblick Einige Problembereiche, die im Rahmen dieser Arbeit berührt worden sind, bedürfen weiterer Untersuchung und Bewertung. Interessant wäre zum Beispiel die Frage, inwiefern der Zugriff auf statische Attribute oder Methoden von Basisklassen (aus abgeleiteten Klassen heraus) zu bewerten ist. Abgeleitete Klassen sind durch die Abhängigkeitsbeziehung automatisch festverdrahtet abhängig von ihren Basisklassen, und insoweit wäre einmal zu Klären, ob durch den Zugriff auf die genannten statischen Objekte überhaupt ein Einfluss auf die Testbarkeit bemerkbar wird. Man könnte auch untersuchen, ob sich zyklische Abhängigkeiten auf Objektebene (z.B. das Observer-Pattern22) negativ auf die Testbarkeit auswirken (was zu erwarten ist), und ob eine Refaktorisierung ohne grundlegende Architekturelle Änderungen am Gesamtsystem möglich wäre. Denkbar wäre die Prüfung, ob der Einsatz des Command-Patterns22 aus Sicht der Testbarkeit hilfreich sein kann, weil diese Änderung nur relativ lokal begrenzt wäre. Speziell für das verwendete Fallbeispiel wird für die weitere Analyse von Testbarkeitsproblemen vorgeschlagen, eine Refaktorisierung durchzuführen, die zu einer konsequenten Verwendung von IErweitertPersistent anstatt ErweitertPersistent durch die Domänenklassen führt, verbunden mit der vollständigen Kapselung von Datenbanktransaktionen. Dafür muss auch das ursprüngliche Design noch einmal auf den Prüfstand, weil die Domänenklassen von ErweitertPersistent erben. Ziel muss es sein, dass zumindest die Testklassen für Kontroll- und Schnittstellenklassen frei von den Problemen werden, die in diesem Zusammenhang genannt worden sind (siehe vorheriger Abschnitt, insbesondere Probleme Nr. 7 und 8, aber auch 6 und 9). Durch eine solche Maßnahme würde das Erstellen neuer Testklassen erheblich erleichtert und beschleunigt. Bei der Refaktorisierung sollte gleich eine neue Version des Systems erstellt werden, bei der auch andere genannte Refaktorisierungen und Korrekturen enthalten sind, zum Beispiel die konsequente Vollendung der Refaktorisierung der Nachrichtenklassen 22 vergl. [Lako96], oder [Six03] Kurseinheit 6

Page 65: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Zusammenfassung

64

(Abschnitt 5.4 bzw. Anhang B), die Entfernung der Zyklen von Geschaeftspartner (Anhang A.1), und die Beseitigung der Auffälligkeiten aus den Abschnitten 5.3 und 5.4). Nach dieser Refaktorisierung könnten weitere Testklassen für Klassen mit indirekten Abhängigkeiten erstellt werden, um die Bedeutung des Einflusses von indirekten Abhängigkeiten auf die Testbarkeit mit einer hohen Anzahl von Tests gesichert und abschließend bewerten zu können, ohne dass für jede Testklasse weiterhin erhebliche Aufwände investiert werden müssen.

Page 66: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

65

Anhang

Page 67: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

66

������������ � � �� � ��

A.1 Test der Klassen Geschaeftspartner, Firma, Person

Abbildung 7: UML - Diagramm der Klasse Geschaeftspartner (und davon abgeleitete Klassen) mitsamt Interfaces

Page 68: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

67

Für den Teil, der für den Test von Geschaeftspartner von Interesse ist (das obere, weiße Rechteck), sind vollständig alle Abhängigkeiten angegeben. Außerhalb des Rechtecks gibt es weitere Klassen, von denen die Klassen Geschaeftspartner, Firma und Person nur indirekt abhängig sind (dies gilt auch für die zugehörigen Interfaces). Die rot markierten Abhängigkeiten23 erscheinen dem Verfasser als unbeabsichtigt, weil sie im Rahmen der Spezifikation nicht nötig sind, und weil sie gegen die Grundsätze des objektorientierten Designs verstoßen. Im Folgenden wird die Testklasse TestFirma vollständig wiedergegeben. package test.SeminarisP.AnwendungslogikP.AnfragenP; import java.util.TreeSet; import java.util.Vector; import junit.framework.*; import persistenz.DatenbankAusnahme; import SeminarisP.AnwendungslogikP.AnfragenP.SpaltenModel; import SeminarisP.DatenhaltungP.AnfragenP.Anfrage; import SeminarisP.DatenhaltungP.AnfragenP.AnfrageSpalte; import SeminarisP.DatenhaltungP.AnfragenP.IAnfrage; import SeminarisP.DatenhaltungP.AnfragenP.IAnfrageSpalte; import SeminarisP.DatenhaltungP.OrdnerP.AnfrageOrdner; import SeminarisP.ExterneSystemeP.PersistenzP.ISeminarisDatenbank; import SeminarisP.ExterneSystemeP.PersistenzP.SeminarisDatenbankProvider; public class TestSpaltenModel extends TestCase { // Verweis auf die einzige Instanz der SeminarisDatenbank: ISeminarisDatenbank m_objDb; public TestSpaltenModel(String name) { super(name); m_objDb = SeminarisDatenbankProvider.getDatenbank(); assertTrue(m_objDb != null); } protected void setUp() { // Write your code here } protected void tearDown() { // Write your code here } public void testStandardKontruktor() { // Aufruf des öffentlichen Standard-Konstruktors: SpaltenModel objKandidat = new SpaltenModel(); // Sicherstellen, daß alle Attribute in initialem Zustand sind: assertTrue(objKandidat.getSpalten() != null); assertTrue(objKandidat.getRowCount() == 0); } public void testKontruktor2() { // Erzeugen eines Vectors, der zwei Objekte vom Type "AnfrageSpalte" enthält:

23 Die rote Markierung könnte durch schwarz/weiß - Drucken schlecht erkennbar sein, daher wurden die jeweiligen gestrichelten Linien besonders dick hervorgehoben.

Page 69: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

68

Vector vcAnfragen = new Vector(); IAnfrageSpalte anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); // Aufruf des öffentlichen Konstruktors, der einen Iterator übergeben bekommt, // mit dem gerade erzeugten Array: SpaltenModel objKandidat = new SpaltenModel(vcAnfragen.iterator()); // Sicherstellen, daß alle Attribute in initialem Zustand sind: assertTrue(objKandidat.getSpalten() != null); assertTrue(objKandidat.getRowCount() == 2); // Robustheitstest mit null - Parameter: SpaltenModel objKandidat2 = new SpaltenModel(null); // Robustheitstest mit typverkehrtem Iterator: Ein TreeSet wird als Basis // verwendet. TreeSet ts = new TreeSet(); ts.add("Test!"); SpaltenModel objKandidat3 = new SpaltenModel(ts.iterator()); } // Erzeugt eine Instanz der CUT, die bereits mit zwei Einträgen gefüllt ist. private SpaltenModel erzeugeGefuellteInstanz() { // Erzeugen eines Vectors, der zwei Objekte vom Type "AnfrageSpalte" enthält: Vector vcAnfragen = new Vector(); IAnfrageSpalte anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); // Erzeugen einer Instanz der CUT mit dem gerade erzeugten Array: SpaltenModel objKandidat = new SpaltenModel(vcAnfragen.iterator()); assertTrue(objKandidat.getRowCount() == 2); return objKandidat; } // Test der Methode "getColumnName" public void testGetColumnName() { SpaltenModel objKandidat = new SpaltenModel(); // Drei reguläre Fälle Testen: String strResult = objKandidat.getColumnName(0); assertTrue(strResult == "Name"); strResult = objKandidat.getColumnName(1); assertTrue(strResult == "Spalteninhalt"); strResult = objKandidat.getColumnName(2); assertTrue(strResult == "Spaltenbreite beim Ausdruck"); // Verschiedene ungültige Eingaben Testen: strResult = objKandidat.getColumnName(-1); assertTrue(strResult == "Fehler"); strResult = objKandidat.getColumnName(3); assertTrue(strResult == "Fehler"); strResult = objKandidat.getColumnName(32700); assertTrue(strResult == "Fehler"); } // Test der Methode "addSpalte" public void testAddSpalte() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2);

Page 70: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

69

// Zwei Aufrufe von "addSpalte()", einfügen vorne (0) und weit hinten (527) objKandidat.addSpalte(0); assertTrue(objKandidat.getRowCount() == 3); objKandidat.addSpalte(527); assertTrue(objKandidat.getRowCount() == 4); // Noch ein Robustheitstest: try { objKandidat.addSpalte(-32766); assertTrue(objKandidat.getRowCount() == 5); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } } // Test der Methode "RemoveSpalte" public void testRemoveSpalte() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); // Erster Robustheitstest: Aufruf von removeSpalte auf der nicht vorhandenen // Spalte Nr. 3: try { objKandidat.removeSpalte(3); assertTrue(objKandidat.getRowCount() == 1); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Zwei Aufrufe von "removeSpalte()", um die Tabelle wieder komplett zu // Entleeren objKandidat.removeSpalte(1); assertTrue(objKandidat.getRowCount() == 1); objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); // Noch ein Robustheitstest: Aufruf von removeSpalte auf der entleerten // Tabelle: try { objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } } // Test der Methode "getSpalten" public void testGetSpalten() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); Vector vcSpalten = objKandidat.getSpalten();

Page 71: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

70

assertTrue(vcSpalten != null); assertTrue(vcSpalten.size() == 2); // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat2 = new SpaltenModel(); vcSpalten = objKandidat2.getSpalten(); assertTrue(vcSpalten != null); assertTrue(vcSpalten.size() == 0); } // Test der Methode "getColumnCount" public void testGetColumnCount() { // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat = new SpaltenModel(); assertTrue(objKandidat.getColumnCount() == 3); // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); assertTrue(objKandidat2.getColumnCount() == 3); } // Test der Methode "getRowCount" public void testGetRowCount() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); // Zwei Aufrufe von "removeSpalte()", um die Tabelle wieder komplett zu // Entleeren objKandidat.removeSpalte(1); assertTrue(objKandidat.getRowCount() == 1); objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat2 = new SpaltenModel(); assertTrue(objKandidat2.getRowCount() == 0); } // Test der Methode "isCellEditable" public void testIsCellEditable() { // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat = new SpaltenModel(); assertTrue(objKandidat.isCellEditable(0, 0) == true); // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); assertTrue(objKandidat2.isCellEditable(2, 2) == true); } // Test der Methoden "getValueAt()" und "setValueAt()" public void testGetAndSetValueAt() { // Erzeugen einer leeren Instanz der CUT. SpaltenModel objKandidat = new SpaltenModel(); // Robustheitstest: abfragen und setzen von Werten einer leeren Instanz: try { objKandidat.setValueAt("Testwert-1-2-3", 0, 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); }

Page 72: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

71

try { Object objErgebnis = objKandidat.getValueAt(0, 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); // Testen der "Normalfälle": objKandidat2.setValueAt("Testwert-1-2-3", 0, 0); Object objErgebnis = objKandidat2.getValueAt(0, 0); assertTrue((String) objErgebnis == "Testwert-1-2-3"); objKandidat2.setValueAt("ZweiterWertZumTesten", 1, 1); objErgebnis = objKandidat2.getValueAt(1, 1); assertTrue((String) objErgebnis == "ZweiterWertZumTesten"); // Test mit ungültigem Spaltenindex: objErgebnis = objKandidat2.getValueAt(1, 3); assertTrue((String) objErgebnis == "Fehler"); // Test mit einem zu Setzenden Inhalt, der nicht numerisch ist: try { objKandidat2.setValueAt("Blödsinn", 1, 2); } catch(NumberFormatException ex) { System.out.println("NumberFormatException abgefangen:"); System.out.println(ex.getMessage()); } //Test mit einem Spaltenindex, der zu groß ist: try { objKandidat2.setValueAt("Blödsinn", 1, 3); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Test mit typverkehrtem ersten Parameter: try { objKandidat2.setValueAt(this, 1, 1); } catch(ClassCastException ex) { System.out.println("ClassCastException abgefangen:"); System.out.println(ex.getMessage()); } // Die nächste Zeile testet einen negativen Wert für die Spaltenbreite; // Eine MessageBox wird HINTER der Entwicklungsumgebung angezeigt! objKandidat2.setValueAt("-1", 1, 2); } }

Page 73: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

72

A.2 Test der Klasse FSVErfassenK Im Folgenden wird dokumentiert, welche Schwierigkeiten bei der Erstellung der einzelnen Testmethoden für die CUT FSVErfassenK aufgetreten sind. Konstruktor Der Konstruktor ist als protected deklariert, daher muss die Instantiierung über die statische Methode FSVErfassenK.erzeuge() erfolgen. Anschließend wird sichergestellt, dass das Ergebnis dieser Methode (eine neue Instanz von FSVErfassenK) nicht leer ist. FSVErfassenK hat kein eigenes Attribut, daher müssen nach dem Konstruktor keinerlei Zustände von Attributen abgefragt werden. Die Basisklasse FSVAendernErfassenK hat vier eigene Attribute, die mit NULL-Werten vorbelegt werden. Diese Basisklasse ist abstrakt und kann nicht für sich selbst getestet werden, daher bietet sich ein Test der korrekten Initialisierung dieser Attribute hier an. Da die Attribute aber allesamt protected sind, verbietet der Compiler den direkten Zugriff zur Prüfung auf Null. Es stehen auch keine get-Methoden zur Verfügung, daher kann keine Prüfung implementiert werden. Dasselbe gilt für SVAendernErfassenK, die Basisklasse der Basisklasse: Auch diese Klasse ist abstrakt und hat einige Attribute, die jedoch ebenfalls alle protected sind und daher die direkte Prüfung verbieten. Es wurde deshalb über die Reflection-API ein Mechanismus eingeführt, der während des Tests sicherstellen kann, dass eine Instanz der CUT sich in einem Initialzustand befindet (und zwar die Methode InitialzustandSicherstellen). Diese Maßnahme erforderte einen hohen Aufwand von mehr als 30 Minuten. Methode getObjektSV Diese Methode erzeugt lediglich eine transiente Instanz der Klasse FirmenSeminarveranstaltung, und gibt dieses Objekt als Seminarveranstaltung zurück. Um den Erfolg dieses Aufrufs sicherzustellen, wird das Ergebnis der Methode auf NULL abgefragt (also die neue Instanz). Wünschenswert wäre an dieser Stelle die Möglichkeit, den Initialzustand des neuen Seminarveranstaltung - Objekts sicherzustellen, da dieser Objektzustand von der CUT offensichtlich beabsichtigt wird. Die Klasse Seminarveranstaltung bietet keine Möglichkeiten für einen solchen Test an, und die Attribute der Klasse sind nicht öffentlich. Eine weitere Idee, nämlich die Erzeugung einer eigenen leeren Seminarveranstaltung mit anschließendem Zustandsvergleich (über isEqual) scheitert daran, dass der Konstruktor einer Seminarveranstaltung nicht öffentlich ist. Unter der Voraussetzung, die Klasse Seminarveranstaltung nicht zu verändern, bleibt als Ausweg lediglich, diesen Umstand zu kommentieren, und beim Testen die Methode toString der Seminarveranstaltung aufzurufen, um deren inneren Zustand als Ausgabe mittels println zu protokollieren. Methode mitteilungenErstellen Diese Methode bekommt keine Parameter übergeben, und wirft zwei Exceptions, daher wird für sie ein ganz simpler Aufruf in der Testmethode implementiert, abgesichert mit einem try / catch-Block. Der Aufruf der Testmethode erzeugt einen Java-Laufzeitfehler an einer Stelle in der CUT, an der ein potentielles Problem vom Entwickler der CUT nicht bedacht wurde: public SeminarisMeldung mitteilungenErstellen() throws DatenbankAusnahme, PrinterException { ... abdk = new FSVAuftragsbestaetigungDK((IFirmenSeminarveranstaltung)persistenteSV); ... }

Page 74: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

73

Man sieht, dass es um das Attribut persistenteSV geht, welches an dieser Stelle noch den Wert NULL hat, und deshalb nicht in ein Objekt vom Typ IFirmenSeminarveranstaltung umgewandelt werden kann. Offensichtlich wurde dieser Fall vom Entwickler der CUT nicht bedacht, daher wurde zunächst die Methode der CUT ergänzt um folgende Anweisung: public SeminarisMeldung mitteilungenErstellen() throws DatenbankAusnahme, PrinterException, SeminarisException { if (persistenteSV == null) { throw new SeminarisException("ungültige persistenteSV in FSVErfassenK.mitteilungenErstellen()."); } ... }

Damit ist aber lediglich ein Programmierproblem erledigt; das eigentliche Problem, nämlich die ungültige Variable persistenteSV, ist damit noch nicht gelöst. Der Code in der CUT bietet auch keinen Anhaltspunkt, wo dieses Attribut gefüllt werden könnte. Auch die Basisklasse der CUT belegt das Attribut nicht mit einem Wert. Erst bei der Analyse der nächsthöheren Stufe, bei der Basisklasse der Basisklasse, wird man fündig. Dort gibt es eine Methode speichern, und diese ist die einzige Möglichkeit, das genannte Attribut mit einem Wert zu belegen. Daher bietet es sich an, eine separate Testmethode zu schreiben, die diese Methode speichern aufruft, und dann absichert, dass „persistenteSV != null“ gilt. Dies wurde auch gemacht. Doch es zeigt sich, dass die Methode speichern aufgerufen werden kann, ohne dass eine Exception geworfen wird, und anschließend trotzdem das Attribut persistenteSV noch den NULL-Wert besitzt. Eine weitere Untersuchung der Methode ergab, dass der Rückgabewert von speichern vom Typ SeminarisMeldung ist, und man nach dem Aufruf der Methode den Inhalt dieser SeminarisMeldung für Testzwecke protokollieren kann. Die Meldung lautete: „Bitte legen Sie einen Seminartyp fest“. Bei einer genaueren Analyse der Methode speichern zeigt sich, dass das Attribut transienterSeminartyp nicht leer sein darf beim Aufruf der Methode speichern. Dieses Attribut gehört zur Klasse SVAendernErfassenK, der Basisklasse von der Basisklasse unserer CUT. Bei der Untersuchung, an welcher Stelle der Wert des Attributs transienterSeminartyp mit einem gültigen Wert belegt wird, zeigte sich, dass eine einzige Methode dies bewerkstelligt: SVAendernErfassenK.aktualisiereTransientenSeminartyp(). Der Wert für dieses Attribut wird kopiert aus dem Attribut persistenterSeminartyp, und solange dieser selbst noch null ist, braucht die Methode aktualisiereTransientenSeminartyp noch nicht aufgerufen zu werden. Also musste untersucht werden, wie das Attribut persistenterSeminartyp mit einem sinnvollen Wert belegt werden kann, und dafür gibt es zwei Methoden in SVAendernErfassenK, nämlich getObjektSTZurSV() und getObjektST(). Die erstgenannte Methode bekommt keinen Parameter; sie weist dem Attribut persistenterSeminartyp aber nur in dem Fall einen Wert zu, wenn persistenteSV nicht null ist. Das aber genau war unser Ausgangsproblem.

Page 75: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

74

Es bleibt also nur noch die Methode getObjektST(), welche einen String als Parameter erwartet. Dieser String muss ein Schlüssel zu einem gültigen Seminartyp sein, der in der Datenbank hinterlegt ist. In der Testmethode wurde getObjektST testweise einfach mit einem leeren String aufgerufen, worauf jedoch eine UngueltigerSchluesselException geworfen wurde, mit leerem Textinhalt. Dies stellt nun ein sehr gravierendes Testproblem dar: Um die Methode mitteilungenErstellen der CUT überhaupt testen zu können, muss zunächst eine Datenbank vorhanden sein, die mindestens einen gültigen Eintrag für einen Seminartyp enthält. Darüber hinaus muss der Tester den Schlüssel von mindestens einem solchen Seminartyp kennen, um ihn als Parameter anzusprechen. Dieser Schlüssel wird aber in keinem Anwendungsdialog als solcher angezeigt (Erst später, nach der Analyse der Klasse Seminartyp, wurde klar, dass der Seminartitel und der Schlüssel identische Strings sind). Der Versuch, die Applikation des Fallbeispiels aufzurufen, um einen solchen Seminartyp zu erstellen und abzuspeichern schlug fehl, weil das Programm nach der Eingabe eines Seminartyps in eine Endlosrekursion eintritt, und sich mit einer stack-overflow-Exception verabschiedet. Der Code, der dies verursacht, ist der Folgende (aus der Klasse SeminartypAendernErfassenK): public boolean istEindeutig(ISeminartyp transienterST) throws DatenbankAusnahme { return istEindeutig(null); }

Man sieht also sofort: Sobald diese Methode aufgerufen wird, ruft sie sich unendlich oft rekursiv selbst auf. Es bleibt also nur die Möglichkeit, die Testklasse um Methoden zu erweitern, die speziell für die Dauer der Tests einen Seminartyp mit einem bestimmten Schlüssel anlegen, und dieses Objekt zum Ende der Tests wieder aus der Datenbank entfernen, damit der Test unendlich oft wiederholt werden kann. Die damit verbundenen Testprobleme sind:

1) Analyse der Klassen Seminartyp und ErweitertPersistent, um die genannte Funktionalität leisten zu können

2) Integration der Pakete Seminartyp, ISeminartyp, ISeminarisDatenbank und SeminarisDatenbankProvider in die Testklasse

3) Implementierung zusätzlicher Methoden zur Erzeugung und Zerstörung des Seminartyps

Damit ist gesichert, dass das Attribut persistenterSeminartyp mit einem gültigen Wert belegt ist. Die Methode speichern funktioniert damit jedoch immer noch nicht; sie erzeugt nun beim Aufruf von SVAendernErfassenK.pruefen einen harten Java-Laufzeitfehler, denn sie geht davon aus, dass das Attribut transienteSV gültig ist, und ruft Methoden auf dem Attributobjekt auf. Diese Fehlermöglichkeit war vom Entwickler der Methode speichern nicht vorhergesehen worden, und eine Fehlerkorrektur wurde vom Verfasser dieser Arbeit in folgender Form nachträglich eingebaut:

Page 76: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

75

protected SeminarisMeldung pruefen() { if (transienteSV == null) { meldung = HinweisNachricht.erzeuge("technischer Fehler", "ungültige transienteSV in SVAendernErfassenK.pruefen()", HinweisNachricht.ERROR); } else { // „normaler“ Code, der vorher schon da war } }

Es wurde bewusst keine Exception geworfen, sondern eine Meldung erzeugt, aus mehreren Gründen: Die ganze Methode pruefen ist darauf ausgelegt, etliche verschiedene Faktoren zu Prüfen, und sobald ein Hinderungsgrund für das Speichern vorliegt, wird eine Meldung gefüllt. Im weiteren Verlauf wird stets abgefragt, ob meldung noch null ist. Außerdem warf diese Methode bisher noch keine Exceptions, und der Umbau wäre möglicherweise zu weitreichend gewesen. Eine Stelle, die das Attribut transienteSV füllt, ist schnell gefunden: die Methode getObjektSV der CUT, für die bereits eine Testmethode erstellt worden ist, leistet das Gewünschte, und wird nun vor dem Aufruf von speichern ausgeführt. Der vollständige Sourcecode der Testmethode wird im Folgenden wiedergegeben: package test.SeminarisP.AnwendungslogikP.SeminarverwaltungP.SeminarveranstaltungP; import junit.framework.*; import SeminarisP.AnwendungslogikP.SeminarverwaltungP.SeminarveranstaltungP.FSVErfassenK; import SeminarisP.DatenhaltungP.SeminarisP.ISeminartyp; import SeminarisP.DatenhaltungP.SeminarisP.Seminartyp; import SeminarisP.DatenhaltungP.SeminarisP.Seminarveranstaltung; import SeminarisP.ExterneSystemeP.PersistenzP.ISeminarisDatenbank; import SeminarisP.ExterneSystemeP.PersistenzP.SeminarisDatenbankProvider; import SeminarisP.UtilP.MeldungP.SeminarisMeldung; import SeminarisP.UtilP.ExceptionP.SeminarisException; import SeminarisP.UtilP.ExceptionP.UngueltigerSchluesselException; import persistenz.DatenbankAusnahme; import java.awt.print.*; import java.lang.reflect.*; public class TestFSVErfassenK extends TestCase { public TestFSVErfassenK(String name) { super(name); } /* Testen des Konstruktors der CUT */ public void testConstructor() { FSVErfassenK objInstanz1 = FSVErfassenK.erzeuge(); assertTrue(objInstanz1 != null); InitialzustandSicherstellen(objInstanz1); } /* Sicherstellen des Initialzustands unseres Test - Objekts: */ private void InitialzustandSicherstellen(FSVErfassenK objKandidat) { assertTrue(objKandidat != null);

Page 77: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

76

/* Die CUThat keine Attribute, also prüfen wir die Attribute der Basisklasse */ /* Die Basisklasse ist abstrakt, daher bietet sich ein Test der Attribute der Basisklasse an. */ /* Die Attribute der Basisklasse sind jedoch alle protected, und es gibt keine "get" - Funktionen, */ /* daher muss mit der Reflection-API geprüft werden. */ /* Erwartet wird: Alle Felder sind null. */ try { Field fldPersistenteFirma = objKandidat.getClass().getSuperclass().getDeclaredField( "persistenteFirma"); assertTrue(fldPersistenteFirma != null); fldPersistenteFirma.setAccessible(true); Object objValue = fldPersistenteFirma.get(objKandidat); assertTrue(objValue == null); fldPersistenteFirma = objKandidat.getClass().getSuperclass().getDeclaredField( "transienteFirma"); assertTrue(fldPersistenteFirma != null); fldPersistenteFirma.setAccessible(true); objValue = fldPersistenteFirma.get(objKandidat); assertTrue(objValue == null); fldPersistenteFirma = objKandidat.getClass().getSuperclass().getDeclaredField( "persistentePerson"); assertTrue(fldPersistenteFirma != null); fldPersistenteFirma.setAccessible(true); objValue = fldPersistenteFirma.get(objKandidat); assertTrue(objValue == null); fldPersistenteFirma = objKandidat.getClass().getSuperclass().getDeclaredField( "transientePerson"); assertTrue(fldPersistenteFirma != null); fldPersistenteFirma.setAccessible(true); objValue = fldPersistenteFirma.get(objKandidat); assertTrue(objValue == null) } catch(java.lang.NoSuchFieldException objException) { System.out.println("NoSuchField Exception in 'InitialzustandSicherstellen'"); } catch(java.lang.IllegalAccessException objException) { System.out.println("Illegal Access Exception in 'InitialzustandSicherstellen'"); } } /* Testen der Methode "getObjektSV()" */ public void testGetObjektSV() { FSVErfassenK objInstanz1 = FSVErfassenK.erzeuge(); assertTrue(objInstanz1 != null); InitialzustandSicherstellen(objInstanz1); /* Da mit der frischen Instanz noch nichts gemacht wurde, sollte die zugehörige SV noch leer sein: */ Seminarveranstaltung objSV = objInstanz1.getObjektSV(); assertTrue(objSV != null); /* Es gibt keine Methode, mit der man prüfen kann, ob eine Seminarveranstaltung im Initialzustand ist. Es wäre an dieser Stelle also sinnvoll, eine leere Seminarveranstaltung zu erzeugen, um mit "assertEqual" sicherzustellen, daß objSV ebenfalls leer ist.

Page 78: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

77

Allerdings kann man keine SV "einfach so" erstellen, es wird kein Konstruktor angeboten. */ System.out.println(objSV.toString()); } public void testSpeichern() { FSVErfassenK objInstanz1 = FSVErfassenK.erzeuge(); assertTrue(objInstanz1 != null); InitialzustandSicherstellen(objInstanz1); // Aufruf "getObjektSV", um das Attribut "transienteSV" sinnvoll zu belegen: objInstanz1.getObjektSV(); // Erzeugung eines Seminartyps; dies ist nötig, weil die Methode "speichern" // auf ihn angewiesen ist. ISeminartyp objSeminartyp = erzeugeSeminartyp(); assertTrue(objSeminartyp != null); try { objInstanz1.getObjektST(objSeminartyp.getSchluessel()); SeminarisMeldung objMeldung = objInstanz1.speichern(); if (objMeldung != null) { System.out.println("SeminarisMeldung erhalten:"); System.out.println(objMeldung.getMeldungsText()); } } catch(DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } catch(UngueltigerSchluesselException objException) { System.out.println("testSpeichern : UngueltigerSchluesselException abgefangen:"); System.out.println(objException.getNachricht()); } objSeminartyp.zerstoere(); } public void testMitteilungenErstellen() { FSVErfassenK objInstanz1 = FSVErfassenK.erzeuge(); assertTrue(objInstanz1 != null); try { SeminarisMeldung objMeldung = objInstanz1.speichern(); if (objMeldung != null) { System.out.println("SeminarisMeldung erhalten:"); System.out.println(objMeldung.getMeldungsText()); } } catch(DatenbankAusnahme objDbAusnahme) { System.out.println("testMitteilungenErstellen : DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } try {

Page 79: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

78

SeminarisMeldung objMeldung = objInstanz1.mitteilungenErstellen(); } catch(DatenbankAusnahme objDbAusnahme) { System.out.println("testMitteilungenErstellen : DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } catch(PrinterException objPrnException) { System.out.println("testMitteilungenErstellen : PrinterException abgefangen!"); } catch(SeminarisException objSemException) { System.out.println("testMitteilungenErstellen : SeminarisException abgefangen:"); System.out.println(objSemException.getNachricht()); } } private ISeminartyp erzeugeSeminartyp() { ISeminartyp objErgebnis = null; ISeminarisDatenbank objDb = SeminarisDatenbankProvider.getDatenbank(); boolean bRollbackNoetig = false; try { objDb.starteTransaktion(); try { objErgebnis = Seminartyp.erzeuge(); } catch(DatenbankAusnahme objDbAusnahme) { bRollbackNoetig = true; System.out.println("DatenbankAusnahme innen abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } if (bRollbackNoetig == true) { objDb.rollbackTransaktion(); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten, rollback nötig."); } else objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme außen abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } objErgebnis.setTitel("JUnit Test"); objErgebnis.setInhaltsueberblick("Bloß ein temporäres, virtuelles Seminar"); objErgebnis.setDauer(1); return objErgebnis; } }

Page 80: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

79

A.3 Test der Klasse RechnungsempfaengerRelation Konstruktor In der Klasse RechnungsempfaengerRelation gibt es bloß ein einziges Attribut, nämlich die systemweit einzige Instanz dieser Klasse selbst (Singleton-Pattern). Die Klasse ist abgeleitet von der Klasse ErweitertPersistentRelation, welche als Basis zur Realisierung von Relationsobjekten verwendet wird. Diese Basisklasse hat ebenfalls nur ein Attribut, nämlich einen Verweis auf die Datenbank-Instanz. Der Test des Konstruktors der CUT gestaltet sich relativ einfach, da eine statische Methode (getEinzigeInstanz) der CUT aufgerufen werden muss. Anschließend wird die Korrektheit des einzigen Attributs der CUT getestet, was ebenfalls sehr einfach gelingt. Der Test des einzigen Attributs der Basisklasse gestaltet sich schwieriger, weil die Basisklasse sowohl das Attribut wie auch die zugehörige get-Methode nicht öffentlich realisiert. Wie schon in A.2 gezeigt, wird auch hier über den Umweg der Java Reflection das gewünschte Ziel erreicht. Der Aufwand hierfür ist als „mittel“ einzustufen. getRechnungsempfaengerPaar Die Methode getRechnungsempfaengerPaar existiert in der CUT in unterschiedlichen polymorphen Ausprägungen, und zuerst ist die Variante ohne Parameter implementiert. Die Methode ist private, also ausschließlich zur Verwendung innerhalb der CUT vorgesehen. Die Suche innerhalb der CUT, wo diese Ausprägung der Methode denn verwendet wird, ergibt schnell, dass sie gar nicht benötigt wird. Daher wird diese erste Ausprägung der Methode auskommentiert, mit einem entsprechenden Kommentar im Sourcecode. Dies trägt zur Reduzierung unnötiger Komplexität bei. Die nächste Ausprägung der Methode hat einen Parameter vom Typ IBelegung, ist ebenfalls private deklariert, und wird auch verwendet. Obwohl die Methode private ist, und eigentlich vom black-box-Ansatz ausgegangen wird, soll die Methode getestet werden, aus zwei Gründen: Zum Ersten reduziert dieses Vorgehen den Aufwand des funktionalen Tests der Aufrufer-Methode, welche ja ebenfalls in der CUT enthalten ist und getestet werden muss. Zum Zweiten ergibt sich eine bessere Struktur und Lesbarkeit der Testklasse, wenn eine direkte Zuordnung einer Testmethode zu einer MUT (method under test) besteht. Das Problem, dass die MUT nicht-öffentlich ist, muss wieder mittels der Java Reflection gelöst werden (mit mittlerem Aufwand). Zunächst benötigt man aber ein Objekt vom Typ IBelegung, welches der Methode als Parameter übergeben wird. Ohne großen Analyseaufwand wurde einfach die statische Methode Belegung.erzeuge verwendet, welche eine leere DatenbankAusnahme erzeugt. Die erzeuge - Methode musste daher weiter analysiert werden, und es zeigte sich, dass auch hier wieder der gleiche Programmierfehler gemacht worden ist, wie schon in der Implementierung der Klasse Firma (siehe Abschnitt A.1): In der Methode Belegung.erzeuge wird ein Exception-Handling in der Weise realisiert, dass als Reaktion auf jegliche aufgetretene Ausnahme einfach eine neue, leere Ausnahme vom Typ DatenbankAusnahme wirft. Besser wäre es natürlich, die DatenbankAusnahme aus der aufgerufenen Methode, die einen sinnvollen Fehlertext enthält, mit dieser Information auch weiter zu werfen. Nach der entsprechenden Änderung des Exception-Handlings zeigt sich dann auch anhand des Ausnahme - Texts, dass einfach nur versäumt wurde, eine Transaktion zu starten. Der zusätzliche Analyse-Aufwand hierfür ist als mittel einzustufen.

Page 81: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

80

Da im Hinblick auf weitere Testmethoden die ordentliche Erstellung sowie das anschließende Zerstören eines Belegung-Objekts in der Datenbank als sinnvoll erscheint, wird diese Verwaltung eines Testklassen-globalen Belegung-Objekts in die Setup / Fixture-Funktionalität der Testklasse eingebaut. Dabei muss das Belegung-Objekt genau analysiert werden, damit man weiß, wie ein eindeutiger Schlüssel für das Objekt generiert werden kann. Die Analyse der Klasse ergibt, dass der Schlüssel aus der Rechnungsnummer und der Auftragsnummer generiert wird. Nun steht ein Belegung-Objekt zur Verfügung, und der erste Test der Methode wird durchgeführt. Beim Aufruf der MUT mittels invoke (aus der Java Reflection API) kommt eine InvocationTargetException, eine Exception, die dadurch ausgelöst wurde, weil innerhalb der MUT eine Exception geworfen wurde. Leider wird dabei die Original-Exception, die in der MUT auch zusätzlich kommentiert wird, nicht durch invoke weitergegeben, so dass die Methode invoke die Fehleranalyse erschwert (hoher Aufwand bei der Testdurchführung). Eine Untersuchung der Kommentierung der MUT zeigt aber eine Precondition, nämlich dass vorher eine Datenbanktransaktion gestartet sein muss, und dies wird nachträglich noch in die Testmethode eingebaut (mittlerer Aufwand für die Testfallerstellung). Dabei wird eine weitere Schwäche der Implementierung offenbar: Hier muss sich der Aufrufer einer High-Level-Methode, die eigentlich für den Aufrufer nichts mit Datenbanken zu tun hat, selbst um die Transaktionssteuerung der Datenbank kümmern. Darüber hinaus stellt sich die Frage nach dem Sinn der Transaktion: Eine Transaktion wird verwendet, um nach einer Folge von Datenbankoperationen, von denen mindestens eine den Zustand der Datenbank verändern kann, die Datenbank wieder gesichert in einen konsistenten Zustand zu führen. In diesem Fall jedoch wollen wir einfach nur lesend zugreifen, technisch handelt es sich um ein einfaches SELECT-Statement. Warum dafür eine Transaktion notwendig sein soll, erschließt sich dem Verfasser dieser Arbeit nicht (es wurde aber auch nicht weiter in den entsprechenden Klassen analysiert). Weil fast jede der nachfolgenden Methoden der CUT eine der Ausprägungen von getRechnungsempfaengerPaar intern aufruft, benötigt auch jede Testmethode einen gewissen Overhead von etwa 30 Codezeilen, mit denen eine Transaktion gestartet und kontrolliert beendet wird, mitsamt assert - Absicherungen und Fehlerbehandlung. Dies erschwert sowohl die Testerstellung wie auch die Lesbarkeit der Testmethoden mit mittlerem Aufwand. Die MUT (getRechnungsempfaengerPaar) läuft nun fehlerfrei durch, jedoch ist der Returnwert ein leerer Iterator. Es muss also eine Möglichkeit geschaffen werden, dass ein Objekt vom Typ RechnungsempfaengerPaar während des Tests gezielt in der Datenbank platziert wird, und nach dem Test wieder entfernt wird, um einen nicht-leeren Returnwert im Test zu erhalten. Dies bedingt zuerst, dass auch der zweite Bestandteil dieses Paar-Objekts, also ein Objekt vom Typ Geschaeftspartner, im Testsetup instantiiert wird. Geschaeftspartner ist jedoch eine abstrakte Klasse, so dass die Wahl auf die abgeleitete Klasse Firma fällt, welche hier Verwendung findet. Zur Erstellung eines eindeutigen Schlüssels müssen auch hier die Klassen Firma und die Basisklasse Geschaeftspartner analysiert werden. Bei der Einbindung eines Objekts vom Typ RechnungsempfaengerPaar in das Testfixture fällt erneut die mangelhafte Fehlerbehandlung in der Methode erzeuge() auf, die sogleich verbessert wird.

Page 82: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

81

Es zeigte sich ein weiteres Problem: In das Testfixture, welches vor jeder Testmethode ausgeführt wird, wurde neben dem Aufbau eines Belegung-Objekts auch der Aufbau eines Geschaeftspartner-Objekts und eines RechnungsempfaengerPaar-Objekts eingebaut. In der Methode tearDown, die nach jeder Durchführung einer Testmethode aufgerufen wird, wird die Methode zerstoere für alle drei Objekte aufgerufen, um das zugehörige Datenbank-Objekt wieder zu vernichten. Es stellte sich heraus, dass die Abfolge Testfixture / Testmethode / Teardown bei jeder Testsuite genau einmal erfolgreich durchlief, aber ab dem zweiten Aufruf des Testfixtures in derselben Suite gab es einen Fehler. Bei der Erzeugung der Belegung kommt eine Datenbankexception „Keine Transaktion vorhanden!“, obwohl unmittelbar vorher eine Transaktion gestartet wurde. Es gab auch vorher keine erkennbare Fehlermeldung, einen Rückgabecode oder eine Exception, die irgendwelche Schlüsse auf einen Fehler zuließe. Es hat sich dann später gezeigt, dass zusätzliche Aufrufe nötig sind: Zuerst müssen die an der Assoziation beteiligten Objekte mit loese von der Assoziationsklasse gelöst werden, und erst dann kann zerstoere auf allen drei Objekten fehlerfrei ausgeführt werden. Ohne Analyse der Assoziationsklasse ist der Verfasser davon ausgegangen, dass die Methode zerstoere implizit auch ein Lösen der Verbindungen durchführt, zumindest jedoch Exceptions erzeugt, wenn dies nicht explizit gemacht wurde. Dies hat sich jedoch nicht bewahrheitet, und es ist im Sourcecode auch nicht entsprechend kommentiert. Nun war das Ziel erreicht, dass die MUT fehlerfrei aufgerufen werden konnte, und das Ergebnis der Methode nicht leer ist. Abzusichern ist noch die Annahme, dass der zurückgegebene Iterator genau ein Element enthält, und dieses Element ein RechnungsempfaengerPaar ist, welches als Einzelbestandteile wieder genau die Belegung und den Geschaeftspartner enthält, die im Testsetup erzeugt werden. Es ist in der Tat genau ein Element im Iterator, doch es zeigte sich, dass die Methode Belegung.equals, welche zum Vergleichen der Belegungsinstanzen verwendet wird, eine leere Exception wirft. Eine detaillierte Analyse der Klasse Belegung ergab daraufhin, dass es Attribute vom Typ Date in der Klasse Belegung gibt, welche beim Initialisieren mit NULL vorbelegt werden, und im Testfixture nicht explizit mit einem Wert belegt wurden. In der Methode Belegung.equals wird aber ein Aufruf der Art getAnmeldedatum().equals gemacht, welcher natürlich voraussetzt, dass das Anmeldetatum nicht null ist. Dasselbe gilt analog für das Attribut Stornodatum. In der Methode Belegung.equals wurde also eindeutig ein Fehler gefunden, der beseitigt werden sollte. Da nirgendwo explizit steht, dass eine Belegung-Instanz diese Date-Attribute mit gültigem Inhalt besitzen muss, um existenzberechtigt zu sein, wurde der relativ einfache Weg gewählt, bei dem eine separate Abfrage sicherstellt, dass im Fall der wechselseitigen NULL-Wertigkeit des Attributs auch die Methode equals() zu einem TRUE-Ergebnis kommt. Wichtig ist vor allem, dass keine unbekannte Exception mehr ausgelöst wird. Nachdem nun sichergestellt ist, dass der Aufruf von getRechnungsempfaengerPaar genau einen Treffer zurückliefert, und die darin enthaltenen Elemente genau die erwarteten Elemente sind, wird anschließend noch sichergestellt, dass der Aufruf mit einem Belegung-Objekt, welches einen völlig unsinnigen Schlüssel hat, einen leeren Iterator ergibt.

Page 83: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

82

Des Weiteren wurde ein Robustheitstest durchgeführt, indem als Belegung-Parameter ein NULL-Objekt übergeben wurde. Es erfolgte eine nicht behandelte Nullpointer-Exception. Die genaue Analyse der CUT ergab daraufhin, dass die Parameter Belegung und Geschaeftspartner, die in nahezu jeder Methode vorkommen, mit Ausnahme von verbinde nirgendwo gegen Nullpointer-Exceptions abgesichert sind, das heißt es wird stets darauf vertraut, dass alle Parameter gültig sind. Die CUT wurde entsprechend überarbeitet, so dass jetzt konsequent entsprechende UngueltigerSchluesselException-Ausnahmen erzeugt werden, falls ein solcher Schlüssel ungültig ist. Daraufhin gelang auch der Robustheitstest wie erwartet. Analog wurden auch für die MUT-Ausprägungen mit Parameter IGeschaeftspartner und IBelegung, IGeschaeftspartner die zugehörigen Testmethoden erstellt. getGeschaeftspartner Die MUT getGeschaeftspartner ist öffentlich (und daher ohne Reflection zugänglich), und erfordert ebenfalls den vorherigen Start einer Transaktion, weil die private (und oben bereits diskutierte) Methode getRechnungsempfaengerPaar verwendet wird. getBelegungen Der Test der Methode getBelegungen() verläuft ohne besondere Auffälligkeiten. existiertGeschaeftspartner Die Methode existiertGeschaeftspartner enthält einen ganz erheblichen Fehler, der beim Testen sofort entdeckt wird: Sie dient zur Entscheidung, ob zu einer gegebenen Belegung ein Geschaeftspartner existiert, und soll einen entsprechenden boolean-Wert zurückgeben. Der Sourcecode dieser Methode sieht folgendermaßen aus: public boolean existiertGeschaeftspartner(IBelegung b) throws AssoziationsException, DatenbankAusnahme, UngueltigerSchluesselException { boolean ergebnis; Iterator iter = getRechnungsempfaengerPaar(b); ergebnis = iter.hasNext(); // Ueberpruefen, ob Datenbank noch konsistent. if (iter.hasNext()) { while (iter.hasNext()) { // Iterator fuer die Datenbank unbedingt abarbeiten (auch wenn er keine Elemente mehr enthalten sollte!). iter.next(); } throw new AssoziationsException("Der Belegung " + b.getSchluessel() + " ist mehr als ein Rechnungsempfänger zugeordnet."); } return ergebnis; }

Man sieht, dass die Methode für den Fall „Kein Geschäftspartner gefunden“ korrekt implementiert ist. Im Fall, dass mindestens ein Geschäftspartner gefunden wurde, wollte der Entwickler der MUT offensichtlich eine zusätzliche Absicherung einbauen, um Sicherzustellen, dass zu der gegebenen Belegung maximal ein passender Geschaeftspartner existiert. Dabei hat er aber versehentlich dafür gesorgt, dass immer, wenn mindestens ein passender Geschäftspartner gefunden worden ist, die AssoziationsException geworfen wird, also auch in den gutartigen Fällen mit genau einem Treffer. Das heißt in der Konsequenz, dass diese Methode völlig unbrauchbar ist.

Page 84: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

83

Diese Beobachtung brachte den Verfasser dieser Arbeit dazu, systemweit nach Verwendungen dieser Methode zu suchen, und es wurde festgestellt, dass die Methode nirgendwo aufgerufen wird. Daher wurde die Methode entsprechend aus der CUT auskommentiert, zusammen mit einem zusätzlichen Erläuterungs-Kommentar. Die zugehörige Testmethode, die bereits fertig erstellt war, wurde komplett gelöscht. Der Erstellungsaufwand war natürlich trotzdem bereits erbracht (ein mittlerer Aufwand bei der Testfallerstellung). existiertBelegung Die Methode existiertBelegung wird tatsächlich verwendet, und zwar von der Klasse FirmaLoeschenK. Interessanterweise ist sie analog zu der oben gezeigten Methode existiertGeschaeftspartner implementiert, jedoch fehlt die Zeile mit der Erzeugung einer Exception. Eine Absicherung der Anzahl der Treffer ist hier aber auch fehl am Platze, weil ein Geschäftspartner durchaus mehrere Belegungen gebucht haben kann. Der Test der Methode verlief problemlos. existiertPaar Die Methode existiertPaar hatte wieder den gleichen Fehler wie die Methode existiertGeschaeftspartner (s.o.), auch hier wird immer eine Exception geworfen nach dem Finden mindestens eines Paares, doch auch diese Methode wird systemweit nicht verwendet und wurde entsprechend auskommentiert. loese Als nächstes stand der Test der beiden Ausprägungen der Methode loese an. Die erste Ausprägung, bei der ein Parameter vom Typ IBelegung übergeben wird, verlief ohne besondere Vorkommnisse und gemäß den Erwartungen. Beim Test der zweiten Ausprägung, bei der ein Parameter vom Typ IBelegung sowie einer vom Typ IGeschaeftspartner übergeben werden, wurde jedoch überraschenderweise eine Inkonsistenz festgestellt. Diese zweite Ausprägung gibt überhaupt keinen Hinweis darauf, ob die Methode loese funktioniert hat, oder nicht. Anders ausgedrückt: Man kann der Methode als Parameter beliebige Objekte übergeben (die natürlich typgerecht sein müssen), und es wird nie eine Exception geworfen, außer wenn ein Parameter NULL ist. Da beide Ausprägungen einen VOID-Returnwert haben, ist auch über diesen Weg keine Kontrolle möglich. Man kann für das Verhalten der zweiten Methode auch ein gewisses Verständnis entwickeln, es steht jedoch im Gegensatz zum Verhalten der ersten Ausprägung, bei der genau dann eine Exception erzeugt wird, wenn keine Verbindung gelöst werden konnte. Dieses Verhalten ist inkonsequent. verbinde Die Tests für die letzte Methode, verbinde, verliefen erwartungsgemäß und ohne besondere Auffälligkeiten. Im Folgenden wird der Sourcecode der Testklasse vollständig wiedergegeben: package test.SeminarisP.DatenhaltungP.TransformationenP; import junit.framework.*; import java.lang.reflect.*; import java.util.*; import persistenz.DatenbankAusnahme; import SeminarisP.DatenhaltungP.SeminarisP.Belegung; import SeminarisP.DatenhaltungP.SeminarisP.Firma; import SeminarisP.DatenhaltungP.SeminarisP.IBelegung; import SeminarisP.DatenhaltungP.SeminarisP.IFirma; import SeminarisP.DatenhaltungP.SeminarisP.IGeschaeftspartner;

Page 85: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

84

import SeminarisP.DatenhaltungP.TransformationenP.RechnungsempfaengerRelation; import SeminarisP.ExterneSystemeP.PersistenzP.ISeminarisDatenbank; import SeminarisP.ExterneSystemeP.PersistenzP.SeminarisDatenbankProvider; import SeminarisP.DatenhaltungP.TransformationenP.IRechnungsempfaengerPaar; import SeminarisP.DatenhaltungP.TransformationenP.RechnungsempfaengerPaar; import SeminarisP.UtilP.ExceptionP.AssoziationsException; import SeminarisP.UtilP.ExceptionP.UngueltigerSchluesselException; /** JUnit TestCase. * @testfamily JUnit * @testkind testcase * @testsetup Empty TestCase * @testedclass SeminarisP.DatenhaltungP.TransformationenP.RechnungsempfaengerRelation*/ public class TestRechnungsempfaengerRelation extends TestCase { // globale Objekte, die in setUp() und tearDown() verwaltet werden. // Der Anhang "Rel" heißt, daß das Objekt in eine Relation eingegliedert wird ISeminarisDatenbank m_objDb; IBelegung m_objBelegung_Rel, m_objBelegung; IGeschaeftspartner m_objGP, m_objGP_Rel; IRechnungsempfaengerPaar m_objRePaar; /** Constructs a test case with the given name. */ public TestRechnungsempfaengerRelation(String name) { super(name); } /** Sets up the fixture, for example, open a network connection. This method is called before a test is executed. */ protected void setUp() { m_objDb = SeminarisDatenbankProvider.getDatenbank(); assertTrue(m_objDb != null); // Erzeugen einer Belegung: createBelegungen(); // Erzeugen eines Geschaeftspartners createGeschaeftspartner(); // Erzeugen eines Rechnungsempfaenger - Paares createRechnungsempfaengerPaar(); } /** Tears down the fixture, for example, close a network connection. This method is called after a test is executed. */ protected void tearDown() { m_objRePaar.loeseBelegung(); m_objRePaar.loeseGeschaeftspartner(); m_objBelegung_Rel.zerstoere(); m_objBelegung.zerstoere(); m_objGP.zerstoere(); m_objRePaar.zerstoere(); m_objRePaar = null; m_objBelegung = null; m_objGP = null; } /* Hilfsmethoden für das TestFixture : */ private void createBelegungen() { try {

Page 86: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

85

boolean bRollbackNoetig = false; m_objDb.starteTransaktion(); try { m_objBelegung_Rel = Belegung.erzeuge(); assertTrue(m_objBelegung_Rel != null); m_objBelegung_Rel.setRechnungsnummer("A-M-I.DE 007/0815"); m_objBelegung_Rel.setAuftragsnummer("A24102003-001"); m_objBelegung = Belegung.erzeuge(); assertTrue(m_objBelegung != null); m_objBelegung.setRechnungsnummer("völligUnsinnig_völligUnsinnig"); m_objBelegung.setAuftragsnummer( "abolutÜberflüssig_absolutÜberflüssig"); } catch(DatenbankAusnahme objDbAusnahme) { bRollbackNoetig = true; System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("DatenbankAusnahme aufgetreten, rollback nötig."); } else { m_objDb.commitTransaktion(); } } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } } private void createGeschaeftspartner() { try { boolean bRollbackNoetig = false; m_objDb.starteTransaktion(); try { // Geschaeftspartner erzeugen, der auch in einer Relation verwendet wird: IFirma objFirma = Firma.erzeuge(); assertTrue(objFirma != null); objFirma.setName("A-M-I.DE"); objFirma.setKundennummer(17769); m_objGP_Rel = (IGeschaeftspartner) objFirma; assertTrue(m_objGP_Rel != null); // unsinnigen Geschaeftspartner erzeugen: objFirma = Firma.erzeuge(); assertTrue(objFirma != null); objFirma.setName("unsinnigerName_unsinnig"); objFirma.setKundennummer(0); m_objGP = (IGeschaeftspartner) objFirma; assertTrue(m_objGP != null); } catch(DatenbankAusnahme objDbAusnahme) {

Page 87: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

86

bRollbackNoetig = true; System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } } private void createRechnungsempfaengerPaar() { try { boolean bRollbackNoetig = false; m_objDb.starteTransaktion(); try { assertTrue(m_objBelegung_Rel != null); assertTrue(m_objGP_Rel != null); m_objRePaar = RechnungsempfaengerPaar.erzeuge(m_objBelegung_Rel, m_objGP_Rel); } catch(DatenbankAusnahme objDbAusnahme) { bRollbackNoetig = true; System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); } if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("createRechnungsempfaengerPaar: DatenbankAusnahme aufgetreten."); } assertTrue(m_objRePaar != null); } /* Ende der Hilfsmethoden */ public void testConstructor() { /* Der Konstruktor ist protected, also nicht zu Prüfen. Außerdem wird er zum Programmstart aufgerufen, weil das einzige member der CUTeine private static - Instanz der CUT selbst ist. Wir testen also bloß noch, ob die Instanz nicht null ist: */ RechnungsempfaengerRelation objKandidat =

Page 88: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

87

RechnungsempfaengerRelation.getEinzigeInstanz(); assertTrue(objKandidat != null); /* Außerdem stellen wir noch sicher, daß das einzige Attribut der Basisklasse, db, gültig ist. Da jedoch alle Zugriffsmöglichkeiten protected sind, muss die Reflection – API benutzt werden : */ try { Field fldDb = objKandidat.getClass().getSuperclass().getDeclaredField("db"); assertTrue(fldDb != null); fldDb.setAccessible(true); Object objValue = fldDb.get(objKandidat); assertTrue(objValue != null); } catch(NoSuchFieldException objException) { System.out.println("NoSuchField Exception in 'testConstructor'"); fail("'testConstructor'"); } catch(IllegalAccessException objException) { System.out.println("Illegal Access Exception in 'testConstructor'"); fail("'testConstructor'"); } } public void testGetRechnungsempfaengerPaar_BelegungParam() { /* Test der Methode "getRechnungsempfaengerPaar()", in der Implementierung mit "IBelegung" - Parameter. */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { Class[] args = { IBelegung.class }; Method mthGetRePa = objKandidat.getClass().getDeclaredMethod("getRechnungsempfaengerPaar", args ); assertTrue(mthGetRePa != null); mthGetRePa.setAccessible(true); // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } assertTrue(m_objBelegung != null); Iterator itResult = (Iterator) mthGetRePa.invoke( objKandidat, new Object [] { m_objBelegung_Rel } ); assertTrue(itResult != null); assertTrue(itResult.hasNext()); /* Den Iterator bis zum Ende Laufen lassen: */ while (itResult.hasNext()) { IRechnungsempfaengerPaar objRE = (IRechnungsempfaengerPaar) itResult.next(); assertTrue(objRE != null);

Page 89: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

88

// Extraktion des Belegung - Objekts, und Vergleich mit dem Ausgangs – // Objekt IBelegung objLokalBelegung = objRE.getBelegung(); assertTrue(objLokalBelegung != null); assertTrue(objLokalBelegung.equals(m_objBelegung_Rel)); // Extraktion des Geschaeftspartner - Objekts, und Vergleich mit dem // Ausgangs - Objekt IGeschaeftspartner objLokalGP = objRE.getGeschaeftspartner(); assertTrue(objLokalGP != null); assertTrue(objLokalGP.equals(m_objGP_Rel)); // Es darf nur ein Eintrag für die Relation gefunden werden: assertTrue(!itResult.hasNext()); } /* Als zweites soll sichergestellt werden, daß eine völlig unsinnige Belegung die an keiner Relation beteiligt ist, auch nicht gefunden wird. */ Iterator itResult2 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { m_objBelegung } ); assertTrue(itResult2 != null); assertTrue(!itResult2.hasNext()); // Dritter Test: Robustheitstest mit null - Parameter try { Iterator itResult3 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { null } ); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(InvocationTargetException expected) {} } catch(IllegalAccessException objException) { System.out.println("Illegal Access Exception in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(NoSuchMethodException objException) { System.out.println("NoSuchMethodException in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(InvocationTargetException objException) { System.out.println("InvocationTargetException in 'testGetRechnungsempfaengerPaar'"); System.out.println(objException.getMessage()); bRollbackNoetig = true; } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testGetRechnungsempfaengerPaar: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion();

Page 90: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

89

} catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } } public void testGetRechnungsempfaengerPaar_GpParam() { /* Test der Methode "getRechnungsempfaengerPaar()", in der Implementierung mit "IGeschaeftspartner" - Parameter. */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { Class[] args = { IGeschaeftspartner.class }; Method mthGetRePa = objKandidat.getClass().getDeclaredMethod( "getRechnungsempfaengerPaar", args ); assertTrue(mthGetRePa != null); mthGetRePa.setAccessible(true); // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } assertTrue(m_objBelegung != null); Iterator itResult = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { m_objGP_Rel } ); assertTrue(itResult != null); assertTrue(itResult.hasNext()); /* Den Iterator bis zum Ende Laufen lassen: */ while (itResult.hasNext()) { IRechnungsempfaengerPaar objRE = (IRechnungsempfaengerPaar) itResult.next(); assertTrue(objRE != null); // Extraktion des Belegung - Objekts, und Vergleich mit dem Ausgangs – // Objekt IBelegung objLokalBelegung = objRE.getBelegung(); assertTrue(objLokalBelegung != null); assertTrue(objLokalBelegung.equals(m_objBelegung_Rel)); // Extraktion des Geschaeftspartner - Objekts, und Vergleich mit dem // Ausgangs - Objekt IGeschaeftspartner objLokalGP = objRE.getGeschaeftspartner(); assertTrue(objLokalGP != null); assertTrue(objLokalGP.equals(m_objGP_Rel)); // Es darf nur ein Eintrag für die Relation gefunden werden: assertTrue(!itResult.hasNext()); } /* Als zweites soll sichergestellt werden, daß ein völlig unsinniger Geschaeftspartner,

Page 91: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

90

der an keiner Relation beteiligt ist, auch nicht gefunden wird. */ Iterator itResult2 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { m_objGP } ); assertTrue(itResult2 != null); assertTrue(!itResult2.hasNext()); // Dritter Test: Robustheitstest mit null - Parameter try { Iterator itResult3 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { null } ); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(InvocationTargetException expected) {} } catch(IllegalAccessException objException) { System.out.println("Illegal Access Exception in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(NoSuchMethodException objException) { System.out.println("NoSuchMethodException in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(InvocationTargetException objException) { System.out.println("InvocationTargetException in 'testGetRechnungsempfaengerPaar'"); System.out.println(objException.getMessage()); bRollbackNoetig = true; } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testGetRechnungsempfaengerPaar: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } } public void testGetRechnungsempfaengerPaar_BeideParam() { /* Test der Methode "getRechnungsempfaengerPaar()", in der Implementierung mit "IBelegung" und "IGeschaeftspartner" – Parameter. */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false;

Page 92: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

91

try { Class[] args = { IBelegung.class, IGeschaeftspartner.class }; Method mthGetRePa = objKandidat.getClass().getDeclaredMethod( "getRechnungsempfaengerPaar", args ); assertTrue(mthGetRePa != null); mthGetRePa.setAccessible(true); // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } assertTrue(m_objBelegung != null); Iterator itResult = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { m_objBelegung_Rel, m_objGP_Rel } ); assertTrue(itResult != null); assertTrue(itResult.hasNext()); /* Den Iterator bis zum Ende Laufen lassen: */ while (itResult.hasNext()) { IRechnungsempfaengerPaar objRE = (IRechnungsempfaengerPaar) itResult.next(); assertTrue(objRE != null); // Extraktion des Belegung - Objekts, und Vergleich mit dem Ausgangs – // Objekt IBelegung objLokalBelegung = objRE.getBelegung(); assertTrue(objLokalBelegung != null); assertTrue(objLokalBelegung.equals(m_objBelegung_Rel)); // Extraktion des Geschaeftspartner - Objekts, und Vergleich mit dem // Ausgangs - Objekt IGeschaeftspartner objLokalGP = objRE.getGeschaeftspartner(); assertTrue(objLokalGP != null); assertTrue(objLokalGP.equals(m_objGP_Rel)); // Es darf nur ein Eintrag für die Relation gefunden werden: assertTrue(!itResult.hasNext()); } // Dritter Test: Robustheitstest mit null - Parametern, drei // Kombinationsmöglichkeiten testen try { Iterator itResult2 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { m_objBelegung_Rel, null } ); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(InvocationTargetException expected) {} try { Iterator itResult3 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { null, m_objGP_Rel } ); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(InvocationTargetException expected) {} try

Page 93: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

92

{ Iterator itResult3 = (Iterator) mthGetRePa.invoke(objKandidat, new Object [] { null, null } ); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(InvocationTargetException expected) {} } catch(IllegalAccessException objException) { System.out.println("Illegal Access Exception in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(NoSuchMethodException objException) { System.out.println("NoSuchMethodException in 'testGetRechnungsempfaengerPaar'"); bRollbackNoetig = true; } catch(InvocationTargetException objException) { System.out.println("InvocationTargetException in 'testGetRechnungsempfaengerPaar'"); System.out.println(objException.getMessage()); bRollbackNoetig = true; } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testGetRechnungsempfaengerPaar: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Erzeugen: DatenbankAusnahme aufgetreten."); } } public void testGetGeschaeftspartner() { /* Test der Methode "getGeschaeftspartner()" */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) {

Page 94: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

93

System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Erster Test: Erwartet wird ein Treffer IGeschaeftspartner lokalGP = objKandidat.getGeschaeftspartner(m_objBelegung_Rel); assertTrue(lokalGP != null); assertTrue(lokalGP.equals(m_objGP_Rel)); // Zweiter Test: Erwartet wird 0 Treffer IGeschaeftspartner lokalGP2 = objKandidat.getGeschaeftspartner(m_objBelegung); assertTrue(lokalGP2 == null); // Dritter Test: Robustheitstest mit null - Parameter try { IGeschaeftspartner lokalGP3 = objKandidat.getGeschaeftspartner(null); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testGetGeschaeftspartner: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testGetGeschaeftspartner: DatenbankAusnahme aufgetreten."); } } public void testGetBelegungen() { /* Test der Methode "getBelegungen()" */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme);

Page 95: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

94

fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Erster Test: Erwartet wird ein Treffer Iterator itResult = objKandidat.getBelegungen(m_objGP_Rel); assertTrue(itResult != null); assertTrue(itResult.hasNext()); /* Den Iterator bis zum Ende Laufen lassen: */ while (itResult.hasNext()) { IBelegung objBelegung = (IBelegung) itResult.next(); assertTrue(objBelegung != null); assertTrue(objBelegung.equals(m_objBelegung_Rel)); // Es darf nur ein Eintrag für die Relation gefunden werden: assertTrue(!itResult.hasNext()); } // Zweiter Test: Erwartet wird 0 Treffer Iterator itResult2 = objKandidat.getBelegungen(m_objGP); assertTrue(itResult2 != null); assertTrue(!itResult2.hasNext()); // Dritter Test: Robustheitstest mit null - Parameter try { Iterator itResult3 = objKandidat.getBelegungen(null); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testGetBelegungen: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testGetBelegungen: DatenbankAusnahme aufgetreten."); } } public void testExistiertBelegung() { /* Test der Methode "existiertBelegung()" */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try

Page 96: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

95

{ m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Erster Test: Erwartet wird ein Treffer (und damit "true") boolean bResult = objKandidat.existiertBelegung(m_objGP_Rel); assertTrue(bResult == true); // Zweiter Test: Erwartet wird 0 Treffer (und damit "false") boolean bResult2 = objKandidat.existiertBelegung(m_objGP); assertTrue(bResult2 == false); // Dritter Test: Robustheitstest mit null - Parameter try { boolean bResult3 = objKandidat.existiertBelegung(null); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testExistiertBelegung: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testExistiertBelegung: DatenbankAusnahme aufgetreten."); } } public void testLoese() { /* Test der Methode "loese()" mit Parameter vom Typ "IBelegung" */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) {

Page 97: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

96

System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Erster Test: Lösen einer nicht eingebundenen Belegung // Erwartetes Ergebnis: Exception mit Fehlermeldung try { objKandidat.loese(m_objBelegung); fail("Die erwartete 'AssoziationsException' wurde nicht erzeugt!"); } catch(AssoziationsException expected) {} // Zweiter Test: Der normale Fall. Erwartet wird keine Exception objKandidat.loese(m_objBelegung_Rel); // Dritter Test: Nochmaliges Lösen der gerade erst gelösten Belegung. // Erwartet wird eine Exception try { objKandidat.loese(m_objBelegung_Rel); fail("Die erwartete 'AssoziationsException' wurde nicht erzeugt!"); } catch(AssoziationsException expected) {} // Vierter Test: Robustheitstest mit null - Parameter try { objKandidat.loese(null); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testLoese: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testLoese: DatenbankAusnahme aufgetreten."); } } public void testLoeseDoppelt() { /* Test der Methode "loese()" mit Parametern vom Typ "IBelegung" und "IGeschaeftspartner */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false;

Page 98: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

97

try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Erster Test: Lösen zweier nicht eingebundener Objekte objKandidat.loese(m_objBelegung, m_objGP); // Zweiter Test: Lösen eines eingebundenen Objekts (Belegung) und eines // nicht eingebundenen Objekts (Geschäftspartner) objKandidat.loese(m_objBelegung_Rel, m_objGP); // Dritter Test: Der normale Fall. Erwartet wird keine Exception objKandidat.loese(m_objBelegung_Rel, m_objGP_Rel); // Vierter Test: Nochmaliges Lösen des gerade erst gelösten Paares. objKandidat.loese(m_objBelegung_Rel, m_objGP_Rel); // Fünfter Test: Robustheitstest mit null - Parametern try { objKandidat.loese(null, null); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} try { objKandidat.loese(null, m_objGP); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testLoeseDoppelt: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testLoeseDoppelt: DatenbankAusnahme aufgetreten."); } } public void testVerbinde() {

Page 99: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

98

/* Test der Methode "verbinde()" mit Parametern vom Typ "IBelegung" und "IGeschaeftspartner */ RechnungsempfaengerRelation objKandidat = RechnungsempfaengerRelation.getEinzigeInstanz(); boolean bRollbackNoetig = false; try { // Es muß vor dem Aufruf der MUT eine Datenbanktransaktion gestartet werden: try { m_objDb.starteTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("Beim Start der Transaktion: DatenbankAusnahme aufgetreten."); } // Test: Parameter - Paar ist bereits in eine Relation eingebunden. try { objKandidat.verbinde(m_objBelegung_Rel, m_objGP_Rel); fail("Die erwartete 'AssoziationsException' wurde nicht erzeugt!"); } catch(AssoziationsException expected) {} // Test: Erfolgreiches Verbinden, der "normale" Fall try { objKandidat.verbinde(m_objBelegung, m_objGP_Rel); objKandidat.loese(m_objBelegung, m_objGP_Rel); } catch(Exception ex) { System.out.println(ex.getMessage()); fail("unerwartete Exception bei verbinde + löse!"); bRollbackNoetig = true; } // Test: Wiederholtes Verbinden try { objKandidat.verbinde(m_objBelegung, m_objGP_Rel); objKandidat.loese(m_objBelegung, m_objGP_Rel); } catch(Exception ex) { System.out.println(ex.getMessage()); fail("unerwartete Exception bei verbinde + löse!"); bRollbackNoetig = true; } // Test: Verbinden eines anderen GP mit einer bereits verwendeten Belegung try { objKandidat.verbinde(m_objBelegung_Rel, m_objGP); fail("Die erwartete 'AssoziationsException' wurde nicht erzeugt!"); } catch(AssoziationsException expected) {} // Test: Robustheitstest mit null - Parametern try { objKandidat.verbinde(null, null); fail("Die erwartete 'IllegalArgumentException' wurde nicht erzeugt!"); }

Page 100: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

99

catch(IllegalArgumentException expected) {} try { objKandidat.verbinde(m_objBelegung, null); fail("Die erwartete 'IllegalArgumentException' wurde nicht erzeugt!"); } catch(IllegalArgumentException expected) {} try { objKandidat.verbinde(null, m_objGP); fail("Die erwartete 'UngültigeSchlüsselException' wurde nicht erzeugt!"); } catch(UngueltigerSchluesselException expected) {} } catch (Exception exc) { System.out.println("Exception abgefangen:"); System.out.println(exc.getMessage()); bRollbackNoetig = true; } try { if (bRollbackNoetig == true) { m_objDb.rollbackTransaktion(); fail("testVerbinde: DatenbankAusnahme aufgetreten, rollback nötig."); } else m_objDb.commitTransaktion(); } catch (DatenbankAusnahme objDbAusnahme) { System.out.println("DatenbankAusnahme abgefangen:"); System.out.println(objDbAusnahme.ausnahme); fail("testVerbinde: DatenbankAusnahme aufgetreten."); } } }

Page 101: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

100

A.4 Test der Klasse SpaltenModel Die Klasse SpaltenModel hat eine Vererbungshierarchie, die das folgende UML-Diagramm darstellt:

Abbildung 8: Vererbungshierarchie der Klasse SpaltenModel

Die Basisklasse AbstractTableModel und das implementierte Interface TableModel stammen dabei aus dem Paket javax.swing.table, einem Teil der Swing-Klassenbibliothek für Java. Dieses spezielle table-Paket dient zur Unterstützung der grafischen Anzeige zweidimensionaler Datenfelder. Detaillierte Informationen finden sich unter dem Internet-Link [URL: JavaSwing]. Konstruktor Der Test des Standard-Konstruktors gestaltet sich recht einfach. Die CUT wird instantiiert, und es wird mit der Instanzreferenz sichergestellt, dass die abfragbaren Eigenschaften auch einen Initialen Zustand wiedergeben. Die CUT hat einen zweiten Konstruktor, welcher einen Parameter (einen Iterator) übergeben bekommt. Dieser Konstruktor erwartet einen Iterator, der Objekte vom Typ IAnfrageSpalte bekommt. Es zeigte sich aber, dass dieser Konstruktor nicht robust gegen einen NULL-Parameter war, so dass nachträglich eine entsprechende Fehlerbehandlung in diesen Konstruktor eingebaut wurde. Allerdings wurde nur eine rudimentäre Behandlung eingebaut insoweit, dass ein NULL-Parameter dasselbe Verhalten wie der Standard-Konstruktor ohne Parameter bewirkt. Der Normalfall, die Konstruktion mit Übergabe eines Iterators über Objekte vom Typ IAnfrageSpalte, wurde ordnungsgemäß von der Testmethode abgearbeitet. Allerdings

Page 102: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

101

war eine Analyse der Klasse AnfrageSpalte nötig zur Konvertierung eines IAnfrageSpalte-Objekts in ein AnfrageSpalte-Objekt, und dies erforderte einen Aufwand der Kategorie „mittel“. Ein zweiter Robustheitstest sollte prüfen, ob bei einem Iterator mit unerwartetem Inhalt eine ordentliche Fehlerbehandlung stattfinden würde. Dies ist jedoch nicht der Fall: Bei Übergabe eines Iterators, der aus einem java.util.TreeSet entnommen wurde, warf der getestete Konstruktor eine Exception vom Typ java.lang.ClassCastException. Es wurde nachträglich in diesen Konstruktor ein try/catch-Block eingebaut, der diesen Fehler abfängt, eine Fehlermeldung in die Standard-Ausgabe einfügt, und ebenfalls wieder das Verhalten des parameterlosen Standard-Konstruktors bewirkt. Die Aufwände für diese Maßnahmen sind als gering einzustufen. addSpalte Die Methode addSpalte, mit der eine zusätzliche Spalte zur zugrunde liegenden Tabelle hinzugefügt wird. Getestet werden die Fälle „Einfügen am Ende“ und „Einfügen vorne“, und die Methode erweist sich als korrekt. Testet man die Methode aber mit einem Parameter (Spaltenwert) kleiner als Null, so wird eine ArrayIndexOutOfBoundsException erzeugt. Der Grund liegt in der Methode Vector.add, die von der MUT aufgerufen wird. Offensichtlich akzeptiert diese Methode keine negativen Indexwerte. Dieser Umstand fand sich jedoch nicht in der vom Verfasser dieser Arbeit verwendeten Standardliteratur zur Sprache Java ([Flan96]). Erst durch Nachschlagen in [Hors97] fand sich die genaue Beschreibung der Methode java.util.Vector.insertElementAt, die mit etwas Transferwissen die Erklärung des Fehlers ergab. Durch diese Umstände ist auch dieses Problem zu der Sorte von Problemen zu zählen, die aufgrund von Abhängigkeiten entstehen. Es handelt es sich hierbei um eine direkte Abhängigkeit von der CUT zur Klasse java.util.Vector. Der durch dieses Problem bedingte Aufwand wird als hoch eingestuft, weil die Klasse java.util.Vector analysiert werden musste, und die verwendete Literatur keine schnelle Antwort auf das Problem hergab. removeSpalte In der Methode removeSpalte zeigte sich wieder, dass bei korrekter Angabe des zu Löschenden Spaltenindex die Methode korrekt funktioniert. Alle Robustheitstest scheiterten jedoch: Die Angabe eines nicht existierenden Spaltenindex, oder jeglicher Aufruf der MUT bei leerer Tabelle erzeugen eine nicht abgefangene ArrayIndexOutOfBoundsException. Dies liegt an der aufgerufenen Methode java.util.Vector.remove, und die Gründe sind analog der Beschreibung im letzten Absatz. getSpalten, getColumnCount, getRowCount Der Test der Methode getSpalten verlief ohne Auffälligkeiten. Die Methode getColumnCount, die lediglich die Konstante 3 zurückgibt, war ebenfalls schnell vollständig getestet. Dasselbe gilt für den Test der Methode isCellEditable, welche konstant TRUE zurückliefert. Auch der Test der Methode getRowCount zum Zählen der Einträge der zugehörigen Instanz gestaltete sich einfach und unauffällig. getValueAt, setValueAt Die letzte Testmethode testet die beiden Methoden getValueAt und setValueAt zur direkten Abfrage und Manipulation von Elementen der enthaltenen Tabelle. Dabei wurde zunächst ein Robustheitstest gemacht, um das Verhalten der beiden Methoden bei einer leeren Tabelle zu testen. Wieder gab es die ArrayIndexOutOfBoundsException (bei beiden Methoden), die aufgrund der Methode java.util.Vector.get auftrat, welche bei nicht-existenten Inhalten nicht erlaubt ist.

Page 103: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

102

Der Test der Normalfälle, also das Setzen und Abrufen von Tabelleninhalten, die bereits existieren, hat einwandfrei funktioniert. Der Test des Auslesens mit einem ungültigen Spaltenindex hat funktioniert, als Rückgabewert kam das Wort Fehler; dies ist jedoch eine recht willkürliche Fehlerbehandlung, da auch in richtigen Tabellen dieses Wort durchaus vorkommen könnte. Die dritte Spalte der zugrunde liegenden Tabelle ist numerisch, und somit wurde getestet, was passiert, wenn ein Wert gesetzt werden soll, bei dem der String-Parameter einen nicht- numerischen Wert enthält. Eine NumberFormatException wurde erzeugt und nicht abgefangen. Ein Test von setValueAt mit einem zu großen Spaltenindex (3) ergab wieder die ArrayIndexOutOfBoundsException aus der Vector-Klasse. Ein Test mit Typverkehrtem ersten Parameter (erwartet per Deklaration wird ein Object, intern wird auf String gecastet) ergab eine nicht abgefangene ClassCastException. Insgesamt muss man also sagen, dass die Fehlerbehandlung äußerst dürftig ausgestaltet, beziehungsweise gar nicht vorhanden ist. Der Entwickler der Klasse hat praktisch keinerlei Vorkehrungen getroffen für Fälle, bei denen fehlerhafte Eingabedaten an die Methoden der Klasse übergeben werden. Dies erschwert das Testen, da man Fremdklassen analysieren muss (in diesem Fall z.B. java.lang.Vector), um die Ursachen für Exceptions herauszufinden, die bei der Testdurchführung auftreten. Mit dem letzten Test von setValueAt trat noch eine faustdicke Überraschung auf: Getestet werden sollte das Setzen eines negativen Wertes für die dritte Spalte, die fachlich als Spaltenbreite interpretiert wird. Dieser Test führte zu einem Stillstand des Tests, es sah aus, als sei die Testdurchführung stehen geblieben, mitten im Test. Auch das Schließen der Entwicklungsumgebung und ein Neustart halfen nichts. Erst nach langem Suchen konnte festgestellt werden, dass eine Hinweisnachricht mit der Klasse MeldungsAnzeigeA erzeugt worden ist, die jedoch hinter der Entwicklungsumgebung angezeigt wurde und damit unsichtbar war. Durch die lange Fehleranalysezeit ist der Aufwand als hoch einzustufen. Dieser Umstand hat nicht nur die Testdurchführung zeitlich verzögert, sondern er sorgt sogar dafür, dass ein wirklich automatisierter Test gar nicht möglich ist. Es muss ein Anwender beim Test dabei sein, um die Meldung hervorzuholen und zu bestätigen. Dies heißt in der Konsequenz, dass der Testentwickler entweder keine automatischen nighly-tests fahren kann, oder dass er bewusst auf Teile des Testcodes verzichten muss und damit keine vollständige Abdeckung der Funktionalität der CUT erreichen kann. Ein Ausweg könnte der so genannte AWT-Roboter sein (siehe [Link02], Kapitel 13), der gezielt einen Mausklick simulieren kann, um das Nachrichtenfenster zu akzeptieren. Dazu muss aber die Position des Fensters auf dem Display bekannt sein. Im Folgenden wird der komplette Code der Testklasse wiedergegeben: package test.SeminarisP.AnwendungslogikP.AnfragenP; import java.util.TreeSet; import java.util.Vector; import junit.framework.*; import persistenz.DatenbankAusnahme; import SeminarisP.AnwendungslogikP.AnfragenP.SpaltenModel; import SeminarisP.DatenhaltungP.AnfragenP.Anfrage; import SeminarisP.DatenhaltungP.AnfragenP.AnfrageSpalte; import SeminarisP.DatenhaltungP.AnfragenP.IAnfrage; import SeminarisP.DatenhaltungP.AnfragenP.IAnfrageSpalte; import SeminarisP.DatenhaltungP.OrdnerP.AnfrageOrdner; import SeminarisP.ExterneSystemeP.PersistenzP.ISeminarisDatenbank;

Page 104: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

103

import SeminarisP.ExterneSystemeP.PersistenzP.SeminarisDatenbankProvider; public class TestSpaltenModel extends TestCase { // Verweis auf die einzige Instanz der SeminarisDatenbank: ISeminarisDatenbank m_objDb; public TestSpaltenModel(String name) { super(name); m_objDb = SeminarisDatenbankProvider.getDatenbank(); assertTrue(m_objDb != null); } protected void setUp() { // Write your code here } protected void tearDown() { // Write your code here } public void testStandardKontruktor() { // Aufruf des öffentlichen Standard-Konstruktors: SpaltenModel objKandidat = new SpaltenModel(); // Sicherstellen, daß alle Attribute in initialem Zustand sind: assertTrue(objKandidat.getSpalten() != null); assertTrue(objKandidat.getRowCount() == 0); } public void testKontruktor2() { // Erzeugen eines Vectors, der zwei Objekte vom Type "AnfrageSpalte" enthält: Vector vcAnfragen = new Vector(); IAnfrageSpalte anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); // Aufruf des öffentlichen Konstruktors, der einen Iterator übergeben bekommt, // mit dem gerade erzeugten Array: SpaltenModel objKandidat = new SpaltenModel(vcAnfragen.iterator()); // Sicherstellen, daß alle Attribute in initialem Zustand sind: assertTrue(objKandidat.getSpalten() != null); assertTrue(objKandidat.getRowCount() == 2); // Robustheitstest mit null - Parameter: SpaltenModel objKandidat2 = new SpaltenModel(null); // Robustheitstest mit typverkehrtem Iterator: Ein TreeSet wird als Basis // verwendet. TreeSet ts = new TreeSet(); ts.add("Test!"); SpaltenModel objKandidat3 = new SpaltenModel(ts.iterator()); } // Erzeugt eine Instanz der CUT, die bereits mit zwei Einträgen gefüllt ist. private SpaltenModel erzeugeGefuellteInstanz() { // Erzeugen eines Vectors, der zwei Objekte vom Type "AnfrageSpalte" enthält: Vector vcAnfragen = new Vector(); IAnfrageSpalte anfrageSpalte = AnfrageSpalte.erzeugeTransient(); vcAnfragen.add(anfrageSpalte); anfrageSpalte = AnfrageSpalte.erzeugeTransient();

Page 105: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

104

vcAnfragen.add(anfrageSpalte); // Erzeugen einer Instanz der CUT mit dem gerade erzeugten Array: SpaltenModel objKandidat = new SpaltenModel(vcAnfragen.iterator()); assertTrue(objKandidat.getRowCount() == 2); return objKandidat; } // Test der Methode "getColumnName" public void testGetColumnName() { SpaltenModel objKandidat = new SpaltenModel(); // Drei reguläre Fälle Testen: String strResult = objKandidat.getColumnName(0); assertTrue(strResult == "Name"); strResult = objKandidat.getColumnName(1); assertTrue(strResult == "Spalteninhalt"); strResult = objKandidat.getColumnName(2); assertTrue(strResult == "Spaltenbreite beim Ausdruck"); // Verschiedene ungültige Eingaben Testen: strResult = objKandidat.getColumnName(-1); assertTrue(strResult == "Fehler"); strResult = objKandidat.getColumnName(3); assertTrue(strResult == "Fehler"); strResult = objKandidat.getColumnName(32700); assertTrue(strResult == "Fehler"); } // Test der Methode "addSpalte" public void testAddSpalte() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); // Zwei Aufrufe von "addSpalte()", einfügen vorne (0) und weit hinten (527) objKandidat.addSpalte(0); assertTrue(objKandidat.getRowCount() == 3); objKandidat.addSpalte(527); assertTrue(objKandidat.getRowCount() == 4); // Noch ein Robustheitstest: try { objKandidat.addSpalte(-32766); assertTrue(objKandidat.getRowCount() == 5); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } } // Test der Methode "RemoveSpalte" public void testRemoveSpalte() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); // Erster Robustheitstest: Aufruf von removeSpalte auf der nicht // vorhandenen Spalte Nr. 3: try { objKandidat.removeSpalte(3);

Page 106: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

105

assertTrue(objKandidat.getRowCount() == 1); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Zwei Aufrufe von "removeSpalte()", um die Tabelle wieder komplett zu // Entleeren objKandidat.removeSpalte(1); assertTrue(objKandidat.getRowCount() == 1); objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); // Noch ein Robustheitstest: Aufruf von removeSpalte auf der entleerten // Tabelle: try { objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } } // Test der Methode "getSpalten" public void testGetSpalten() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); Vector vcSpalten = objKandidat.getSpalten(); assertTrue(vcSpalten != null); assertTrue(vcSpalten.size() == 2); // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat2 = new SpaltenModel(); vcSpalten = objKandidat2.getSpalten(); assertTrue(vcSpalten != null); assertTrue(vcSpalten.size() == 0); } // Test der Methode "getColumnCount" public void testGetColumnCount() { // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat = new SpaltenModel(); assertTrue(objKandidat.getColumnCount() == 3); // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); assertTrue(objKandidat2.getColumnCount() == 3); } // Test der Methode "getRowCount" public void testGetRowCount() { // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat = erzeugeGefuellteInstanz(); assertTrue(objKandidat.getRowCount() == 2); // Zwei Aufrufe von "removeSpalte()", um die Tabelle wieder komplett zu // Entleeren objKandidat.removeSpalte(1);

Page 107: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

106

assertTrue(objKandidat.getRowCount() == 1); objKandidat.removeSpalte(0); assertTrue(objKandidat.getRowCount() == 0); // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat2 = new SpaltenModel(); assertTrue(objKandidat2.getRowCount() == 0); } // Test der Methode "isCellEditable" public void testIsCellEditable() { // Erzeugen einer leeren Instanz der CUT: SpaltenModel objKandidat = new SpaltenModel(); assertTrue(objKandidat.isCellEditable(0, 0) == true); // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); assertTrue(objKandidat2.isCellEditable(2, 2) == true); } // Test der Methoden "getValueAt()" und "setValueAt()" public void testGetAndSetValueAt() { // Erzeugen einer leeren Instanz der CUT. SpaltenModel objKandidat = new SpaltenModel(); // Robustheitstest: abfragen und setzen von Werten einer leeren Instanz: try { objKandidat.setValueAt("Testwert-1-2-3", 0, 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } try { Object objErgebnis = objKandidat.getValueAt(0, 0); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Erzeugen einer Instanz der CUT mit zwei Einträgen: SpaltenModel objKandidat2 = erzeugeGefuellteInstanz(); // Testen der "Normalfälle": objKandidat2.setValueAt("Testwert-1-2-3", 0, 0); Object objErgebnis = objKandidat2.getValueAt(0, 0); assertTrue((String) objErgebnis == "Testwert-1-2-3"); objKandidat2.setValueAt("ZweiterWertZumTesten", 1, 1); objErgebnis = objKandidat2.getValueAt(1, 1); assertTrue((String) objErgebnis == "ZweiterWertZumTesten"); // Test mit ungültigem Spaltenindex: objErgebnis = objKandidat2.getValueAt(1, 3); assertTrue((String) objErgebnis == "Fehler"); // Test mit einem zu Setzenden Inhalt, der nicht numerisch ist: try { objKandidat2.setValueAt("Blödsinn", 1, 2); } catch(NumberFormatException ex)

Page 108: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang A Testklassen

107

{ System.out.println("NumberFormatException abgefangen:"); System.out.println(ex.getMessage()); } //Test mit einem Spaltenindex, der zu groß ist: try { objKandidat2.setValueAt("Blödsinn", 1, 3); } catch(ArrayIndexOutOfBoundsException ex) { System.out.println("ArrayIndexOutOfBoundsException abgefangen:"); System.out.println(ex.getMessage()); } // Test mit typverkehrtem ersten Parameter: try { objKandidat2.setValueAt(this, 1, 1); } catch(ClassCastException ex) { System.out.println("ClassCastException abgefangen:"); System.out.println(ex.getMessage()); } // Die nächste Zeile testet einen negativen Wert für die Spaltenbreite; // Eine MessageBox wird HINTER der Entwicklungsumgebung angezeigt! objKandidat2.setValueAt("-1", 1, 2); } }

Page 109: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

108

������� ���� � ��� � �� �� �� ���� � �� � �� � � ��� �� ���� �� �� �� � ��

Im Kapitel 5 wurde bereits erwähnt, dass sehr viele Klassen eine festverdrahtete Abhängigkeit zu einer Benutzernachrichtenklasse haben, und diese Häufung zu der Idee einer Refaktorisierung geführt hat. In der Folgenden Grafik (Abbildung 9) wird die Hierarchie der Benutzernachrichtenklassen als UML-Diagramm dargestellt.

Abbildung 9: UML-Diagramm der Benutzernachrichtenklassen

Die größten Probleme, die sich im Benutzungs - Umfeld dieser Klassen zeigten, sind zum einen die statischen und öffentlichen Konstanten der Klasse SeminarisMeldung (siehe Abbildung 9), und die statischen erzeuge – Methoden, die jede einzelne Nachrichtenklasse anbietet. Dies sind die Gründe für die vielen festverdrahteten Abhängigkeiten anderer Klassen von diesen Nachrichtenklassen. Die Refaktorisierungsidee, die sich sofort aufdrängt, ist eine class factory, die jede Art von Nachrichtenklasse erzeugen kann, und durch das Anbieten von entsprechenden Methoden auch die Verwendung der statischen öffentlichen Konstanten überflüssig macht. Die neue Klasse NachrichtenFabrik, die hierzu entwickelt wurde, und deren Interface INachrichtenFabrik, werden im Folgenden vorgestellt (beide sind vergleichsweise kompakt geraten): public interface INachrichtenFabrik { public SeminarisMeldung erzeugeExceptionNachricht(Exception eineExp); public SeminarisMeldung erzeugeFehlermeldung(String strMeldungsnummer,

String strMeldungstext); public SeminarisMeldung erzeugeFrageNachrichtOhneSymbol(String strMeldungsnummer,

String strMeldungstext); public SeminarisMeldung erzeugeFrageNachrichtMitSymbol(String strMeldungsnummer,

String strMeldungstext); public SeminarisMeldung erzeugeHinweis(String strMeldungsnummer,

String strMeldungstext); public SeminarisMeldung erzeugeInformation(String strMeldungsnummer,

String strMeldungstext); public SeminarisMeldung erzeugeLeerNachricht(); public SeminarisMeldung erzeugeWarnung(String strMeldungsnummer,

Page 110: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang B

109

String strMeldungstext); public boolean istInformation(SeminarisMeldung objMeldung); }

public class NachrichtenFabrik implements INachrichtenFabrik { public static NachrichtenFabrik erzeuge() { return new NachrichtenFabrik(); } public SeminarisMeldung erzeugeExceptionNachricht(Exception eineExp) { return ExceptionNachricht.erzeuge(eineExp); } public SeminarisMeldung erzeugeFehlermeldung(String strMeldungsnummer, String strMeldungstext) { return HinweisNachricht.erzeuge(strMeldungsnummer, strMeldungstext, SeminarisMeldung.ERROR); } public SeminarisMeldung erzeugeFrageNachrichtOhneSymbol(String strMeldungsnummer, String strMeldungstext) { return FrageNachricht.erzeuge(strMeldungsnummer, strMeldungstext); } public SeminarisMeldung erzeugeFrageNachrichtMitSymbol(String strMeldungsnummer, String strMeldungstext) { return FrageNachricht.erzeuge(strMeldungsnummer, strMeldungstext, SeminarisMeldung.QUESTION); } public SeminarisMeldung erzeugeHinweis(String strMeldungsnummer, String strMeldungstext) { return HinweisNachricht.erzeuge(strMeldungsnummer, strMeldungstext); } public SeminarisMeldung erzeugeInformation(String strMeldungsnummer, String strMeldungstext) { return HinweisNachricht.erzeuge(strMeldungsnummer, strMeldungstext, SeminarisMeldung.INFORMATION); } public SeminarisMeldung erzeugeLeerNachricht() { return KeineNachricht.erzeuge("", "Keine Meldung"); } public SeminarisMeldung erzeugeWarnung(String strMeldungsnummer, String strMeldungstext) { return HinweisNachricht.erzeuge(strMeldungsnummer, strMeldungstext, SeminarisMeldung.WARNING); } public boolean istInformation(SeminarisMeldung objMeldung) { if (objMeldung.getMeldungsTyp() == SeminarisMeldung.INFORMATION) return true; else return false; }

Page 111: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang B

110

}

Der Refaktorisierungsaufwand hat sich dabei als etwas aufwändiger erwiesen als zunächst geplant. Es konnte nicht mit Copy-and-Paste-Ersetzungen gearbeitet werden, da jeder Änderungsfall einzeln betrachtet werden musste. In den meisten Fällen wurde eine Zeile der Form HinweisNachricht meldung = HinweisNachricht.erzeuge(„<Überschrift>“, „<Meldung>“, SeminarisMeldung.KONSTANTE);

ersetzt durch SeminarisMeldung meldung = objNachrichtenFabrik.erzeugeHinweis(„<Überschrift>“, „<Meldung>“);

Dabei musste natürlich auch beachtet werden, dass entsprechend der Konstante die richtige Methode verwendet wird (Information, Warnung, Error, ...). Außerdem war zu Beachten, dass vorher eine Nachrichtenfabrik instantiiert oder verfügbar war. In diesem Zusammenhang wurde gleich eine weitere Verbesserung erreicht: Es hat sich gezeigt, dass die überwiegende Menge der Aufrufe in einem ganz bestimmten Schema erfolgt. Dabei ruft eine AAS-Klasse die Methode einer Kontrollklasse auf, z.B. pruefen, speichern, oder ähnliches, und erhält von dieser Methode ein Objekt vom Typ SeminarisMeldung zurück. Die Kontrollklassen wiederum rufen ihrerseits weitere Methoden auf, die ebenfalls eine SeminarisMeldung zurückgeben. Die AAS-Klasse wertet am Ende die SeminarisMeldung aus und zeigt diese gegebenenfalls an. Es wurden daher die Instanzen der NachrichtenFabrik in den AAS-Klassen erstellt, und in Form des Interfaces INachrichtenFabrik an die jeweils aufgerufenen Methoden weitergegeben. Damit konnte erreicht werden, dass nur die AAS-Klassen festverdrahtet abhängig werden von der NachrichtenFabrik, und die Kontrollklassen komplett von festverdrahteten Abhängigkeiten zu Nachrichtenklassen befreit werden konnten. Dies erhöhte natürlich den Refaktorisierungsaufwand zusätzlich. Denkbar wäre auch, eine einzige globale Instanz der NachrichtenFabrik bereitzustellen und den AA-Klassen bei der Instantiierung nur das zugehörige Interface mitzugeben; dies wurde jedoch nicht implementiert. Das Erstellen der NachrichtenFabrik mitsamt dem zugehörigen Interface dauerte nur etwa 30 Minuten. Es dauerte jedoch sieben Stunden, alle festverdrahteten Abhängigkeiten mit der Eigenschaft

DSTMh < 50

zu bearbeiten (genauer: dort alle festverdrahteten Abhängigkeiten zu den Klassen HinweisNachricht, FrageNachricht, ExceptionNachricht, KeineNachricht und SeminarisMeldung zu eliminieren). Nach diesen insgesamt siebeneinhalb Stunden wurde die Refaktorisierung abgebrochen. Die Gesamtzahl der festverdrahteten Abhängigkeiten im System konnte alleine mit dieser Maßnahme von 1085 auf 965 reduziert werden.

Page 112: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang B

111

Die Veränderung der Projektmetriken wird mit der folgenden Tabelle veranschaulicht: Metrik Wert vorher Wert nachher ACD 35,81 35,83 NFD 23,00 23,00 NSBC 19,00 19,00 NCDC 39,00 39,00 NDC 10,00 10,00

Tabelle 20: Links die Projektmetriken vor, rechts nach der Refaktorisierung

Es zeigt sich nur eine minimale Änderung des ACD-Wertes, überraschenderweise zum Schlechteren hin. Offensichtlich trat der Effekt, dass durch die neue Klasse und das neue Interface auch zusätzliche Abhängigkeiten geschaffen wurden, minimal stärker zu Tage, als die Reduzierung der Abhängigkeiten von den verschiedenen Nachrichtenklassen zum Tragen kam. Möglicherweise sähen die Zahlen auch besser aus, wenn die Refaktorisierung konsequent bis zum Schluss Weitergetrieben worden wäre.

Page 113: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

112

������������ � �� �� �� � ��

Literaturverzeichnis [Flan96] FLANAGAN, David. Java in a Nutshell. A Desktop Quick Reference. 4. Auflage

März 2002. Verlag O’Reilly & Associates Inc. ISBN 0-596-00283-1. [Gamm94] GAMMA, Erich; HELM, Richard; JOHNSON, Ralph; VLISSIDES John: Design

Patterns. Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994

[Hors97] HORSTMANN, Cay S.; CORNELL, Gary. Core Java. Volume I-Fundamentals.

1. Auflage 1997. Herausgeber: Sun Microsystems Press. Verlag: Prentice Hall. ISBN 0-13-766957-7.

[Jung02] JUNGMAYR, Stefan. Testability Measurement and Software Dependencies.

Proceedings of the 12th International Workshop on Software Measurement, October 7-9, 2002, Magdeburg, Germany, pp. 179-202. Verfügbar unter http://www.testbarkeit.de, oder unter www.informatik.fernuni-hagen.de/import/pi3/stefan/Publications/IWSM02.pdf

[Jung03] JUNGMAYR, Stefan. Improving testability of object-oriented systems.

Dissertation, FernUniversität in Hagen, Fachbereich Informatik, 2004. [Lako96] LAKOS, John. Large-Scale C++ Software Design. 10. Auflage August 2001.

Addison-Wesley Verlag. ISBN 0201633620. [Link02] LINK, Johannes. Unit Tests mit Java. 1. Auflage 2002. Heidelberg. dpunkt-

Verlag. ISBN 3-89864-150-3. [McCo93] MCCONNELL, Steve. Code Complete – A practical Handbook of Software

Construction. Redmond, USA, 1993. Deutsche Übersetzung 1995, Microsoft Press Deutschland, 85716 Unterschleißheim. ISBN 3-86063-333-3.

[Merl03] MERL, Edgar. Testbarkeitsanalyse von Abhängigkeiten in objektorientierter

Software. Diplomarbeit an der FernUniversität in Hagen, Lehrgebiet Praktische Informatik III. April 2003.

[Meye92] MEYER, Bertrand. Applying „Design by Contract“. IEEE Computer Magazine,

vol.25, no.10, Oktober 1992, Seite 40 - 51. Institute of Electrical and Electronics Engeneers, 10662 Los Vaqueros Circle, PO Box 3014, Los Alamitos, CA 90720-1264, USA.

[Six03] SIX, Prof. Dr. Hans-Werner; WINTER, Prof. Dr. Mario. Software Engeneering I.

Skript zum Kurs Nummer 1793 an der FernUniversität in Hagen.

Page 114: Untersuchung der Auswirkungen von Abhängigkeiten auf den ... · Abhängigkeiten identifiziert werden können, deren Refaktorisierung eine wesentliche Verbesserung der Testbarkeit

Anhang C Referenzen

113

URLs [URL: CppUnit] http://sourceforge.net/projects/cppunit [URL: Java] http://wwws.sun.com/software/java [URL: JavaSwing] http://java.sun.com/j2se/1.4.1/docs/api/javax/swing/table/class-use/

TableModel.html [URL: JDBC] http://java.sun.com/products/jdbc [URL: JUnit] http://www.junit.org [URL: MFC] http://msdn.microsoft.com/library/en-us/vcmfc98/html/vcmfchm.asp [URL: Lutris] http://www.lutris.com [URL: Sun] http://www.sun.com [URL: Together] http://www.borland.de/together/controlcenter/index.html