• Keine Ergebnisse gefunden

Besonderheiten objektorientierter Systeme .1 Das Besondere an objektorientierten Programmen

Im Dokument Das Praxishandbuch für den Test (Seite 35-44)

Während zu Beginn der 80er Jahre die ersten industriell verwendeten Implementie-rungen objektorientierter Programmiersprachen verfügbar waren und sich ab 1985 methodische Ansätze zur objektorientierten Software-Entwicklung durchzusetzen begannen, blieb der Test objektorientierter Software lange Zeit über unbeachtet.

Viele populäre „Methodengurus“ verlieren in ihren Veröffentlichungen kein Wort über den Test. Grady Booch schreibt lediglich

„... the use of object-oriented design doesn’t change any basic testing principles; what does change is the granularity of the units tested.“ [Boo94]

James Rumbaugh behauptet sogar

„Both testing and maintenance are simplified by an object-oriented approach...“

[RBP+91]

Glaubte man also anfangs, dass objektorientierte Software einen erheblich reduzier-ten Aufwand für die Prüfung erfordern würde und dass bekannte Prüfverfahren unmodifiziert verwendet werden können, so weiß man heute, dass diese Hoffnun-gen sich nicht erfüllt haben. Erste Anzeichen für einen erhöhten Qualitätssiche-rungsbedarf für objektorientierte Software finden Perry und Kaiser, die bei der Untersuchung des Wiederverwendungspotenzials objektorientierter Software schon 1990 feststellen:

„... we have uncovered a flaw in the general wisdom about object-oriented languages

— that "proven" (that is well-understood, well-tested, and well-used) classes can be reused as superclasses without retesting the inherited code.“[PeKa90]

Im Allgemeinen gelten für objektorientierte Programme die gleichen Anforderun-gen wie für konventionell funktionsorientierte Programme. Es müssen repräsentati-ve Test-Eingabedaten generiert, Zwischenergebnisse geprüft, Ablaufpfade repräsentati-verfolgt und Testergebnisse validiert werden. Ein Testfall wird in einem bestimmten Vorzu-stand ausgelöst und führt zu einem gegebenen NachzuVorzu-stand. Das Programm, spezi-fiziert als Transformationsregel, gilt als korrekt, wenn der Ist-Nachzustand dem spezifizierten Soll-Nachzustand entspricht [LiRü96]. In dieser Hinsicht sind objekt-orientierte Programme nicht anders zu betrachten als die bisherigen. Dennoch ist diese Sicht nur für den Systemtest gültig. Was den Modultest und den Integrations-test betrifft, gibt es einige signifikante Unterschiede.

Erstens führt die stärkere Modularisierung zu mehr intermodularen Abhängigkeiten.

Eine Methode einer bestimmten Klasse ist oft auf Methoden anderer Klassen ange-wiesen, um ihre Funktion ausführen zu können. Diese anderen Klassen unterliegen nicht selten der Zuständigkeit anderer Entwickler. So sind auch die Entwickler mehr voneinander abhängig [Dlu94]. Es ist nicht mehr so, dass ein Entwickler für die Abwicklung einer Transaktion vom Eintritt ins System bis zum Abschluss ver-antwortlich ist. Die Arbeitsteilung ist nicht mehr funktional ausgerichtet, sondern nach den Objekten der Verarbeitung. Hinzu kommen die vielen potenziellen An-wendungen. Nicht nur, dass öffentliche Methoden einer Klasse von Methoden jeder beliebigen anderen Klasse aufgerufen werden können, eine Klasse kann auch frem-de Attribute und Methofrem-den von jefrem-der übergeordneten Klasse übernehmen. Das Erben fremder Eigenschaften aus anderen Klassen schafft weitere Abhängigkeiten.

1.4 Besonderheiten objektorientierter Systeme _______________________________23

In objektorientierten Systemen wird Redundanz auf Kosten der gegenseitigen Ab-hängigkeit eliminiert.

