Übersicht Inhaltsverzeichnis Vorwort 1 Einführung 2 Agile und UML-basierte Methodik 3 Kompakte Übersicht zur UML/P 4 Prinzipien der Codegenerierung 5 Transformationen für die Codegenerierung 6 Grundlagen des Testens 6.1 Einführung in die Testproblematik 6.2 Definition von Testfällen 7 Modellbasierte Tests 8 Testmuster im Einsatz 9 Refactoring als Modelltransformation 10 Refactoring von Modellen 11 Zusammenfassung und Ausblick Literatur |
6.1 Einführung in die TestproblematikZur Sicherung oder Verbesserung der Qualität von Software gibt es eine Reihe von Verfahren, die während eines Softwareentwicklungsprojekts oder projektübergreifend zum Einsatz kommen können. „Qualität“ ist ein viel gebrauchter Begriff, der jedoch nur schwer zu definieren ist. In [Lig90] werden eine Reihe von Qualitätseigenschaften genannt, von denen unter anderem die zur Produktnutzung wichtigen Eigenschaften funktionale Korrektheit und Robustheit durch Tests überprüft werden können. Weitere Qualitätseigenschaften wie Laufzeit- und Speichereffizienz lassen sich durch statistische Tests zumindest in eingeschränkter Form prüfen. 6.1.1 TestbegriffeAbbildung 6.1 enthält mehrere Definitionen des Begriffs „Test“ aus der Literatur.
In der Literatur sind unterschiedliche Definitionen für den vielfach gebrauchten Begriff Test und die
Tätigkeit Testen zu finden (teilweise ins Deutsche übersetzt):
Typisch für den Extreme Programming-Ansatz ist das Fehlen einer eigenschaftsorientierten Definition des Begriffs „Test“. Stattdessen wird eine operative Beschreibung angegeben, die gleichzeitig festlegt, wer Tests entwickelt, und sich dabei auf zwei einfache Testarten einschränkt [AM01]. Besonders die Definition von [Bin99] ist als Grundlage für dieses Buch geeignet. Daraus lassen sich die in Abbildung 6.2 zusammengestellten Charakteristika für Tests ableiten, die in diesem Buch als Grundlage dienen. Es gibt allerdings eine Reihe von Ausnahmen dieser Charakterisierung, die ebenfalls als Tests bezeichnet werden. Dazu gehören manuelle, interaktive Tests, die zum Beispiel durch den Anwender beim Abnahmetest durchgeführt werden. Eine weitere Alternative ist das symbolische Testen, bei dem symbolische Berechnungen stattfinden, damit also das zu testende System nicht wirklich „abläuft“. Lasttests in real verteilten Systemen produzieren typischerweise nicht immer dieselben Ergebnisse, sondern ermitteln durch wiederholte Ausführung Durchschnittswerte. All diese Testarten sind nicht Gegenstand dieses Kapitels. Die Definition vollständig automatisierter Tests gewinnt in den letzten Jahren immer mehr an Gewicht. So enthält [FG99] eine ausführliche Motivation und eine detaillierte Abgrenzung zu semi-automatischen oder manuellen Vorgehensweisen.
Wichtig ist auch die Unterscheidung zwischen der Aktivität des Testens mit dem Ziel der Fehlererkennung und der Fehlerbehebung, die nach dem Testen folgt. Weitere Maßnahmen zur Qualitätssicherung sind die Code-Inspektion, in der der Quellcode von Entwicklern auf mögliche Fehler untersucht wird, und die Verifikation, die für industrierelevante Systeme normalerweise (oder besser gesagt „noch“) nicht durchführbar ist, aber im Gegensatz zum Test die vollständige Korrektheit einer Implementierung gegenüber einer Spezifikation nachweisen könnte. Tabelle 6.3 ordnet Testbegriffe nach der Art des zu testenden Systemelements und charakterisiert, wer normalerweise diese Tests entwerfen und durchführen sollte. Je nach verwendetem Entwicklungsprozess liegt die Gewichtung der Testentwicklung mehr auf einem eigenständigen Testteam oder bei den Entwicklern selbst. 6.1.2 Ziele der TestaktivitätenIm Portfolio der Qualitätssicherungsmaßnahmen spielen Tests eine wesentliche Rolle, da eine systematische, zielgerichtete Erstellung und Durchführung automatisierter Tests mit vertretbarem Aufwand möglich ist. Testcode übertrifft meist die Größenordnung von dem zu testenden Code, jedoch ist er deutlich einfacher strukturiert und im Gegensatz zum Produktionscode ist seine Eleganz und Redundanzfreiheit deutlich weniger wichtig. So ist es akzeptabel, dass Tests im Copy-und-Paste-Verfahren entwickelt werden [Den91, Fow99], obwohl es sich auch bei Tests lohnt, wiederverwendbare Abstraktionen in eigene Methoden auszulagern. Da das Auktionssystem mit hohen Geldbeträgen arbeitet und strengen zeitlichen Rahmenbedingungen unterliegt, wurde dort die notwendige Qualität in Bezug auf Fehlerfreiheit der Software besonders hoch eingeschätzt. Dementsprechend ist 63% des insgesamt entwickelten Codes Teil des Testsystems.1 Die zusätzlichen Kosten für die Entwicklung und Wartung einer automatisierten Testsammlung sind dementsprechend vertretbar. Eine Untersuchung in [KFN93] schätzt ab, dass der Break-Even zwischen Kosten und Nutzen automatisierter Tests gegenüber etwa zehn manuellen Testdurchgängen erreicht wird. Bei sich dynamisch weiterentwickelnden Systemen, bei denen auch nach einer Installation noch Änderungen vorgenommen werden sind zehn manuelle Testdurchgänge schnell erreicht. Durch die mittlerweile zur Verfügung stehenden Werkzeuge und Frameworks wie JUnit [JUn11, BG98, BG99] steigt außerdem die Effizienz der Testfalldefinition, so dass der Break-Even weiter gesenkt werden dürfte. Vorteile automatisierter Tests sind:
Die Entwicklung von Tests ist zielorientiert. Während Tests für einzelne Methoden, Klassen und kleine Subsysteme vor allem dazu dienen, Fehler zu entdecken und für eine Behebung herauszuarbeiten und damit zu dokumentieren, dienen Integrations- und Systemtests vor allem dazu, zu demonstrieren, dass Fehler (weitgehend) abwesend sind und dass das implementierte System sich den vorgegebenen Beschreibungen/Spezifikationen gemäß verhält. 6.1.3 FehlerkategorienIn einem Softwaresystem können mehrere Fehlerkategorien unterschieden werden. Abbildung 6.4 enthält eine Begriffsbestimmung für die wichtigsten Fehlerkategorien.
Ein Mangel in der Software drückt sich dadurch aus, dass er bei Ausführung des mangelhaften Codes zu einem Versagen des Softwaresystems führen kann. Mit einem Test lässt sich das Versagen von Software in der Testsituation erkennen und darauf zurückschließen, dass die Software einen Mangel besitzt. Ein Versagen kann auf eine Kombination von Mängeln zurückzuführen sein. Umgekehrt kann derselbe Mangel zu unterschiedlichen Formen des Versagens führen, so dass mitunter detektivische Arbeit notwendig ist, um den Ort eines Mangels einzugrenzen. Zum einen kann dies durch Debugging mit manueller Verfolgung von Einzelschritten im System erfolgen. Besser ist es jedoch, Tests auf jeder Ebene des Systems verfügbar zu haben, so dass ein Mangel bereits bei der kleinsten möglichen Systemkonfiguration erkannt wird. Durch die Definition weiterer Tests kann ein Mangel gezielt lokalisiert werden. Oft wird auch der Begriff Bug als Oberbegriff für Versagen und Fehler definiert.2 Auslassungen und Überraschungen lassen sich nicht durch automatisierte Tests feststellen. Für die Erkennung von Auslassungen sind formale oder informelle Beschreibungen der geforderten Funktionalität notwendig, die für eine statische Analyse oder einen vergleichenden Review verwendet werden. Beispielsweise werden Auslassungen durch nicht übersetzbare Programme und durch Abnahmetests entdeckt. Überraschungen sind demgegenüber weniger problematisch. Sie führen zwar bei der Systementwicklung zu unnötigem Mehraufwand, stören aber die wesentliche Funktionalität nicht. Statische Analysen können zum Beispiel unerreichbaren Code in einer Methode erkennen. 6.1.4 Begriffsbestimmung für TestverfahrenIm Kontext von Tests gibt es eine Reihe weiterer Begriffe, die teilweise auch in diesem Kapitel bereits verwendet wurden und noch einer Klärung bedürfen. Abbildung 6.5 beschreibt die wesentlichsten in Kurzform.
Der Begriff „Testling“ ist zwar ein Kunstwort, trifft aber die Bedeutung des zu testenden Systems beziehungsweise der Systemkomponente, weshalb in diesem Buch dieser Begriff von [Den91] übernommen wird.4 Besonderer Beachtung bedarf, dass das Scheitern eines Tests bedeutet, dass Test und Implementierung nicht konform sind oder während des Tests eine unerwartete Exception aufgetreten ist. Damit ist der Test oder die Implementierung mangelhaft, dies bleibt zu klären. In [Mye79] wird darauf hingewiesen, dass ein in diesem Sinn gescheiterter Test seinen Testzweck, nämlich die Fehlerfindung erfüllt hat und somit als Erfolg für den Test gewertet werden kann. 6.1.5 Suche geeigneter TestdatenEin wesentlicher Problemkreis beim Testen ist die effiziente Entwicklung einer systematischen und alle wesentlichen Fälle überdeckenden Sammlung von Testfällen. Sind für einen Testfall die Testdaten gegeben, so ist das Sollergebnis meist aus der Spezifikation abgeleitet oder vom Entwickler beziehungsweise Anwender festgelegt worden. Die Vorgabe eines Sollergebnisses kann aufwändig werden und ist fehleranfällig. Jedoch testen sich das Produktionssystem und die Testsammlung gegenseitig, so dass auch Fehler in Testfällen erkannt und behoben werden können. Als wesentliche Schwierigkeit bleibt daher die Identifikation geeigneter Testdaten und die Festlegung, wieviele Testfälle für eine adäquate Sammlung von Tests ausreichend sind. Bereits in [Mye79] wurden Heuristiken und Verfahren zur Entwicklung von Testdatensätzen beschrieben, die in [Lig90] und [Bei04] in verfeinerter Form diskutiert werden. Dazu gehören Verfahren, die sich am Kontrollfluss der Implementierung orientieren, indem sie alle Anweisungen, Verzweigungen, Bedingungsvariationen oder Pfade innerhalb einer Methode nach bestimmten Kriterien überdecken. Andere Verfahren identifizieren zusätzlich Äquivalenzklassen von Testdaten und Grenzwertbereiche. Datenflussorientierte Testverfahren nutzen Attribut- und Variablenzugriffe, um Testdaten zu entwickeln. Den kontrollfluss- und datenflussorientierten Verfahren ist gemeinsam, dass sie die Implementierung des Systems als bekannt voraussetzen und die Erstellung der Testfälle basierend auf der Analyse der Implementierung beruht. Demgegenüber steht die Klasse der funktionalen oder spezifikationsbasierten Tests. Sie basieren nicht auf der Implementierung, sondern einer Spezifikation und prüfen die funktionalen Eigenschaften eines Systems. Sie erkennen also Konformitätsfehler des Systems, beziehungsweise demonstrieren die Übereinstimmung zu der spezifizierten Funktionalität. Zur Bestimmung der Qualität einer Sammlung von Tests werden Metriken verwendet, die eine Testüberdeckung nach verschiedenen Kriterien messen. Jedoch ist auch eine vollständige Testüberdeckung nach diesen Metriken keine Garantie für ein korrektes System. Deshalb wird in der Praxis von der eher dogmatischen Testtheorie verstärkt zu einer erfahrungsgetriebenen, zum Beispiel durch Testmuster [Bin99], Checklisten [PKS02] und „Best Practices“ beschriebenen Vorgehensweise übergegangen. Es ist den pragmatischen Testmustern anzumerken, dass Elemente der Testtheorie Eingang gefunden haben, ohne jedoch dogmatisch deren Erfüllung zu 100% zu fordern. So kann zum Beispiel aus dem Extreme Programming-Ansatz gefolgert werden, dass die Anweisungsüberdeckung als Minimalziel gefordert und nach Möglichkeit eine minimale Pfadüberdeckung gewünscht ist. 6.1.6 Sprachspezifische FehlerquellenEine zu obigen Punkten orthogonale Fehlerkategorisierung ergibt sich aus der Frage, ob ein System zum einen robust und zum anderen konform zur Spezifikation ist. Die Robustheit einer Implementierung kann durch anormale Abstürze (Exceptions) aufgrund von nicht initialisierten Attributen, Referenzen auf nicht existierende Objekte und ähnlichen Problemen gestört werden. Typisch für Mängel in der Robustheit ist, dass nicht gegen eine Spezifikation getestet wird, sondern sprachspezifische Fehlerquellen zu eliminieren sind. C++ ist ein Paradebeispiel für außerordentlich viele Fehlerquellen, die aus der hohen Anzahl von ungesicherten C++-Konstrukten resultieren. Die dem Entwickler überlassene Speicherverwaltung, die Zeigerarithmetik und ungeprüfte Zugriffe auf Felder sind nur einige der möglichen Fehlerquellen. Java ist, obwohl syntaktisch der Sprache C++ ähnlich, in Bezug auf derartige Fehlerquellen wesentlich robuster. Viele Fehlerquellen werden in Java durch restriktive Kontextbedingungen in der Sprache und damit bereits durch statische Analysen eliminierbar. Zum Beispiel wird durch eine ausgefeilte Datenflussanalyse [GJSB05] geprüft, ob Variablen besetzt wurden, bevor sie benutzt werden. Die ESC/Java-Erweiterung [RLNS00] um Zusicherungen erlaubt eine noch weitergehende statische Analyse, erfordert jedoch eine detaillierte Beschreibung von Zusicherungen im Java-Code. Dennoch kann dadurch die Anzahl der Fehlerquellen weiter reduziert werden. Weitere Java-Fehlerquellen werden durch Laufzeitüberprüfungen entdeckt und durch Exceptions dem Programm gemeldet. Dazu gehören zum Beispiel das Überschreiten von Arraygrenzen, die Division durch 0 oder illegale Typkonversionen. Damit lässt sich ein Programm relativ leicht robust gestalten. Jedoch sollte die dafür notwendige Verwendung von Exceptions soweit wie möglich begrenzt werden, da die Verarbeitung von Exceptions Charakteristika der goto-Anweisung aufweist und leicht zu unübersichtlichem Code führt. Als generelles Prinzip sollten nur externe Fehlerquellen, wie eine nicht vorhandene Datei, eine nicht erreichbare Datenbank oder eine abgebrochene Internet-Verbindung, mit Exceptions behandelt werden und intern zu verantwortende Fehlerquellen, wie Division durch 0 oder falsche Arraygrenzen durch explizite Abfragen abgesichert werden. Unabhängig von der Art des Abfangens solcher Fehler ist eine robuste Behandlung des Fehlers und dementsprechender Tests notwendig, die demonstrieren, dass der Fehler korrekt behandelt wird. Auch dafür ist es sinnvoll, möglichst wenig Exceptions durch die Aufrufhierarchie verfolgen zu müssen. Obwohl Java gegenüber C++ sehr viel sicherer entworfen wurde, gibt es auch in Java eine Reihe von Fehlerquellen. Von diesen Fehlerquellen können viele durch restriktive Programmierung verhindert werden. So sollte in Java ein Attribut der Oberklasse nicht verschattet werden, indem in der Unterklasse ein gleichnamiges Attribut definiert wird. Objektorientierte Programme haben mit ihrer hohen Dynamik, der Vererbungshierarchie und des dynamischen Bindens von Methoden eine deutlich höhere Komplexität, als dies noch bei prozeduralen Sprachen der Fall war. Objektorientierte Methoden sind meist sehr viel kleiner als das Prozeduren waren und interagieren stärker mit anderen Methoden. Dadurch entsteht zum Beispiel die in Frameworks so wichtige Flexibilität durch Adaption von Methoden in Unterklassen [FPR01, FSJ99]. Jedoch erfordert diese Möglichkeit zur Redefinition zusätzlichen Aufwand beim Test. Insbesondere reicht es nicht mehr, nur innerhalb einer Methode alle möglichen Kontrollflüsse zu testen, sondern es müssen alle potentiellen Konstellationen der Zusammenarbeit von Methoden in allen Unterklassen geprüft werden. Die Aufgabe potenziert sich, wenn mehrere Objekte kollaborieren, von denen jedes aus einer von mehreren Unterklassen stammen kann. Es ist daher oft nicht mehr durchführbar, Tests, die alle Kombinationen von Methodenaufrufen und Objektstrukturen überdecken, zu entwickeln. Da UML/P als Zielsprache Java nutzt, sind die in Java vorhandenen Probleme weitgehend in der UML/P wiederzufinden. Eine Ausnahme ist zum Beispiel die erwähnte Verschattung von Attributen, die in UML aufgrund entsprechender Kontextbedingungen nicht dargestellt werden kann und daher bei einer Codegenerierung nach Java nicht entsteht. Bei einer konstruktiven Nutzung von UML-Diagrammen zur Codegenerierung entstehen für das resultierende Programm eine Reihe von Möglichkeiten, die vorgegebene Spezifikation zu verletzen. Je nach Form der Generierung und der dabei umgesetzten Konzepte, werden bestimmte Fehler der zugrunde liegenden Sprache Java vermieden oder neue Probleme eingeführt. Beispielsweise werden gemäß der in Abschnitt 5.1.3 angegebenen Transformation einschränkende Kardinalitäten von Assoziationen normalerweise nicht konstruktiv umgesetzt. Dadurch kann eine Verletzung der Invarianten auftreten, die nur dadurch verhindert werden kann, dass die Umgebung der Assoziation die Invariante beachtet. Dies ist in Tests zu prüfen. Ähnlich werden Zustands- oder Nachbedingungen in Statecharts nicht notwendig konstruktiv sichergestellt und sind daher zu testen. Andererseits kann, wie in Abschnitt 5.1.3 gezeigt, durch Generierung geeigneter Funktionalität beispielsweise sichergestellt werden, dass eine bidirektionale Assoziation immer konsistent ist, so dass dadurch Tests entfallen können. Die Definition von sprachspezifischen Tests, die Robustheitsfehler erkennen können, hängt daher wesentlich von der Form des generierten Codes ab. Da der Codegenerator in wesentlichen Elementen parametrisierbar sein muss, ist damit eine Vorhersage, welche sprachspezifischen Tests für UML/P notwendig sind, schwer durchführbar. Es ist daher hilfreich, für Stellen, an denen ein Generator es nicht verhindert, Invarianten zu verletzen, geeignete Testfälle zu deren Prüfung zu definieren. 6.1.7 UML/P als Test- und ImplementierungsspracheWie in Abschnitt 4.1 diskutiert, kann die UML/P im Softwareentwicklungsprozess mehrere Rollen einnehmen. Sie ist gleichzeitig als Implementierungssprache und als Sprache zur Definition von Tests geeignet. Damit übernimmt sie ähnliche Aufgabenstellungen wie die jeweils benutzte Programmiersprache in Extreme Programming-Projekten. Dort werden Tests und Implementierung ebenfalls in derselben Sprache formuliert. Erfahrungen mit der UML als Sprache zur Testmodellierung zeigen außerdem, dass die Effizienz der Entwickler verbessert wird [BMJ01, BPR04]. Jedoch ist es wichtig, die UML in einer testbaren Form einzusetzen [BL01, Rum03]. Unter Testbarkeit wird generell die Fähigkeit verstanden, aus dem Modell Tests abzuleiten oder – idealerweise – automatisch zu generieren. Wird die UML/P als Implementierungssprache verwendet, so ist für eine systematische Testentwicklung die Kenntnis notwendig, welche sprachspezifischen und typisch objektorientierten Probleme die UML/P behebt, aber auch mit sich bringt. Ein typisches Problem der Umsetzung von bidirektionalen Assoziationen ist die Wahrung der Konsistenz zwischen den Attributen auf beiden Seiten, die die Assoziation speichern. Bei der Generierung von Code aus einem Klassendiagramm, der nicht mehr manuell verändert werden darf, kann diese Konsistenz durch den in Abschnitt 5.1.3 beschriebenen Code konstruktiv gesichert werden. Sie muss also nicht mehr durch Tests geprüft werden. Andererseits führt die Verwendung eines Attributs des referenzierten Objekts als Qualifikator in einer qualifizierten Assoziation Redundanz ein, die bei Änderung des Attributwerts zur Inkonsistenz führen kann. Eine statische Analyse des Codes kann feststellen, ob eine Änderung dieses Attributs überhaupt stattfindet. Ist das jedoch der Fall, so sind dynamische Tests notwendig, um festzustellen, ob dadurch die Konsistenz für eine qualifizierte Assoziation verletzt wird. Um also in der UML/P formulierte Implementierungen auf ihre Robustheit zu prüfen, ist es notwendig die Notationen der UML/P und ihre Umsetzung selbst einer kritischen Analyse auf mögliche Fehlerquellen zu untersuchen. Dabei ist zu beachten, dass die UML/P nicht nur aus mehreren Diagrammarten und der OCL besteht, sondern auch Java-Code explizit als Methodenrümpfe und als prozedurale Aktionen in Statecharts erlaubt. Dadurch bleiben wie bereits erwähnt viele der für Java typischen Fehlerquellen erhalten. Die potentiellen Fehlerquellen der Implementierungssprache UML/P hängen aber weitgehend von der konkreten Umsetzung durch den parametrisierten Codegenerator ab und können daher nicht allgemein diskutiert werden. Die Untersuchung nach UML/P-Fehlerquellen beinhaltet wie im obigen Beispiel der Assoziationen auch die Frage, wie diese Fehlerquellen zu behandeln sind. Dazu gibt es fünf wesentliche Strategien:
Der letzte Punkt ist eigentlich nicht notwendig, wenn der Codegenerator korrekt funktioniert. Jedoch ist ein Codegenerator, wie in Kapitel 4 beschrieben, parametrisiert. Das heißt, Skripte können die Codegenerierung in einer flexiblen Weise steuern. Möglicherweise wird dadurch Code generiert, der beispielsweise die Konsistenz der bidirektionalen Assoziation nicht durch geeignete Maßnahmen sicherstellt oder sogar selbst verletzt. Das bedeutet, dass Tests für solche Zwecke typischerweise den Codegenerator beziehungsweise seine Skripte prüfen und damit ihre Berechtigung haben. Es ist daher für jedes Projekt einzeln zu entscheiden, welche Teile des Systems wie intensiv getestet werden. Dies hängt natürlich auch von den Projektzielen, der zu erreichenden Qualität und der Einsatzform des Produkts ab. Bereits Abschnitt 4.1.2 diskutiert vom Standpunkt der Codegenerierung, welche UML/P-Teile sich konstruktiv oder für Tests einsetzen lassen. Nicht direkt in konstruktiven Code übersetzbare Objektdiagramme, OCL-Bedingungen und Statecharts bilden dennoch nicht notwendigerweise Testmodelle, die vom Codegenerator zur Generierung von Tests verwendet werden können.5 Abbildung 4.3 skizziert den typischen Einsatz von UML/P-Diagrammen für Tests und Implementierung. Während der Produktionscode ein vollständiges System darstellt, ist der Testcode nur in Kombination mit dem Produktionscode lauffähig. Der Produktionscode wird außerdem für den Einsatz im Test instrumentiert. Das heißt, er wird durch zusätzliche Codestücke erweitert, damit der Testcode auch während des Testablaufs auf alle notwendigen Informationen zugreifen kann. Dazu gehören zum Beispiel spezielle Funktionen zum Besetzen und Auslesen gekapselter Attribute, wenn die entsprechenden get- und set-Funktionen nicht standardmäßig zur Verfügung stehen oder zusätzliche Effekte haben können, die zum Beispiel zur Erhaltung der Konsistenz zwischen mehreren Attributen dienen. So wird Code eingefügt, der Invarianten und OCL-Methodenspezifikationen zur Laufzeit prüft sowie die Protokollierung von Methodenaufrufen zum Vergleich mit vorgegebenen Aufrufreihenfolgen ermöglicht. Bei konventioneller Programmierung mit Java müssen für solche Aufgaben Testmonitore oder Adapter entwickelt werden [Wil01], die jedoch nur einen begrenzten Zugang zu den Testlingen haben. Die Instrumentierung darf das funktionale Verhalten des Produktionscodes nicht verändern, weshalb es verboten ist, in OCL-Bedingungen modifizierende Methoden einzusetzen. Nur so ist gewährleistet, dass der für die Freigabe bestimmte, nicht instrumentierte Produktionscode dasselbe Verhalten besitzt wie der getestete Code. Es kann notwendig sein, denselben Produktionscode für verschiedene Tests unterschiedlich zu instrumentieren. Zum Beispiel sind nur für manche Tests Aufrufreihenfolgen irrelevant. Auch kann es sehr ineffizient werden, wenn alle OCL-Invarianten in allen Tests geprüft werden. Dauert die Ausführung von Tests zu lange, so werden diese unpraktikabel. Die Instrumentierung ist daher entweder abhängig vom gerade ausgeführten Test zu individualisieren oder durch boolesche Flags zur Laufzeit parametrisieren. Letzteres hat den Nachteil, dass dadurch der instrumentierte Code groß werden kann, aber den Vorteil, dass keine wiederholte Codegenerierung und Übersetzung des generierten Codes notwendig ist. Die Besetzung der Flags kann durch den Testtreiber direkt oder durch Stereotypen in der Testbeschreibung festgelegt werden. Aufgrund der stetig leistungsfähiger werdenden Rechner und der Effizienz guter Compiler kann davon ausgegangen werden, dass die Codeinstrumentierung für Testzwecke und die damit mögliche Simulation von Umgebung und Verteilung sowie die dynamische Prüfung von Invarianten, Vor- und Nachbedingungen im Produktionscode praktikabel sind. Der instrumentierte Produktionscode und der Testcode testen sich gegenseitig. Ist ein Testfall gescheitert, so kann der Testling, aber auch der Testfall selbst fehlerhaft sein. In der auch im Auktionsprojekt beobachteten Praxis sind deutlich häufiger die Testfälle selbst fehlerhaft, zum Beispiel weil eine Änderung in der Funktionalität einer Methode im Testfall nicht adäquat nachgezogen wurde. Unangenehm wird es wenn beide, der Testling und der Testfall, konsistent falsch sind und fälschlicherweise ein Testerfolg gemeldet wird. Dieses Problem tritt besonders dann auf, wenn ein Entwickler sowohl den Produktionscode als auch den Test entwirft und dabei einen Denkfehler wiederholt. Dem wird zum Beispiel im Extreme Programming-Ansatz dadurch entgegnet, dass zwei Entwickler gleichzeitig und gemeinsam am Code arbeiten. Zusätzlich sollten Tests der darüber liegenden Schichten bzw. Integrationsstufen in der Lage sein, einen solchen Fehler dennoch zu entdecken. 6.1.8 Eine Notation für die TestfalldefinitionFür bestimmte Zwecke werden auch eigenständige Testnotationen verwendet. Die Telekommunikationsindustrie nutzt zum Beispiel vorrangig TTCN [ISO92, GS02] in Kombination mit MSCs [IT11, Krü00] und SDL [IT07b]. Die Verwendung einer speziellen Testnotation wird als Vorteil gegenüber der Benutzung einer Programmiersprache zur Definition von Testtreibern empfunden, weil sie abstrakter und damit übersichtlicher ist und Veränderungen der Funktionalität damit leichter in den Tests nachgezogen werden können. Dies ist allerdings nur richtig, wenn die in einer Programmiersprache wie Java implementierten Tests von einer gut entworfenen Bibliothek von Hilfsfunktionen unterstützt werden. Bei der Verwendung einer abstrakten Testnotation ist viel Aufwand in die Entwicklung des Testwerkzeugs zur Interpretation der Testnotation und in die Ansteuerung von Soft- und Hardwarekomponenten sowie die Schulung der Entwickler zu investieren. Bei der Verwendung der Programmiersprache Java zur Testfalldefinition spiegelt sich dieser Aufwand in der Benutzung des Frameworks wider, das die implementierten Hilfsfunktionen zur Verfügung stellt. Damit haben beide Vorgehensweisen einiges gemeinsam. Jedoch hat die Verwendung derselben Programmiersprache für Tests und Implementierung einige Vorteile gegenüber der Verwendung einer eigenständigen Testnotation:
Aus diesen Gründen ist es nicht verwunderlich, dass in vielen und insbesondere agilen Softwareentwicklungsprojekten für die Realisierung von Tests dieselbe Notation verwendet wird, wie zur Programmierung. In Projekten, die UML/P, also eine Kombination aus Modellierungstechniken der UML und Java nutzen, ist es deshalb von Vorteil, für die Modellierung von Tests ebenfalls UML/P einzusetzen:
Die UML/P vereinigt also die Vorteile einer abstrakten Notation für Testfälle mit der guten Integration von Test- und Implementierungssprache. Durch diese integrierte Verwendung der UML/P ist es tatsächlich realistisch, in kurzer Zeit parallel zur Entwicklung eine ausreichende Testsammlung zu erstellen, die die Qualität der erstellten Software demonstriert. Deshalb ist es nicht überraschend, dass nicht nur bei der Entwicklung von Geschäftssoftware, sondern auch bei der Entwicklung eingebetteter Systeme, wie etwa bei Telekommunikationssystemen, verstärkt Java parallel zu Testnotationen, wie etwa MSC, zum Einsatz kommen.6 Da ein Test meistens aus mehreren UML-Diagrammen besteht, ist dennoch ein Stück zusätzlicher Syntax notwendig, um Tests in kompakter Form zu definieren. Der hier dargestellte Vorschlag beschränkt sich auf die Referenzierung von UML-Diagrammen, OCL-Bedingungen und der Einbindung von Java-Code, um Teile des Tests zu formulieren. Abbildung 6.6 zeigt das vollständige Schema für die Definition von Tests. Jeweils ungenutzte Anteile können entfallen.
Die einzelnen Bestandteile werden im weiteren Verlauf dieses Kapitels diskutiert. Dabei wird auch eine tabellenartige Variante dieses Schemas verwendet, die für die übersichtliche Definition mehrerer Tests geeignet ist.
|
|||||||||||||||||||||||||||||||