Zweitens kann eine Klasse eine Vielzahl einzelner Methoden beinhalten, die zwar prozedural voneinander unabhängig sind, jedoch über den Zustand der gemeinsa-men Objektattribute miteinander verquickt sind. So kann die eine Methode einen Objektzustand hinterlassen, der das Verhalten der Nachfolgemethode beeinflusst, denn zum Argumentenbereich einer Methode gehören nicht nur ihre Eingangspara-meter, sondern auch der Zustand ihres Objekts. Dadurch sind die Methoden einer Klasse doch über gemeinsame Daten gekoppelt. Falls Methoden derselben Klasse sich gegenseitig nutzen, was bei komplexen Klassen häufig vorkommt, entsteht auch noch eine prozedurale Kopplung [HaWi94].

Drittens ist bei der Entwicklung einer Klasse oft nicht bekannt, zu welchem Zweck die Methoden der Klassen verwendet werden. Sie müssten so programmiert sein, dass sie jeden potenziellen Zweck erfüllen können. Solche Offenheit führt zu einer Vielzahl möglicher Zustände, die nicht alle getestet werden können, ohne den fi-nanziellen Rahmen eines Projekts zu sprengen. Ein Test aller relevanten Zustände erfordert eine Prognose über die potenzielle Nutzung eines Objekts. Falls diese Prognose daneben liegt, bleiben einige wichtige Zustände ungetestet. Hier offenbart sich eine Diskrepanz zwischen funktionsbezogener Software, die auf einen be-stimmten Zweck zugeschnitten ist und die nur in Bezug auf diesen Zweck getestet werden muss, und objektorientierter Software, die vielen potenziellen Zwecken dienen sollte und deshalb in Bezug auf das potenzielle Nutzungsprofil getestet wer-den sollte [SmRo90].

Viertens können die von den Klassen beschriebenen Objekte viele mögliche Zu-stände annehmen. Je komplexer die Objekte sind, je mehr Attribute und Methoden sie haben, umso mehr Zustände können sie annehmen. Wer eine Klasse zu einhun-dert Prozent testen will, müsste alle möglichen Zustände des darin enthaltenen Ob-jekts erzeugen und sämtliche Zustandsübergänge testen. Dies führt zu einer expo-nentiellen Steigerung der Anzahl von Testfällen für den Test der Klasse. Deshalb ist das 100-prozentige Austesten einer Klasse (wegen der Überzahl der Testfälle und Eingabedaten) in der Regel nicht praktikabel. Also muss man sich (wie schon beim herkömmlichen Test) auf geeignet ausgewählte Stichproben beschränken, Testen ist und bleibt also ein stichprobenartiges Verfahren. Bei der bisherigen prozeduralen transaktionsorientierten Programmierung hing die Anzahl der Datenzustände von der Logik der jeweiligen Transaktion ab, d.h. die Transaktionslogik grenzte den Problembereich ein. In offenen wiederverwendbaren OO-Systemen sind die Gren-zen des Problembereiches eher fließend [TuRo93].

Schließlich reicht die klassische Instrumentierungstechnik nicht aus, um die Test-überdeckung des Codes einer Klasse festzustellen. Die prozedurale Testüberde-ckungsmessung ging davon aus, dass alle Anweisungen bzw. Zweige und Ablauf-pfade eines Programms der Erfüllung der Programmfunktion dienten und deshalb

getestet werden mussten. Das Programm war letzten Endes eine 1:1-Abbildung der Funktion. In objektorientierten Programmen gibt es diese einfache 1:1-Beziehung zwischen Programm und Funktion nicht mehr. Eine Klasse enthält Methoden, die alle Operationen auf einem bestimmten Objekt ausführen, unabhängig davon, zu welcher Anwendung sie gehören. So wird von einer bestimmten Anwendung nur ein Teil der Methoden benutzt. Es hat deshalb wenig Sinn, den Code in seiner Ge-samtheit zu instrumentieren, denn große Teile davon bleiben von den zu testenden Transaktionen unberührt. Es gilt, nur jene Methoden zu testen, die eine gegebene Transaktion beansprucht. Darum muss nach einem Nutzungsprofil instrumentiert werden, um die funktionale Testüberdeckung zu ermitteln. Eine derart selektive Instrumentierung ist natürlich technisch anspruchsvoller als eine blinde Instrumen-tierung des gesamten Codes [Sne95].

Objektorientierte Software ist nicht nur schwieriger zu testen, sie kann auch mehr Fehler verursachen, vor allem wenn die Entwickler mit der Technologie nicht so vertraut sind. Polymorphie und dynamische Bindung führen zu einer Vermehrung der potenziellen Ablaufpfade und damit zu einer Vermehrung der potenziellen Feh-ler. Vererbung schafft viele subtile, unsichtbare Abhängigkeiten und Kapselung beschränkt die Sicht auf die Objektzustände und erschwert dadurch die Fehlerer-kennung. Die zahlreichen Kollaborationen zwischen Objekten schaffen auch viele Schnittstellen, die wiederum viele Abstimmungen erfordern. Abstimmungen führen wiederum zu Missverständnissen und Missverständnisse zu Fehlern [Bin99].

In bisherigen Untersuchungen über Fehlerraten in objektorientierten Systemen hat es sich gezeigt, dass die Fehlerdichte eher höher ist als in klassischen Systemen.

Nach den Daten der Bournemouth Universität in England sind Klassen in Verer-bungshierarchien dreimal so fehleranfällig wie Klassen ohne Vererbung [ShCa97].

Bereits 1996 hat Capers Jones 600 objektorientierte Projekte in 150 Anwenderbe-trieben untersucht und ist zu den folgenden Schlüssen gekommen:

• Die Anzahl Fehler aus mangelnder Erfahrung mit der Technologie ist auffallend hoch.

• Fehler in der Analyse und im Entwurf haben eine viel größere Auswirkung als Fehler in den bisherigen Analyse- und Entwurfsmethoden.

• Es ist schwieriger, Fehlerursachen aufzudecken.

• Der Code ist weniger umfangreich, was zur Folge hat, dass die Fehlerdichte höher ist.

• Wiederverwendete Klassen weisen in der Regel weniger Fehler auf [Jon97].

Daraus lässt sich schließen, dass objektorientierte Systeme im Prinzip zuverlässiger sein könnten, vorausgesetzt, alles stimmt und die Entwickler beherrschen die Tech-nologie, was aber in der Tat selten so ist. In der Praxis haben objektorientierte Sys-teme eine höhere Fehlerrate als die bisherigen. Hinzu kommt, dass der Test objekt-orientierter Programme sich um einiges anspruchsvoller als der Test klassischer

1.4 Besonderheiten objektorientierter Systeme _______________________________25

Programme gestaltet. Boris Beizer schreibt in einem Beitrag zum American Pro-grammer:

„...it costs a lot more to test OO-software than to test ordinary software – perhaps four or five times as much ...Inheritance, dynamic binding, and polymorphism create testing problems that might exact a testing cost so high that it obviates the advantages [Bei94].“

Ob diese Aussage stimmt oder nicht, bleibt dahingestellt. Fest steht, dass objektori-entierte Programme anders zu testen sind. Einige Probleme sind geringer geworden, z.B. tief verschachtelte Ablaufstrukturen, dafür treten neue Probleme auf, z.B. dy-namische Bindung mit einem nicht statisch determinierbaren „Zielobjekt“ einer Botschaft. Die Komplexität der Programmlogik verlagert sich von der intramodula-ren zur intermodulaintramodula-ren Komplexität. Offenheit und Wiederverwendung fordern ihren Preis. Daher müssen neue Testansätze erprobt werden, Ansätze, die den Be-sonderheiten objektorientierter Programme gerecht werden.

1.4.2 Testgegenstände in einem OO-System

Der Test bezieht sich immer auf bestimmte Gegenstände. Ein Gegenstand wird aus dem Gesamtzusammenhang herausgeholt und für sich in einer kontrollierten Test-umgebung getestet. Es werden Vorbedingungen erfüllt, Ausgangszustände gesetzt, Eingaben zugeführt und Ausgaben abgefangen und untersucht. Es gilt festzustellen, ob der Gegenstand sich korrekt bzw. laut Spezifikation verhält. Der Umfang des Tests hängt von der Größe des Gegenstands ab.

In einem objektorientierten System ist im Prinzip die kleinste testbare Einheit eine Methode (Abbildung 1.8). Eine Methode ist zwar eingebettet in einer großen Quell-code-Einheit, aber sie hat einen eigenen Eingang, einen Ausgang und eigene Para-meter. Es ist daher ohne weiteres möglich, einzelne Methoden aufzurufen und ihr Verhalten zu prüfen. Methoden entsprechen in etwa den Kontrolleinheiten in einer graphisch-interaktiven Benutzungsoberfläche und sind dementsprechend einzeln testbar.

Die nächste größere Einheit ist die Klasse. In einer Klasse sind alle Methoden, die einen bestimmten Objekttyp verarbeiten, zusammengefasst bzw. gekapselt. In der Regel realisiert eine Klasse genau einen abstrakten Datentyp. Von bzw. anhand der Klasse werden Objekte (Instanzen) generiert, verarbeitet und am Ende entweder gelöscht oder aufbewahrt. Im zweiten Fall spricht man auch von persistenten Ob-jekten. Getestet wird eine Klasse über ihre Schnittstellen bzw. über ihre Methoden, angefangen mit dem Konstruktor zur Erzeugung eines Objekts bis hin zum Destruk-tor zu seiner Zerstörung. Klassen können jedoch sehr klein sein. Oft sind mehrere Klassen in einem Source-Modul zusammengefasst bzw. einer Klassenmenge – einem Paket – zugeordnet.

1

4 2

5 3 Eine Methode

Eine Klasse

Eine Klassenmenge

Ein Subsystem bzw.

eine Komponente

Eine Applikation bzw. ein System Abbildung 1.8 Testgegenstände in OO-Systemen

Objektorientierte Systeme sollten von Anfang an auf Testbarkeit ([Jun99]) und Wartbarkeit ausgerichtet, d.h. in Komponenten geteilt werden. Komponenten sind mehr oder weniger abgeschlossene Klassenmengen, in denen nur sehr wenige Ab-hängigkeiten zu Klassen in anderen Komponenten bestehen. Es soll nur definierte Schnittstellen bzw. APIs zwischen getrennt gebildeten Komponenten geben. Kom-ponenten sind also gute Testgegenstände. Je mehr KomKom-ponenten ein System hat, desto leichter ist es zu testen [Bin94a].

Schließlich ist das Anwendungssystem selbst der endgültige Testgegenstand. Natür-lich gibt es hier auch kleine und große Anwendungen. Aus der Sicht des Tests, aber nicht nur aus dieser Sicht, ist es immer besser, kleine überschaubare Anwendungen mit einer begrenzten Anzahl Ein- und Ausgaben zu haben. Je kleiner die Anwen-dung, desto geringer der Testaufwand. Gerade in einer komplexen Client/Server-Welt empfiehlt es sich, die Anwendungen möglichst klein zu halten. Die Argumen-tation, dass alles fachlich zusammengehört, ist nicht stichhaltig. Im Prinzip ist alles ein großes System. Dazu zählt das ganze Unternehmen. Demnach muss das noch lange nicht heißen, dass das Ganze als ein einziges allumfassendes Anwendungs-system implementiert werden muss. Die Kunst der Systemplanung ist, möglichst kleine unabhängige Anwendungssysteme zu schaffen, die in einer angemessenen Zeit implementierbar und auch testbar sind.

1.4.3 Folgen der Kapselung

Die Kapselung der Daten und Funktionen in einzelnen abgeschlossenen Objekten mit fest definierten Schnittstellen nach außen bringt für den Test bzw. für die

Feh-1.4 Besonderheiten objektorientierter Systeme _______________________________27

lerfindung auf den ersten Blick nur Vorteile. Es müsste leichter sein, gekapselte Objekte unabhängig von den anderen Objekten zu testen und es müsste vor allem leichter sein, Fehlerursachen zu lokalisieren. Andererseits können Objekte eine Vielzahl möglicher Zustände annehmen, und diese Zustände müssen alle auspro-biert werden. Hinzu kommt, dass die Schnittstellen zu den Objekten recht komplex werden können. Auch sie sind in allen Varianten zu testen. Methoden können nicht einfach einzeln aufgerufen werden. Da ihre Ausführung vom jeweiligen Objektzu-stand abhängt und dieser ZuObjektzu-stand durch die vorher ausgeführten Methoden geprägt wird, müssen alle Kombinationen der Methodenausführungsfolge erprobt werden.

Nur dadurch ist sicher zu stellen, dass die Methoden aufeinander abgestimmt sind (Abbildung 1.9). Dennoch wirkt sich die Kapselung für die Testbarkeit eher positiv aus, auch wenn die Kontrolle der Objektzustände dadurch erschwert wird. Die Kap-selung sperrt nämlich die Sicht auf die Zwischenergebnisse. Tester sind gezwungen, über Umwege wie z.B. Friend-Funktionen in C++ auf die Objekte zuzugreifen.

Insofern hat Kapselung auch eine (geringe) negative Wirkung auf die Testbarkeit ([Str92] [Jun99]).

Spezifikation

Prüfung Zustände 123n

­ Alle Zustände des eingekapselten Objekts müssen spezifiziert werden.

­ Alle Zustände des eingekapselten Objekts müssen validiert werden.

­ Der Zugriff auf die Objektzustände wird aber verwehrt.

Objekt

Assert

Abbildung 1.9 Folgen der Kapselung

1.4.4 Folgen der Vererbung

Bei der Vererbung sieht dies anders aus. Vererbung ist ein zweischneidiges Schwert. Man kann den Gegner – die Komplexität – damit bekämpfen, aber man kann sich auch selbst damit verletzen. Auf der einen Seite ist es positiv anzusehen, dass dadurch weniger Code entsteht. Die Unterklassen verweisen auf die Attribute und Funktionen der Oberklassen. Wenn diese stimmen, ist die Welt in Ordnung,

aber wehe wenn sie nicht stimmen. Probleme in den Basisklassen werden auf alle abgeleiteten (Unter-)Klassen übertragen, d.h. auch die Fehler werden vererbt. Ba-sisklassen dürfen auch nicht verändert werden, denn jede Änderung kann zu neuen Fehlern in den abhängigen Klassen führen (Abbildung 1.10). Daraus folgt, dass die Basis- und übergeordneten Klassen im voraus sehr gut getestet werden müssen und dass sie nachher stabil bleiben müssen. Dies stellt hohe Anforderungen an die Ent-wickler dieser Klassen. Anforderungen, denen die meisten AnwendungsentEnt-wickler nicht gewachsen sind.

Es kann sein, dass Klassen in einer Generalisierungshierarchie so sehr voneinander abhängig sind, das es kaum möglich ist, einzelne Klassen allein für sich zu testen.

In diesem Falle ist nicht die Klasse, sondern die Klassenmenge bzw. die Teilhierar-chie der Testgegenstand. Hier wird nicht die einzelne Klasse, sondern die Klassen-hierarchie als Modul behandelt und entsprechend getestet. Objektorientierte Syste-me können sehr große Klassenhierarchien beinhalten, in denen alles von allem abhängt. Die untersten Klassen erben von bzw. verweisen auf die weiter oben in der Hierarchie stehenden Klassen. Dem Tester bleibt nichts anderes übrig, als alles zusammen zu testen. Dies ist oft der Fall bei Smalltalk-Systemen. Für den Test und auch für die Wartung ist dies eine denkbar ungünstige Situation, die, wenn möglich, zu vermeiden ist.

Vererbung ist in der Tat die GOTO-Anweisung der Objektorientierung. Sie gewährt abgeleiteten Klassen (Unterklassen) den direkten Zugriff auf Elemente der ihnen übergeordneten Klassen (Oberklassen) und durchbricht damit die Kapselungsmau-ern. Durch das Überschreiben geerbter Funktionen (overloading) wird die Komple-xität der Abläufe erhöht, durch das Ändern von Parametertypen (Overriding) wer-den die Schnittstellen verschleiert. Vererbung bewirkt eine automatische Wieder-verwendung, auch dort, wo sie nicht beabsichtigt ist. Deshalb muss sie mit großer Vorsicht angewendet werden, da sie sonst nicht nur den Test erschwert, sondern auch zu schwer erkennbaren Fehlern führt [Rya97].

Die beiden Forscher Perry und Kaiser fassen zusammen:

„Inheritance is one of the primary strengths of object-oriented programming. How-ever, it is precisely because of inheritance that we have found so many problems aris-ing with respect to testaris-ing....Encapsulation together with inheritance, which intuitively ought to bring a reduction in testing problems, compounds them instead.” [PeKa90]

Vererbung hat, was die Testbarkeit betrifft, einen weiteren entscheidenden Nachteil.

Es ist damit nicht mehr möglich, die untergeordneten Klassen unabhängig von den übergeordneten Klassen zu testen. Eine im objektorientierten Sinne gut entwickelte Klasse besteht fast nur aus Verweisen auf eine oder mehrere Ahnenklassen. Klasse

A erbt von Klasse B und Klasse B erbt von Klasse C usw. Wer Klasse A testen will, muss die Klassen B und C usw. mit testen. Zum Schluss wird die Hälfte des Systems in den Klassentest einbezogen. Wo bleibt dann der Modultest? Was ist dann ein Modul – am Ende eine ganze Klassenhierarchie? Dies führt dazu, dass in

komple-1.4 Besonderheiten objektorientierter Systeme _______________________________29

xen Systemen mit einer hohen Vererbungstiefe die untergeordneten Klassen kaum testbar sind, zumindest nicht im Sinne eines Modultests. Auf jeden Fall stellt dies hohe Anforderungen an den Klassentestrahmen.

Abgeleitete Klassen sind abhängig von den Basisklassen Spezifische Funktion/Methode

Allgemeine Funktion/Methode Basisklasse

Unterklasse Änderungen Änderungen zu Basisklassen

betreffen Unterklassen Allgemeine Datenbeschreibung

Spezifische Datenbeschreibung

Auch Fehler werden vererbt

Abgeleitete Klassen sind abhängig von den Basisklassen Spezifische Funktion/Methode

Allgemeine Funktion/Methode Basisklasse

Unterklasse Änderungen Änderungen zu Basisklassen

betreffen Unterklassen Allgemeine Datenbeschreibung

Spezifische Datenbeschreibung

Auch Fehler werden vererbt

Vererbung

Abbildung 1.10 Folgen der Vererbung

1.4.5 Folgen der Polymorphie

Die Polymorphie stellt nicht weniger Probleme für den Tester dar. Die Entschei-dung zur Laufzeit, welche Funktion in welchem Objekt einen Auftrag zu erledigen hat, macht aus dem Programmablauf einen nicht unmittelbar aus dem Quellcode herleitbaren Vorgang (Abbildung 1.11). Da die Ablauffolge nicht statisch voraus-sagbar ist, müssen alle möglichen dynamischen Folgen erprobt werden. Wenn die Polymorphie mehrfach wiederholt wird, explodiert die mögliche Anzahl der Ab-laufpfade. Es wird kaum möglich sein, alle potenziellen Ablaufpfade zu testen. Man wird sich auf repräsentative Funktionsfolgen beschränken müssen, d.h. es bleibt die Unsicherheit, ob alle potenziellen Pfade durch das System wirklich funktionieren.

Jede mögliche dynamische Bindung einer Nachricht stellt einen weiteren Ablauf-pfad dar. Die Tatsache, dass ein Pfad funktioniert, ist noch lange keine Garantie, dass die anderen Pfade auch korrekt sind. Jede einzelne Bindung muss für sich getestet werden. Außerdem bringt die Polymorphie eine Vielzahl neuer Fehlerquel-len mit sich, die in der Literatur hinlänglich dokumentiert sind [PoBu94].

Es wäre verkehrt, den Polymorphismus als solchen pauschal zu verdammen, aber mit ihm muss ebenso wie mit der Vererbung äußerst diszipliniert und vorsichtig umgegangen werden. Nur so lassen sich potenzielle Fehler vermeiden und der Test-aufwand in Grenzen halten.

Client Klasse

Server-klasse

Server-klasse

Server-klasse Auftrag an Validate

Potenzieller Server

Alle Serverklassen, die eine Anforderung befriedigen könnten, müssten getestet werden.

Client Klasse

Server-klasse

Server-klasse

Server-klasse Auftrag an Validate

Potenzieller Server

Alle Serverklassen, die eine Anforderung befriedigen könnten, müssten getestet werden.

A :: Validate B :: Validate C :: Validate

Abbildung 1.11 Folgen der Polymorphie

Im Dokument Das Praxishandbuch für den Test (Seite 35-44)