DIPLOMARBEIT Entwicklung einer Software für die
Transcription
DIPLOMARBEIT Entwicklung einer Software für die
DIPLOMARBEIT Entwicklung einer Software für die automatische Generierung und wiederholbare Durchführung von Funktionstests mit .NET Nico Franze diplom@nfranze.de Technische Fachhochschule Berlin Sommersemester 08 Betreuer: Prof. Dipl.-Ing. Detlef Gramm Gutachter: Prof. Dr. Heike Ripphausen-Lipa Kurzzusammenfassung Qualität der Software - was bedeutet das überhaupt? Und wie steigert man die Qualität einer Software? Wo liegen eigentlich die Ursachen der Fehler und Abstürze in Software? Eines der wichtigsten Bestandteile für die Erhöhung der Qualität sind Softwaretests. Das bedeutet aber nicht das Starten der Software. Gute Tests beinhalten wesentlich mehr. In dieser Arbeit werden zu Beginn Softwarefehler vorgestellt und mit berühmt „berüchtigten“ 1 Beispielen belegt. Weiterhin werden die Ursachen der Fehler analysiert. Nach der Vorstellung und Analyse von Fehlern werden die Vor- und Nachteile von Softwaretestsystemen analysiert. Zielstellung der Arbeit ist es, ein eigenes Testsystem zu entwickeln, welches auf der Testmethode Fuzzing beruht. Dieses Testsystem, welches AutoTest.Net genannt wird, ist in der Lage, mithilfe von Reflection eine .NET-Assembly zu analysieren und alle enthaltenen Methoden auszuführen. Jede einzelne Methode wird separat getestet (Funktionstests). Die dafür nötigen Parameter werden aus Äquivalenzklassen generiert. Am Ende eines jeden Tests wird ein Bericht ausgegeben, der die Ergebnisse des Tests präsentiert und detailliert aufschlüsselt, welche Methoden mit welcher Exception abstürzte. Für die Entwicklung dieses Testsystems wird Microsoft .NET 3.5 und Microsoft Visual Studio 2008 verwendet. Das Testsystem ist in der Lage, jegliche Software zu testen, die ebenfalls mit .NET entwickelt wurde. Es konnte gezeigt werden, dass durch diese Testmethode Fehler gefunden werden können, die bisher mit anderen Testmethoden nur sehr schwer gefunden wurden. Somit ist dieses Testsystem im allgemeinen Testzyklus eine optimale Ergänzung. 1 berühmte Negativ-Beispiele ii [K] Abstract Quality in software: what is that really? How can you improve your software? Where are the causes of errors and crashes in software? One of the most important elements for improvement are tests, specifically software tests, but that does not mean simply to start your software. Good tests contain considerably more. At first a few software bugs are presented and documented with notorious samples. Also the causation of these bugs are analysed. After that the advantages and disadvantages of different test systems are analysed. The main goal of this thesis is to develop a custom test system, which is based on the test method Fuzzing. This test system is called AutoTest.Net and is able to decode a .NET-Assembly with Reflection. All containing methods will be executed separately (function tests). All needed parameters are generated from equivalence classes. At the end a report shows in detail which method crashed, with which error, and which parameters were used for the crash. This test system will be developed with Microsoft .NET 3.5 and Microsoft Visual Studio 2008. The test system will have the ability to test all .NET software. It should be shown that this test method can find some errors that cannot be found easily with other test methods. Therefore, this test system will be the optimal final step of the general test cycle. iii [A] Inhaltsverzeichnis Kurzzusammenfassung ii Abstract iii 1 Einleitung 1 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Problemstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 Zielstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3.1 Warum .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3.2 Was ist Reflection . . . . . . . . . . . . . . . . . . . . . . . . . 3 Aufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.4 2 Bekannte Fehler und deren Ursache 6 2.1 Die Einführung des Begriffes „Software-Bug“ . . . . . . . . . . . . . . . 6 2.2 Allgemeines zu Softwarefehlern . . . . . . . . . . . . . . . . . . . . . . 7 2.3 Ursachen und Ausmaße von Softwarefehlern . . . . . . . . . . . . . . . 10 2.4 3 2.3.1 Tippfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.3.2 Umwandlungsfehler . . . . . . . . . . . . . . . . . . . . . . . . 13 2.3.3 Überläufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.3.4 Unterschiedliche Einheiten . . . . . . . . . . . . . . . . . . . . . 16 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Grundlegendes zu Softwaretests 18 3.1 Was ist Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3.2 Qualitätsmerkmale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.3 Blackbox-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.4 Whitebox-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.4.1 Statische Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.4.2 Strukturelle Analyse . . . . . . . . . . . . . . . . . . . . . . . . 25 3.4.3 Überdeckungstests . . . . . . . . . . . . . . . . . . . . . . . . . 26 iv 3.5 3.6 3.7 4 Testverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.5.1 Komponententest . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.5.2 Integrationstest . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.5.3 Systemtest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.5.4 Abnahmetest . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.5.5 Regressionstests . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Konkrete Testverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.6.1 Unit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.6.2 GUI-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.6.3 Fuzz-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.6.4 Datengesteuerte Tests . . . . . . . . . . . . . . . . . . . . . . . . 36 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Vorhandene Testsysteme 4.1 4.2 Test-Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 4.1.1 NUnit / JUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 4.1.2 Fuzzing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Statische Codeanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 4.2.1 4.3 4.4 5 6 38 Microsoft FxCop . . . . . . . . . . . . . . . . . . . . . . . . . . 42 GUI-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 4.3.1 Ranorex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 4.3.2 TestComplete . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Projekt AutoTest.Net 49 5.1 Soll-Zustand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 5.2 Optionale Funktionalitäten . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.3 Aufbau der Bedienoberfläche . . . . . . . . . . . . . . . . . . . . . . . . 55 5.4 Arbeitsablauf in AutoTest.Net . . . . . . . . . . . . . . . . . . . . . . . 57 5.5 Lösungskonzeption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 5.5.1 Äquivalenzklassenverwaltung . . . . . . . . . . . . . . . . . . . 58 5.5.2 Exceptionverwaltung . . . . . . . . . . . . . . . . . . . . . . . . 59 5.5.3 Testdurchführung . . . . . . . . . . . . . . . . . . . . . . . . . . 60 Implementierung des Testsystems 62 6.1 Implementierung der GUI . . . . . . . . . . . . . . . . . . . . . . . . . . 63 6.2 Implementierung der Use-Cases . . . . . . . . . . . . . . . . . . . . . . 65 v 6.3 7 8 6.2.1 Implementierung Use-Case „Laden/Analysieren der Assembly“ . 65 6.2.2 Implementierung Use-Case „Tests durchführen“ . . . . . . . . . 66 6.2.3 Implementierung Use-Case „Ausgabe der Testergebnisse“ . . . . 67 6.2.4 Implementierung Use-Case „Exceptions verwalten“ . . . . . . . . 68 6.2.5 Implementierung Use-Case „Äquivalenzklassen verwalten“ . . . 68 Fertigstellung von AutoTest.Net . . . . . . . . . . . . . . . . . . . . . . 70 Sollanalyse - Testen des Testsystems 72 7.1 Testergebnisse mit selbstentwickelter Test-Assembly . . . . . . . . . . . 72 7.2 Testergebnisse mit System-Assembly . . . . . . . . . . . . . . . . . . . 75 7.3 Vergleich mit NUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 7.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Fazit 77 8.1 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 8.2 Grenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 8.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 8.4 Schlusswort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Literaturverzeichnis I Abbildungsverzeichnis V Tabellenverzeichnis VII Listings VIII Glossar IX Index XII Anhang XIII vi Einleitung [1] 1.1 Software ist ein wesentlicher Bestandteil der modernen Gesellschaft geworden. Kaum eine Anwendung ist heutzutage ohne Software denkbar. Dabei geht Software weit über das hinaus, was als Textverarbeitungs- oder Mailprogramm auf dem heimischen Computer bekannt ist. Fast jedes elektronische oder technische Gerät, vom Fernseher und Motivation 1.2 Problemstellung Kühlschrank bis hin zur elektrischen Zahnbürste, wird heutzutage von Software auf ei- 1.3 nem kleinen Chip gesteuert. Mit Softwarefehlern können Menschen bewusst oder un- Zielstellung bewusst praktisch täglich in Kontakt kommen. Einige Fehler haben keine gravierenden 1.4 Folgen oder bleiben unbemerkt, andere wiederum können sehr viel Geld oder sogar Men- Aufbau schenleben kosten. Je mehr Verantwortung einer Software oder einem technischen System übertragen wird, desto mehr muss bei der Herstellung auf Fehler geachtet werden. Funktionierende und vor allem benutzbare Software zu entwickeln, ist eine echte Herausforderung. Um Software hoher Qualität etwas näher zu kommen, sind Tests ein unerlässlicher Bestandteil im Entwicklungsprozess. 1.1 Motivation Die Motivation für Softwaretests ist die angestrebte Qualität des Softwareproduktes. Fehlerarme, robuste und funktionierende Software ist das Ziel eines jeden Softwareentwicklers. Dies kann aber nur mit entsprechenden Tests erreicht werden. Allerdings versteht unter dem Begriff „Testen“ Jeder etwas anderes; angefangen vom fehlerfreien Kompilieren eines Programmes bis hin zu äußerst intensiven methodischen Tests jeglicher Art. Je gründlicher die Tests sind, desto höher ist die Qualität der Software. Zwar sind selbst die intensivsten Tests kein Garant dafür, dass die Software fehlerfrei ist, aber es kann dann schon von einer wesentlich höheren Qualität gesprochen werden. Aus diesem Grunde ist das Testen von Software ein essentieller Schritt bei der Entwicklung. 1 1 Einleitung 1.2 Problemstellung Das größte Problem bei Softwaretests ist der Aufwand. Das manuelle Erstellen der Tests und das Konfigurieren der Eingabewerte sowie das Prüfen der Rückgabewerte kostet sehr viel Zeit. Tests können zwar automatisiert werden, allerdings kostet auch dies sehr viel Zeit von Entwicklern und/oder Testern, da die Tests auf jede zu testenden Komponente einzeln zugeschnitten werden müssen. Individualisierte Tests sind aufwändig zu erstellen, da die Eigenheiten von Methoden und Klassen gezielt angesprochen und getestet werden müssen. Grobe Tests auf Knopfdruck wären eine gute Ergänzung, um während der Entwicklung zu testen. Solche Tests sollen mit dem entwickelten Testsystem realisiert werden, ohne dass ein Entwickler selbst manuell Werte oder andere Informationen eingeben muss. 1.3 Zielstellung In dieser Arbeit sollen Softwaretests untersucht und ein eigenes Testsystem entwickelt werden. Das Testsystem basiert auf der Methode Fuzzing. Es soll versucht werden, mit diesem System schnell und einfach Fehler zu finden, die mit anderen Systemen nur mit wesentlich größerem Aufwand zu finden wären. Das Testsystem trägt den Namen „AutoTest.Net“ und soll in mehreren Schritten unterschiedliche Fähigkeiten aufweisen (siehe Kapitel 5). Ziel des Projektes ist es, eine oder mehrere Klassen automatisch zu instanziieren und alle Methoden zu testen, indem die Methoden mit unterschiedlichen und teilweise zufällig generierten Parameterwerten aufgerufen werden. Dabei wird das Verhalten der Methode überwacht. Am Ende eines Tests wird dem Benutzer ein übersichtliches Protokoll ausgegeben, aus dem genau ersichtlich ist, welche Methode mit welchen Parameterwerten welchen Fehler erzeugt hat. Folgende Fähigkeiten soll das Testsystem aufweisen: • Laden und Analysieren des Aufbaus einer .NET-Assembly • Konfigurieren der Testgenerierung • Durchführen von Funktionstests • Ausgabe eines Protokolls 2 1 Einleitung 1.3.1 Warum .NET Bei Recherchen in der .NET-Welt wurde festgestellt, dass Fuzzing-Tests für Software nicht so stark verbreitet sind wie im Java-Umfeld. Aus diesem Grund soll das entwickelte System in .NET geschrieben und für .NET-Programme anwendbar sein. Das .NETFramework ist ein zukunftssicheres Entwicklungs-Framework der Firma Microsoft. Es bietet sehr vielfältige und moderne Möglichkeiten für die Entwicklung von Software. Anwendungsentwicklung mit dem .NET-Framework ist äußerst effektiv und effizient. In Kombination mit Microsoft Visual Studio 2008 werden viele Ergänzungen geliefert, die das Entwickeln an vielen Punkten enorm erleichtern und unterstützen. 1.3.2 Was ist Reflection Der Namespace System.Reflection ist seit .NET 1.1 ein fester Bestandteil des Frameworks. Die enthaltenen Objekte bieten die Möglichkeit, Typinformationen von Objekten und deren Bestandteilen in .NET zur Laufzeit der Software abzufragen, zu analysieren und auszuwerten. Um dies genauer zu erläutern werden vorher einige Begriffe erklärt: • .NET-Assembly Eine Assembly ist eine Datei, welche typischerweise auf .exe oder .dll endet, die mithilfe eines .NET-Compilers erstellt wurde. Neben dem eigentlichen Quellcode, der in der Assembly als Modul zur Ausführung bereit steht, kann eine Assembly Ressourcen wie Bilder oder Textdateien beinhalten. • Properties Properties sind ein Bestandteil einer Klasse auf den über einen Accessor (eine Get- und/oder Set-Methode) zugegriffen wird, wobei die Accessor nicht explizit aufgerufen werden. Ein solcher Klassen-Member kapselt interne Variablen (Felder). Properties stellen Daten dar, wohingegen Methoden Aktionen darstellen. Die im folgenden Listing dargestellte Klasse Kunde enthält eine Property Name vom Typ string (siehe Listing 1.1): Listing 1.1: Beispiel für Property 1 class Kunde 2 { public string Name { get ; set ; } } 3 4 Kunde kunde = new Kunde () ; // Neue Instanz erzeugen 5 kunde . Name = " Meier "; // Property Name zuweisen 3 1 Einleitung • Attribut Mit Attributen sind in .NET Metainformationen für Klassen, Methoden oder auch Properties gemeint. In Java werden diese Attribute „Annotations“ genannt. Sie werden über den Objekten in eckigen Klammern (C#) angegeben. In der UML (Unified Modeling Language) ist ein Attribut ein Speicherplatz, der einem Objekt zugeordnet ist, z.B. für Zwischenspeicherung von Ergebnissen bei Berechnungen. Das folgende Beispiel zeigt das Serializable-Attribut über einer Klasse, mit dem angegeben wird, dass diese Klasse serialisiert werden kann (siehe Listing 1.2): Listing 1.2: Beispiele für ein Attribut 1 [ Serializable ] 2 class Hello Zu den auswertbaren Typinformationen gehören enthaltene Klassen in einer Assembly, Methoden oder Properties von Klassen bis hin zu Informationen über lokale Variablen einer Methode. Ebenso sind alle Arten von Attributen über Reflection abfragbar. Im folgenden Beispiel wird die Benutzung von Reflection vorgestellt (siehe Listing 1.3): Listing 1.3: Beispiele für Reflection 1 using System ; 2 using System . Reflection ; 3 namespace ReflectionTest 4 { 5 class Hello 6 { // Test - Klasse " Hello " 7 public void World () // Methode " World " 8 { Console . WriteLine ( " Hello World !" ); } // Ausgabe von " Hello World " 9 } 10 class Program 11 { // Hauptprogrammklasse 12 static void Main ( string [] args ) 13 { 14 Hello hello = new Hello () ; 15 hello . World () ; // Einsprungmethode Main // Erzeugen einer neuen Instanz ohne Reflection // Ausführen der Methode 16 17 Assembly a = Assembly . GetExecutingAssembly () ; // aktuelle Assembly abrufen 18 Type t = a. GetType ( " ReflectionTest . Hello " ); // Abrufen des Typs Hello 19 MethodInfo m = t. GetMethod ( " World " ); 20 Object o = Activator . CreateInstance ( t ); // Instanzerzeugung mit Rflctn . 21 m. Invoke ( o , null ); // Ausführen der Methode 22 23 } } 24 } 4 // Methode " World " des Typs " Hello " 1 Einleitung Dieses Beispiel führt zwei Mal die Methode World aus, weswegen auch „Hello World!“ zwei Mal auf der Konsole ausgegeben wird. Reflection sind ebenfalls in der Lage, mithilfe des Namespace System.Reflection.Emit Code dynamisch zu erzeugen. Parallel dazu gibt es noch Introspection, diese Technik wird aber nur mit FxCop ausgeliefert (vgl. Abschnitt 4.2.1 auf Seite 42) und ist aufgrund der fehlenden Fähigkeit, Programmteile auszuführen, für diese Arbeit weniger interessant. 1.4 Aufbau Im folgenden Kapitel werden Softwarefehler im Allgemeinen untersucht und mit berühmten Beispielen belegt. Im Kapitel 3 werden allgemeine Testverfahren vorgestellt und Vor- und Nachteile erörtert. Ein Überblick über aktuell vorhandene Testsysteme wird in Kapitel 4 gegeben. In Kapitel 5 werden das Projekt im Detail formuliert und Anforderungen an die zu erstellende Software definiert. Implementiert wird AutoTest.Net in Kapitel 6, wo Probleme und Herausforderungen bei der Umsetzung sowie Fortschritte dokumentiert werden. In der experimentellen Phase (Kapitel 7) wird das Testsystem angewendet und abgewogen, inwiefern ein praktischer Einsatz denkbar ist. Abschließend wird die gesamte Arbeit in Kapitel 8 zusammengefasst. Sowohl die Grenzen als auch ein Ausblick darüber, welche ergänzenden oder weiterführenden Möglichkeiten denkbar sind, werden erörtert. Als allgemeiner Hinweis soll vermerkt werden, dass alle Quellcodebeispiele aus Platzgründen nicht mit den üblichen XML-Kommentarblöcken versehen sind, aus denen später eine übersichtliche Quellcodedokumentation erzeugt werden kann. Weiterhin sei angemerkt, dass die genutzten programmiertechnischen Begriffe an die von Microsoft veröffentlichte C#-Terminologie angelehnt sind, welche unter folgender Adresse zu finden sind: http://msdn.microsoft.com/de-de/library/ms173231.aspx. 5 Bekannte Fehler und deren Ursache [2] 2.1 „Die Fehler sind alle da, sie müssen nur noch gemacht werden.“ Die Einführung Savielly Tartakower, polnischer Schachspieler des Begriffes „Software-Bug“ 2.2 „The most dangerous phrase in the language is, ’We’ve always done it this way.’“ Grace Murray Hopper, Konteradmiral der amerikanischen Marine, Doktor für Mathematik, Entwicklerin des ersten Compilers und Hauptentwicklerin der Programmiersprache COBOL (Wil04) Allgemeines zu Softwarefehlern 2.3 Ursachen und Ausmaße von Softwarefehlern Wenn es keine Fehler in Software geben würde, so müsste sie nicht getestet werden. 2.4 Was genau ist ein Softwarefehler bzw. welche Auswirkungen kann er haben? Die wich- Zusammen- tigste Frage aber ist, welche Ursachen diesen Fehlern zu Grunde liegen? Das Problem fassung liegt bekanntlich immer im Detail. Deswegen sollen in diesem Kapitel Fehler in Software und mögliche Fehlerarten untersucht und deren Ursache analysiert werden. Oft ist das Ausmaß eines Fehlers bei genauerer Fehlerbetrachtung weit größer, als es auf den ersten Blick den Anschein hat. Um dies zu belegen, werden einige berühmte Fehler vorgestellt, die verheerende Auswirkungen hatten und teilweise viele Menschenleben gekostet haben. 2.1 Die Einführung des Begriffes „Software-Bug“ Der Begriff „Bug“ (dt. Käfer/Wanze) stammt aus der Zeit, als Computer noch mit Röhren und Relais realisiert waren. Damals kam es des Öfteren vor, dass Insekten in die Röhren oder Relais hineinflogen und es so zu Kurzschlüssen oder anderen Fehlern in der Hardware kam. Der erste offizielle Bug wurde am 9. September 1945 an der Har- 6 2 Bekannte Fehler und deren Ursache vard University im damaligen Großrechner „Mark II Aiken Relay Calculator“ von Frau Konteradmiral Grace Murray Hopper, Doktor für Mathematik, gefunden. (Wil04) Der Begriff Bug hat sich stark verbreitet und ist heute ein gängiges Wort, um Fehler in einer Software zu beschreiben. So entstand auch das Wort „Debugger“ (dt. entwanzen/entkäfern). Fast jedes Softwareentwicklungs-Tool verfügt heutzutage über einen Debugger, um Fehler in Software aufspüren zu können. Dabei wird die Möglichkeit geboten, ein Programm Schritt für Schritt abzuarbeiten und alle Zustände von internen Variablen oder anderen Objekten aufzuzeigen. 2.2 Allgemeines zu Softwarefehlern Ein Fehler in einer Software ist ein unerwartetes Verhalten der Software. Etwas detaillierter betrachtet ist ein Fehler ein Unterschied zwischen beobachteten, ermittelten oder berechneten Zuständen oder Vorgängen bzw. Daten und wahren, festgelegten oder theoretisch korrekten Zuständen oder Vorgängen bzw. Daten. Das deutsche Institut für Normen (DIN) definiert Fehler folgendermaßen: Ein Fehler ist die Nichterfüllung einer Forderung (Vog08). Wenn ein Programmierer einen Fehler im Quellcode macht, so handelt es sich hierbei um einen Defekt (engl. Defect). Bei der Ausführung erzeugt dieser Fehler im Quellcode einen inkorrekten Programmzustand, der Infektion (engl. Infection) genannt wird. Die Infektion breitet sich aus und führt früher oder später zu einem beobachtbaren Fehlverhalten der Software (engl. Failure), was im Allgemeinen Fehler oder Bug genannt wird. Nicht jeder Fehler im Code führt zwangsläufig zu einer Infektion, und nicht jede Infektion führt zwangsläufig zu einem Fehler (Fehlverhalten) in der Software. (Mer08) Nach Basili lässt sich ein Erwartungswert der Fehlerzahl berechnen. Es gilt: 4 F = 4, 04 + 0, 0014 · LOC 3 mit LOC = Lines Of Code (Anzahl der Programmzeilen). Somit hätte ein Programm mit 1.000 effektiven Codezeilen eine erwartete Fehlerzahl von 18,04. (sR08, S. 45) Ein Graph visualisiert das LOC - Fehlerverhältnis (siehe Abbildung 2.1 auf der nächsten Seite). Diese empirische Formel basiert auf Auswertungen von vergangenen Projekten der NASA, bei der die Beziehung zwischen der Größe eines Programmes, also die Anzahl der Quellcodezeilen, und der Programmfunktionalität sowie der Fehlerträchtigkeit analysiert wurde. Auf Nachfrage deutete Basili darauf hin, dass diese Formel für eine geringe 7 2 Bekannte Fehler und deren Ursache Abbildung 2.1: Erwartete Fehlerrate im Programmcode nach Basili Anzahl an Quellcodezeilen nicht präzise sei. Der Nullpunkt und kleinere Werte wurden nicht berücksichtigt, da dies den Graph der Funktion negativ beeinflusst hätte. (Bas08) Eine andere, etwas kompliziertere Methode zur Berechnung der erwarteten Fehler entwickelten Takahashi und Kamayachi. Hierbei gilt folgende Formel (sR08): Fr = C1 +C2 · ( DOCC SCHG ) −C3 · ISKL −C4 · ( ) KLOC KLOC Wobei gilt: • FR = Anzahl Fehler pro 1.000 Zeilen Quellcode • C1 = 67,98 • C2 = 0,46 • C3 = 9,69 • C4 = 0,08 • SCHG = Änderungen in der Spezifikation während der Entwicklung • KLOC = 1.000 LOC (Lines of code) • ISKL = Durchschnittserfahrung des Teams mit der Programmiersprache in Jahren • DOCC = Anzahl der geänderten oder neu eingefügten Seiten im Designdokument Beispiel: In einem Softwareprojekt wurde eine Komponente von 1.000 Programmzeilen von einem Entwicklungsteam geschrieben, das seit zwei Jahren mit Java programmiert. Während der Entwicklung musste dreimal das Lastenheft geändert werden. Die vorliegende Komponente liegt in der vierten Version vor, d.h. das ursprüngliche 8 2 Bekannte Fehler und deren Ursache Designdokument wurde dreimal geändert. Die Fehlerrate berechnet sich nun wie folgt (Ger08): • SCHG = 3 • KLOC = 1 • ISKL = 2 • DOCC = 3 Die erwartete Fehlerzahl der Softwarekomponente (1.000 Programmzeilen) ist nach dieser Formel 49,74. Diese Formel weist ausschließlich empirische Konstanten auf. Normale Software, die keine Menschenleben oder Millionen Euro teure Ausrüstung steuert, enthält ca. 10 bis 50 Fehler auf 1.000 Zeilen Quellcode, was auch den Erfahrungen mit Rechtschreibfehlern in neu verfassten Texten entspricht. Das sind im Durchschnitt 25 Fehler pro 1.000 Zeilen. Wichtige Software, bei der es teilweise um die Sicherheit oder auch relativ teures Equipment gehen kann, beinhaltet nur 2 bis 3 Fehler auf 1.000 Zeilen (siehe Tabelle 2.1). Diese Software wird mit wesentlich höherem Aufwand hergestellt und viel stärker getestet. Die Software bei der NASA im Space Shuttle beinhaltet nach eigenen Angaben weniger als 0,1 Fehler auf 1.000 Zeilen Quellcode. Es wird eine sehr penible Datenbank geführt, in die jeder jemals aufgetretene Fehler sorgfältig mit Datum, Ursache und auch der Lösung des Problems eingetragen wird. (Gie08) Programm Zeilen Handy Space Shuttle, auch IIS Windows 2000 SDI (Raketenabwehr USA) 200.000 3 Millionen 27 Millionen 25-100 Millionen Fehler ca. 500 ca. 300 ca. 50.000 ca. 10.000 Tabelle 2.1: Programme mit der Anzahl der Quellcodezeilen und Fehler (Gie08) Zeilen müssen kommentiert, dokumentiert und getestet werden. Nicht nur einzeln, sondern auch im Zusammenhang mit anderen Zeilen, also anderen Teilen des Programmes. Fehler können in jeder Zeile, bzw. in jeder Anweisung oder auch in jedem Operator auftreten. Einige sind offensichtlich und schnell gefunden, andere treten nur manchmal auf. Auch Aspekte wie z.B. das soziale oder organisatorische Umfeld des Einsatzortes der Software können Fehlerindikatoren sein. Simple Denkfehler, wie z.B. dass aus cos(a) = cos(b) folgt, dass a = b ist, können eine Ursache darstellen. Dies mag logisch erscheinen, wenn es in einem Kontext gelesen wird. In einem Programm kann dies aber in einem wesentlich komplexeren Zusammenhang auftreten. Somit können simple Sachverhalte unüberschaubar werden. (Gie08) 9 2 Bekannte Fehler und deren Ursache Abgesehen von bewusst betriebener Sabotage an Software trägt der Benutzer keine Schuld an Abstürzen in Software. Bei ungültigen Benutzereingaben, die zum Absturz der Software führen, liegt ein Fehler in dem Programmteil vor. Der Programmierer hat bestimmte Fehlerfälle nicht berücksichtigt und keine fehlerüberprüfenden Routinen implementiert, weil bestimmte Eingaben sehr unwahrscheinlich erschienen. Im Allgemeinen wird ein Großteil des entwickelten Codes dafür verwendet, um Fehler abzufangen und Eingangsdaten auf Plausibilität zu prüfen. Selbst die beste Recherche zu Beginn kann niemals garantieren, dass immer alle möglichen Fälle berücksichtigt wurden. Jegliche Tests sind kein Beweis dafür, dass eine Software fehlerfrei ist. Tests können nur das Vorhandensein von Fehlern beweisen, nicht aber deren Abwesenheit. Es kann generell nicht davon ausgegangen werden, dass keine Fehler auftreten bzw. keine Fehlerüberprüfungsroutinen erforderlich sind. Weitere Beispiele werden im Laufe des Kapitels aufgeführt. 2.3 Ursachen und Ausmaße von Softwarefehlern Die meisten Fehler, egal in welcher Form sie auftreten, haben Folgen. Diese Folgen können teilweise sehr verheerende Ausmaße annehmen. Es gibt sehr unterschiedliche Ursachen für Fehler in Software. Die Ursachen der berühmtesten Fehler werden im Folgenden etwas näher betrachtet, um einen Zusammenhang zwischen trivialen Fehlern in Software und riesigen Ausmaßen herzustellen. 2.3.1 Tippfehler Tippfehler werden in der Programmierung oft unterschätzt, weil normalerweise davon ausgegangen wird, dass der Compiler solche Fehler entdeckt. Fehler, wie z.B. zwei vertauschte Buchstaben, würde wohl jeder Compiler entdecken, wenn es nicht gerade zufällig eine Struktur mit diesem Namen gibt und diese auch noch auf die gleiche Art und Weise angesprochen werden würde. Wenn aber eine solche Struktur existiert, liegt wahrscheinlich ein Designfehler in der Namensgebung vor. Selbst in natürlichen Sprachen können kleine Tippfehler bei Satzzeichen ganz andere Bedeutungen hervorrufen. • Der brave Mann denkt an sich selbst zuletzt. • Der brave Mann denkt an sich, selbst zuletzt. In einem Satz wurde ein Komma gesetzt, im anderen nicht. Das Ergebnis ist eine gänzlich gegenteilige Aussage. Auch im nächsten kurzen Beispiel ist lediglich ein Komma verschoben worden, was die Aussage verändert. (Gie08) 10 2 Bekannte Fehler und deren Ursache • Treu war sie, nicht ohne Tränen ließ ich sie gehen. • Treu war sie nicht, ohne Tränen ließ ich sie gehen. In .NET sind solche, meistens offensichtlichen Fehler, aufgrund der CompilerWarnungen recht leicht zu finden. In anderen Sprachen können kleine Tippfehler schon verheerende Auswirkungen haben, besonders dann, wenn der Compiler sie nicht als fehlerhaft interpretiert. Diese Codezeilen werden dann vom Compiler in einer anderen Art und Weise übersetzt, als es der Entwickler im Sinn hatte. Die klassischen Tippfehler sind falsche Interpunktionszeichen, wie z.B. ein Semikolon hinter einer while-Schleife oder Punkte und Kommas, welche vertauscht benutzt werden. Teilweise können auch Leerzeichen oder andere, nicht sichtbare Zeichen zu Fehlern führen. Diese sind ohne einen Debugger besonders schwer zu entdecken. • Mariner 1 (1962) Am 22. Juni 1962 verlor die NASA die Raumsonde Mariner 1, die in Cape Canaveral startete und sich auf dem Weg zur Venus befand. Durch eine Abweichung der Flugbahn wurde die Rakete nach 290 Sekunden zerstört. Ursache hierfür war ein simpler Rechtschreibfehler in dem Programm zur Steuerung der Flugbahn der Trägerrakete. Das Programm wurde in der Programmiersprache FORTRAN entwickelt (siehe Listing 2.1). Listing 2.1: Ausschnitt aus dem FORTRAN-Steuerprogramm von Mariner 1 (Gie08) 1 IF ( TVAL . LT . 0.2E -2) GOTO 40 2 DO 40 M = 1, 3 3 W0 = (M -1) *0.5 4 X = H *1.74533 E -2* W0 5 DO 20 N0 = 1, 8 6 EPS = 5.0*10.0**( N0 -7) 7 CALL BESJ (X , 0, B0 , EPS , IER ) 8 IF ( IER . EQ . 0) GOTO 10 9 20 CONTINUE 10 DO 5 K = 1. 3 11 T(K) = W0 12 Z = 1.0/( X **2) * B1 **2+3.0977 E -4* B0 **2 13 D(K) = 3.076 E -2*2.0*(1.0/ X* B0 * B1 +3.0977 E -4* !!! Fehlerhafte Zeile !!! 14 *( B0 **2 - X* B0 * B1 ))/Z 15 E(K) = H **2*93.2943* W0 / SIN ( W0 )*Z 16 H = D(K) -E(K) 17 5 CONTINUE 18 10 CONTINUE 19 Y = H/W0 -1 20 40 CONTINUE 11 2 Bekannte Fehler und deren Ursache In Zeile 10 wurde anstatt einem Komma, welches eine Schleife beschrieben hätte, ein Punkt gesetzt, was einer normalen Variablenzuweisung entspricht. Aufgrund dessen wurde der Variablen „DO5K“ der Wert 1,3 zugewiesen, da in FORTRAN Variablen Zahlen und Leerzeichen enthalten dürfen. Somit wurde die ursprünglich gedachte Schleife niemals ausgeführt. (Gie08) Auch in anderen Sprachen können solche trivialen Satzzeichen ein gänzlich anderes Verhalten hervorrufen. Ein altbekanntes Problem ist der ungewollte Abbruch einer Bedingung durch ein Semikolon. Dies geschieht im Zusammenhang mit Steueranweisungen wie for, do/while oder if. Die folgenden Beispiele zeigen die drei Programmiersprachen C, Pascal und Modula 2 (siehe Listing 2.2): Listing 2.2: Beispiele für Tippfehler und deren Auswirkungen (IS08) 1 C for (i =1; i <=3; ++ i); 2 f(i); 1 mal mit i =4 3 4 Pascal for i :=1 TO 3 DO ; 5 f(i); 1 mal mit i =3 6 7 Modula 2 FOR i := 1 TO 3 DO ; 8 f(i) 9 richtig END ; Dieses Problem, welches in C besteht, ist ebenfalls in C++ und C# vorhanden, allerdings liefert Visual Studio bei dieser Anweisung eine Warnung, die auf einen ungewollten Schleifenabbruch hinweist („Possible mistaken empty statement“). In C und C++ geht es sogar noch weiter. Im folgenden Listing sind zwei Schleifen gezeigt, die sich beide kompilieren lassen (siehe Listing 2.3): Listing 2.3: Beispiele für Tippfehler in C-Schleifen 1 // Schleife mit 6 Durchläufen 2 float x = -1; 3 while (x < 0.1) 4 { 5 x +=0.2; 6 Console :: WriteLine (L"x: " + x. ToString () ); 7 } 8 // Endlosschleife aufgrund des Kommaoperators 9 float x = -1; 10 while (x < 0 ,1) 11 { 12 x +=0.2; 13 Console :: WriteLine (L"x: " + x. ToString () ); 14 } 12 2 Bekannte Fehler und deren Ursache Die erste Schleife endet nach 6 Durchläufen, wohingegen die andere Schleife nie endet, also eine Endlosschleife darstellt. Diese recht simplen Beispiele zeigen, welche Auswirkungen triviale Tippfehler haben können. Solche Fehler können nur durch gewissenhafte Programmierung vermieden oder im Nachhinein durch Tests und mit Hilfe des Debuggers gefunden werden. 2.3.2 Umwandlungsfehler Umwandlungsfehler treten bei der Umwandlung einer Variable von einem Datentyp in einen anderen auf. Wenn z.B. eine 64-Bit-Gleitkommazahl in eine 64-Bit-Ganzzahl konvertiert wird, entstehen unter Umständen Ungenauigkeiten, da die Nachkommastellen nicht mit übertragen und entweder wegfallen oder gerundet werden. Bei einer Umwandlung von einem Datentyp in einen Datentyp mit geringerem Speicherbedarf, z.B. von einer 64-Bit-Zahl in eine 16-Bit-Zahl, können unter Umständen auch Überläufe entstehen, wenn der Wert größer als der maximale Wert des kleineren Datentyps ist (vgl. Abschnitt 2.3.3 auf der nächsten Seite). • „Ariane 5“-Rakete (1996) Wohl einer der teuersten Softwarefehler zeigte seine Ausmaße am 4. Juni 1996, als vom französischen Guyana aus die „Ariane 5“-Rakete startete. Sie explodierte ca. 40 Sekunden nach dem Start. Der Verlust betrug ca. 500 Millionen Dollar für die Rakete und vier Satelliten zuzüglich der weiteren 300 Millionen Dollar für nachfolgende Verbesserungen. Der Verdienstausfall betrug ca. 2 bis 3 Jahre. Der Bordcomputer stürzte nach 36,7 Sekunden ab, weil eine 64-Bit-Gleitkommazahl in ein 16-Bit-Signed-Integer umgewandelt werden sollte. In einer Flughöhe von 3700m erreichte die Horizontalgeschwindigkeit einen Wert von 32768,1 interne Einheiten und war somit größer als 215 = 32768. (Gie08) Die eingesetzte Software stammte aus der „Ariane 4“-Rakete, wo sie einwandfrei funktionierte. Tragischerweise war dieser Teil der Software für den eigentlichen Flug überflüssig. Die Geschwindigkeit war bei der „Ariane 4“ bei weitem nicht so hoch wie bei der „Ariane 5“. Ein redundanter Computer verwendete die gleiche Software. Dieser war aber schon kurze Zeit vorher abgestürzt. Die Zahlenumwandlung war nicht abgesichert, da niemand gedacht hätte, dass die Geschwindigkeit so groß werden könnte. Im folgenden Listing ist in der Zeile 15 die Umwandlung zu sehen, welche den tragischen Unfall auslöste (siehe Listing 2.4): (Huc08) 13 2 Bekannte Fehler und deren Ursache Listing 2.4: Auszug aus dem Trägheitsnavigationssystem der „Ariane 5“ (Gie08) 1 ... 2 declare 3 vertical_veloc_sensor : float ; 4 horizontal_veloc_sensor : float ; 5 vertical_veloc_bias : integer ; 6 horizontal_veloc_bias : integer ; 7 ... 8 begin 9 declare 10 pragma suppress ( numeric_error , horizontal_veloc_bias ); 11 begin 12 sensor_get ( vertical_veloc_sensor ); 13 sensor_get ( horizontal_veloc_sensor ); 14 vertical_veloc_bias := integer ( vertical_veloc_sensor ); 15 horizontal_veloc_bias := integer ( horizontal_veloc_sensor ); 16 ... 17 exception 18 when numeric_error => calculate_vertical_veloc () ; 19 when others => use_irs1 () ; 20 end ; 21 end irs2 ; 2.3.3 Überläufe Jeder Datentyp entspricht einer gewissen Anzahl an Bytes im Speicher. Zum Beispiel entspricht der Datentyp byte dabei genau einem Byte. In den meisten Sprachen ist die Folge eines Überlaufs, dass der Variablenwert wieder auf der anderen Seite des Wertebereiches beginnt und daher vom größtmöglichen Wert auf den kleinstmöglichen Wert, oder andersherum, überspringt. Bei einem 16-Bit-Integer (Int16), welcher einen Wertebereich von -32768 bis +32767 hat, gilt also folgendes: 32767 + 1 = −32768. In .NET bzw. in Microsoft Visual Studio 2008 ist dies eine Einstellungssache im Compiler oder in der Entwicklungsumgebung. Teilweise können Überläufe auch ein gewünschter Effekt sein, z.B. bei einem Ringpuffer, bei dem nach der maximalen Anzahl wieder automatisch von vorn begonnen werden soll. Überläufe können auch als Folge einer Umwandlung (vgl. Abschnitt 2.3.2 auf der vorherigen Seite) entstehen. Allerdings gibt es ab .NET 2.0 das Schlüsselwort checked, mit dem das Werfen einer Exception bei einem Überlauf für arithmetische Operationen und Konvertierungen mit ganzzahligen Typen erzwungen werden kann. Somit kann sichergestellt werden, dass ein Überlauf niemals übersehen oder versehentlich ausgeführt wird, da eine System.OverflowException bewusst behandelt 14 2 Bekannte Fehler und deren Ursache werden muss. Der Befehl checked kann als Methode mit runden Klammern oder auch als Block mit geschweiften Klammern genutzt werden (siehe Listing 2.5). Listing 2.5: Beispiel für sichere Handhabung von Überläufen 1 using System ; 2 3 namespace @checked 4 { 5 class Program 6 { 7 static void Main ( string [] args ) 8 { 9 short x = 32765; 10 // 16 Bit Wert mit MAX = 32767 for ( int i = 0; i < 5; i ++) 11 // checked - Block . Überläufe in diesem Block werden sicher erkannt . 12 checked 13 { 14 Console . WriteLine ( x ++ ); 15 } 16 // alternativ zu diesem Block würde auf folgendes gehen : 17 // Console . WriteLine ( checked ( x ++ ) ); 18 } 19 } 20 } In diesem Beispiel entsteht kein Überlauf, da eine System.OverflowException geworfen wird. Ohne die Hilfe von checked wäre die Ausgabe in diesem Beispiel: 32765 32766 32767 -32768 -32767 Im Gegensatz zum Schlüsselwort checked steht das Schlüsselwort unchecked. Es wird genutzt, um Überlaufprüfungen für arithmetische Operationen und Konvertierungen mit ganzzahligen Typen zu unterdrücken. • Röntgen/Elektron-Bestrahlung - Therac-25 (1985-1987) Das Bestrahlungsgerät Therac-25 der Firma Atomic Energy of Canada Limited (AECL) sollte vielen Menschen helfen, Krankheiten wie Krebs zu überwinden. Die Patienten wurden einer Strahlendosis ausgesetzt, die ca. das 100fache der eigentlich vorgesehenen Energiemenge besaß. Die Software, die dieses Gerät steuerte, bestand aus mehr als 20.000 Anweisungen und wurde von nur einem einzigen 15 2 Bekannte Fehler und deren Ursache Programmierer entwickelt. Eine Variable, die als Zählvariable für Fehleinstellungen galt, war vom Typ byte. Wenn diese Variable den Wert 0 hatte, lag kein Fehler vor und die Bestrahlung konnte begonnen werden. Bei genau 256 fehlerhaften Einstellungen entstand ein Byte-Überlauf und der Wert der Variablen lag wieder bei 0. Der Fehler wurde anfangs nicht dem Gerät zugeschrieben. Erst nachdem insgesamt 6 Menschen an den Folgen der viel zu hohen Strahlendosis gestorben waren, wurde das Gerät außer Betrieb genommen. (Pfe08) 2.3.4 Unterschiedliche Einheiten Noch gibt es auf der Welt für gleiche Dinge viele unterschiedliche Namen in vielen unterschiedlichen Sprachen. Genauso wie diese unterschiedlichen Bezeichnungen haben viele Nationen dieser Erde auch noch eigene Begriffe und Einheiten für z.B. Größen- oder Gewichtsangaben, die aus längst vergangenen Zeiten stammen. Einheiten wie Pfund, Unze, Fuß oder Gallone, die auch noch in jeder Gegend andere Werte annehmen können, sind ungenau, uneinheitlich und führen zwangsläufig zu Problemen. Aus diesem Grunde wurden die SI-Basiseinheiten eingeführt, welche genormt sind und deswegen ein internationales Einheitensystem darstellen. Die SI-Tabelle beinhaltet genau 7 Basiseinheiten, u.a. Einheiten für Länge, Zeit, Masse und Lichtstärke. Aus diesen Einheiten lassen sich wiederum andere zusammengesetzte Einheiten bilden, die auch mit anderen Basis- und abgeleiteten Einheiten widerspruchsfrei kombiniert werden können. Abgesehen vom Kilogramm basieren alle Basiseinheiten auf Naturgesetzen und eignen sich von daher hervorragend für alle Regionen dieser Erde. Welche Folgen aufgrund von unterschiedlichen Einheiten entstehen können, zeigen die folgenden Beispiele: • Mars Climate Orbiter (1999) Am 11. Dezember 1998 startete eine Sonde ihren Weg zum Mars mit der Aufgabe, eine Umlaufbahn zu erreichen und die Marsoberfläche zu kartographieren. Ein dreiviertel Jahr später, am 23. September 1999, kurz bevor die Sonde ihre Marsumlaufbahn erreichte, stürzte sie beim Anflug auf den Mars ab. Die Ursache des Absturzes lag in der Verwendung von unterschiedlichen Einheiten für die Angabe von Kräften. Die damalige Firma lieferte Module, die mit der imperialen Einheit „pound“ (Pfund) rechneten. Die NASA hatte aber in Auftrag gegeben, dass das Modul die Einheit in Newton angeben sollte, was dem metrischen System entspricht. Dieser simple Fehler hatte den Absturz der Sonde zur Folge. Die Kosten beliefen sich auf ca. 125 Millionen US-Dollar. (Gla08) 16 2 Bekannte Fehler und deren Ursache • Shuttle-Laser-Fehler (1985) Am 19. Juni 1985 bekam die Mannschaft eines Space Shuttles die Aufgabe, dass Shuttle so zu positionieren, dass ein Spiegel, welcher an der Außenseite des Shuttles montiert war, einen Laserstrahl umlenken konnte. Dieser Laserstrahl ging von einem Berg aus, welcher sich 10.023 Feet (ca. 3055 m) über dem Meeresspiegel befand. Das Computerprogramm, welches das Shuttle steuerte, hatte als Berechnungsgrundlage die Einheit nautical mile (dt. nautische Meile oder auch Seemeile). Das Shuttle wurde also so positioniert, als wenn der Berg eine Höhe von 10.023 nautischen Meilen (ca. 18.562.596 m) hätte. (Lin85) 2.4 Zusammenfassung Fehler können immer und überall in einer Software auftreten. Die Unterschätzung des nötigen Aufwandes, das Setzen von zu knappen Timelines und Fehlbudgetierung zwingt Entwickler, wichtige Fehlerprüfroutinen auszulassen. Essentiellen Programmteilen wird zu wenig Aufmerksamkeit geschenkt. Dies führt häufig zu nicht bedachten Nebeneffekten und Fehlern. Zudem entstehen Fehler hauptsächlich, weil sich die Anforderungen bei wohl keinem anderen Produkt so schnell ändern wie bei Software. Solche ständig wechselnden Aufgabenstellungen sind eine große Herausforderung an Entwickler, da bei größeren Änderungen das Projekt fast immer neu überdacht und überarbeitet werden muss. Viele Fehler könnten eventuell durch weitreichende Fehlerprüfroutinen vermieden werden, welche unvorhergesehene Fälle, wie Division durch Null oder Zahlenumwandlungen, absichern. Deswegen hilft jede Fehlerprüfroutine die Software etwas sicherer und fehlerfreier zu machen. Durch solche Routinen können Fehler früher erkannt werden. Selbst Fehler, die auf den ersten Blick keine Auswirkungen hätten, können entdeckt werden. Diese Routinen allein sind aber nicht hinreichend, um fehlerarme Software zu produzieren. Natürlich sind Tests eine äußerst nützliche und auch notwendige Art, Fehlern vorzubeugen. Und das so frühzeitig wie möglich. Durch ausreichende Kontrolle können Fehler, die in der Soft- und Hardwareentwicklung entstehen, im Allgemeinen vermieden werden. Dies kann zum einen durch gute Kommunikation der Entwickler gewährleistet werden, zum anderen durch gute Dokumentation und Spezifikation des Projekts. Einst funktionstüchtige Soft- und Hardware sollte vor der Wiederverwendung in anderen Projekten genau auf deren Funktionsweise unter anderen bzw. neuen Voraussetzungen getestet werden. Aus all diesen Fehlern muss für zukünftige Softwareprojekte gelernt werden. 17 Grundlegendes zu Softwaretests [3] 3.1 „Program testing can be used to show the presence of bugs, but never to show their absence!“ Was ist Testen Edsger W. Dijkstra, niederländischer Informatiker Qualitäts- 3.2 merkmale „Qualität ist kein Zufall; sie ist das Ergebnis angestrengten Denkens.“ John Ruskin, englischer Schriftsteller und Maler 3.3 Blackbox-Tests 3.4 Whitebox-Tests Testen und Qualität stehen in einer engen Beziehung zueinander. Getestet werden kann 3.5 nur, was vorher definiert wurde. Definiert werden Anforderungen an die Software, was Testverfahren ebenfalls zu Qualitätsmerkmalen zählt. Neben den unterschiedlichen Testarten soll in 3.6 diesem Kapitel ein allgemeiner Überblick über das Testen gegeben und erläutert werden, Konkrete was genau Testen eigentlich ist. Testverfahren 3.7 3.1 Was ist Testen Zusammenfassung Testen ist ein wesentlicher Schritt beim Entwickeln von Software. Im Zuge dessen treten viele Begriffe auf, die im Folgenden erläutert werden sollen. • Testorakel Dieser sehr mystisch klingende Begriff weist auf eine Komponente hin, welche bei der Eingabe von Testfällen immer die korrekten, erwarteten Ausgabewerte liefert. Wichtig ist hierbei, dass korrekte Ausgabewerte niemals anhand eines Programmcodes ermittelt werden dürfen, sondern immer anhand von Spezifikationen. Ein rechnergestütztes Testorakel wäre optimal, allerdings handelt es sich dabei meistens um einen Menschen. (Ger08) 18 3 Grundlegendes zu Softwaretests • Testtreiber Ein Testtreiber ist ein Programm, welches in der Lage ist, einen Test an einem Testobjekt durchzuführen und die Resultate aufzunehmen. Mit Hilfe eines Testtreibers werden auch Fehlverhalten der Software protokolliert. Im Rahmen dieser Arbeit wird ebenfalls ein solcher Testtreiber entwickelt. • Testen Das Testen ist ein Prozess, um die Qualität von Software systematisch zu untersuchen, indem Teile der Software in einer definierten Umgebung ausgeführt werden, um die Resultate der Software mit den Erwartungen zu vergleichen (dynamische Tests). Mit möglichst gut gewählten Testwerten (Möl08) wird eine Software gegen die Anforderungen hin getestet, die zu Beginn festgelegt wurden, um so die Ist-Daten mit den Soll-Daten zu vergleichen. Testen ist im Allgemeinen eine stichprobenartige Ausführung der Software unter spezifizierten Testbedingungen (vV08), um die Qualität von Software zu messen. Dabei wird unterschieden zwischen Verifizieren und Validieren. Aus den Anforderungen werden die Soll-Daten vom Testorakel abgeleitet. Alle Funktionalitäten, Daten oder Leistungen der Software, müssen mindestens mit einem Testfall abgedeckt sein. Ziel des Testens ist es, Fehler zu finden oder diese zu vermeiden. Ziel ist nicht, Fehler zu korrigieren, denn dieser Vorgang wird Debuggen genannt. Weiterhin ist Testen kein Korrektheitsnachweis, denn ein Test kann nur das Vorhandensein von Fehler nachweisen, nicht aber, dass keine Fehler vorhanden sind. (Ger08) • Validieren Beim Validieren bzw. bei einer Validierung werden bestimmte Eigenschaften der Software bezüglich ihrer Anforderungen geprüft (Möl08). Die Entwicklungsergebnisse werden bezüglich der individuellen Anforderungen geprüft (vV08). Laut ISO ist Validierung das Sicherstellen, dass ein Produkt und somit auch eine Software zu den Benutzerbedürfnissen und den Anforderungen konform ist (Ger08). Hier gilt als Anmerkung, dass die Benennung „validiert“ eine Bezeichnung für den Status der Software darstellt (Vog08). Die Bedingungen, unter denen eine Software validiert wird, können echt oder simuliert sein (ebd.). • Verifizieren Beim Verifizieren bzw. bei einer Verifikation wird die Qualität der Software durch einen mathematischen Beweis gegen die Vorgaben geprüft. Somit kann die Korrektheit eines Softwareteils bewiesen werden (vV08). Dies ist allerdings nur mög- 19 3 Grundlegendes zu Softwaretests lich, wenn die Anforderungen an die Software in einem exakten mathematischen Modell spezifiziert sind. Im Gegensatz zu den testenden (validierenden) Verfahren wollen verifizierende Verfahren die Korrektheit solcher Spezifikationen beweisen (Möl08). Als Anmerkung gilt, dass die Benennung „verifiziert“ zur Bezeichnung des entsprechenden Status verwendet wird (Vog08). • Testfall Ein Testfall umfasst neben den notwendigen Vorbedingungen die aus den Spezifikationen oder dem Programm abgeleiteten Testdaten als Eingabedaten für den Test. Zusätzlich beinhalten die Testfälle die zugehörigen erwarteten Ergebnissen (Sollwerte) sowie die Prüfanweisungen (wie die Eingaben an das Testobjekt übergeben werden müssen) und erwartete Nachbedingungen. (Möl08) und (vV08) • Testdaten Testdaten sind die Werte, die für die Tests genutzt werden (Möl08). Im Rahmen dieser Arbeit entspricht ein Testdatum genau einem übergebenen Parameterwert, z.B. „15.08.2008“ für einen Parameter vom Typ System.DateTime. Bei mehreren Parametern einer Methode wird für jeden Parameter ein Wert übergeben. • Testplan Zeitliche Planung der Testdurchführung (Zuordnung der Testfälle zu Testern und Festlegung des Durchführungszeitpunktes). Enthält Verzeichnis aller Testfälle sowie die erwarteten Ergebnisse, in der Regel thematisch bzw. nach Testzielen gruppiert. (vV08) Meistens wird eine Validierung durchgeführt, weil selten ein exakt mathematisches Modell der Anforderungen vorliegt bzw. ein Beweis der Funktionalität durchgeführt wird oder durchgeführt werden kann. Dazu gibt es unterschiedliche Verfahren, um bestimmte Merkmale zu testen. Kein Test kann alle Merkmale auf einmal abdecken. Bei den zu testenden Merkmalen handelt es sich um die Qualitätsmerkmale einer Software, die im Folgenden näher erläutert werden sollen. 3.2 Qualitätsmerkmale Qualität im Allgemeinen ist ein fehlerfreies, funktionierendes Produkt oder eine Dienstleistung, wie es vom Auftraggeber oder Kunden gewünscht ist. Die Qualität speziell für Software wurde eigens nach DIN ISO 9126 standardisiert. Diese Norm besagt Folgendes: „Software-Qualität ist die Gesamtheit der Merkmale und Merkmalswerte ei- 20 3 Grundlegendes zu Softwaretests nes Software-Produkts, die sich auf dessen Eignung beziehen, festgelegte oder vorausgesetzte Erfordernisse zu erfüllen“. (Möl08) Diese Norm unterteilt Qualitätsmerkmale in sechs Hauptkategorien. Sie sind wiederum in weitere Teilmerkmale aufgegliedert (siehe Tabelle 3.1): Merkmal Teilmerkmal Beschreibung Funktionalität Korrektheit Korrektes Arbeiten der Software mit gewünschten Resultaten. Sequentielle sowie nebenläufige Fehlerfreiheit. Korrekte Terminierung. Angemessenheit Vorhandensein einer Reihe angemessener Funktionen für die spezifizierte Problemstellung. Interoperabilität Allgemein: Fähigkeit der Software mit anderen Systemen zusammenzuarbeiten - hier (.NET): Zusammenarbeiten von verwaltetem und unverwaltetem Code (NWR06, S. 921). Normenkonformität Einhaltung bestehender Normen, Regelungen oder Gesetze. Sicherheit Verhindern von unbefugtem Zugriff auf vertrauliche Daten. Persistenz Fähigkeit, Daten jeglicher Art dauerhaft zu speichern und jederzeit bereitzustellen. Zuverlässigkeit Verfügbarkeit Fähigkeit der Software, schnell und häufig zur Verfügung zu stehen. Auch bei schwierigen Einsatzsituationen. Determinismus Vorherbestimmbarkeit aller Reaktionen der Software auf Aktionen. Fehlertoleranz Fähigkeit der Aufrechterhaltung des Leistungsniveaus beim Auftreten von Fehlern, fehlerhafter Bedienung oder Verstöße gegen Schnittstellenspezifikationen. Wiederherstellbarkeit Fähigkeit, auch im Fehlerfall mit geringstmöglichem Aufwand die Software an sich und alle Daten wiederherstellen zu können. 21 3 Grundlegendes zu Softwaretests Benutzbarkeit Verständlichkeit Das logische Konzept im Programm und deren Anwendbarkeit sind intuitiv zu verstehen. Erlernbarkeit Einfaches Erlernen aller Funktionen der Software. Bedienbarkeit Einfache und vor allem intuitiv richtige Bedienung und Steuerung der Software. Effizienz Ressourcen Effizientes Verhalten der Software beim Verwenden von Ressourcen. Laufzeit Zeitliches Verhalten der Software bezüglich Antwort- und Verarbeitungszeiten. Wartbarkeit Analysierbarkeit Fähigkeit, mit geringstmöglichem Aufwand Diagnosen zu stellen und Mängel, Fehler und Ursachen zu identifizieren. Änderbarkeit Änderungen und Fehlerbehebung sind mit geringem Aufwand und nicht notwendigerweise Veränderung der Umgebung durchführbar und betreffen ausschließlich die geänderten Softwareteile. Stabilität Fähigkeit, nach Änderungen stabil auf unerwartete Auswirkungen zu reagieren und das Leistungsniveau beizubehalten. Testbarkeit Änderungen sind mit geringstmöglichem Aufwand prüfbar. Portabilität Anpassbarkeit Fähigkeit der Software, auf unterschiedlichen Umgebungen zu laufen, ohne diese zu verändern. Installierbarkeit Die Software lässt sich unter Berücksichtigung der Anforderungen mit geringstmöglichem Aufwand installieren. Austauschbarkeit Fähigkeit der Software, andere Software mit geringstmöglichem Aufwand an die Software selbst oder die Einsatzumgebung zu ersetzen. Tabelle 3.1: Liste der Qualitätsmerkmale nach DIN ISO 9126 (Möl08) und (GK08) 22 3 Grundlegendes zu Softwaretests Einige der Merkmale überschneiden oder bedingen sich gegenseitig, positiv sowie auch negativ. Zum Beispiel wirkt sich die Korrektheit einer Software positiv auf die Robustheit aus, wohingegen die Effizienz einer Software sich negativ auf die Lesbarkeit oder die Änderbarkeit auswirken kann (Möl08, nach Pomberger). Grob betrachtet treffen all diese Merkmale auf jedes Softwareprodukt zu. Bei detaillierter Betrachtung der Teilmerkmale ist allerdings festzustellen, dass jedes Softwareprodukt seine eigenen Qualitätsmerkmale besitzt, da laut schon erwähnter Definition „vorausgesetzte Erfordernisse“ ein Teil der Qualitätsmerkmale darstellt. An einer Software können nur Qualitätsmerkmale getestet werden. Tests bei Software sind in zwei große Kategorien unterteilt, die sich wiederum unterteilen lassen. Diese beiden Kategorien sind Blackbox- und Whitebox-Tests (TS04). Zwar existiert seit kurzer Zeit auch noch der Begriff „Graybox-Test“, allerdings ist dies nur eine Mischform und soll die Vorteile von Blackbox- und von Whitebox-Tests verbinden. 3.3 Blackbox-Tests Blackbox-Tests sind Tests, welche ohne den Zugriff auf den Quellcode ausgeführt werden. Hierbei wird die zu testende Software benutzt und von außen getestet. Deswegen werden sie auch Funktionstests oder funktionale Tests genannt. Als Grundlage dient die Spezifikation der Software (TS04). Zugegriffen wird auf die Schnittstelle der Software nach außen. Diese Tests sind meist wesentlich einfacher zu erstellen als Whitebox-Tests (vgl. Abschnitt 3.4 auf der nächsten Seite), da der Quellcode nicht zur Verfügung steht und nicht analysiert werden muss. Blackbox-Tests eignen sich hervorragend, um schon vorhandene Software im Nachhinein zu testen, sie können aber auch während aller anderen Testphasen angewendet werden. Sie eignen sich ebenfalls um automatische Tests zu generieren. (Ger08) Blackbox-Tests sind ausschließlich dynamische Tests. Bei dynamischen Tests wird die Software gestartet und an der lauffähigen Version Untersuchungen und Analysen durchgeführt. Ausgabewerte und Zustände werden protokolliert. Diese Art von Tests verläuft stichprobenartig, da niemals oder nur sehr selten alle auftretenden Fälle getestet werden können (Ger08). Die Software wird mit wahrscheinlichen sowie unwahrscheinlichen Testdaten in einer realen Umgebung ausgeführt (Möl08). Zu Blackbox-Tests gehören: • Äquivalenzklassenbildung Äquivalenzklassen sind gruppierte Testdaten von Eingabewerten für eine Methode. Gebildet werden diese Klassen, indem der Wertebereich eines Eingabeparameters 23 3 Grundlegendes zu Softwaretests einer Methode unterteilt und in Intervalle oder Untermengen zerlegt wird. Diese Untermengen werden so gewählt, dass alle Werte einer Äquivalenzklasse die gleiche Reaktion der Testmethode zur Folge haben. Zum Testen ist also ein Repräsentant einer Äquivalenzklasse ausreichend. (TS04) Als Negativtest werden Äquivalenzklassen gebildet, die ungültige Werte enthalten. Für Methoden, die mehrere Parameter enthalten, sollten viele Permutationen als Testfall angewendet werden. Da so die Anzahl der Tests sehr schnell sehr groß werden kann, gibt es Verfahren, um die Anzahl der Testfälle zu reduzieren (SL02): – Priorisierung und Auswahl der Testfälle nach Benutzungshäufigkeit – Bevorzugung der Testfälle, die Grenzwerte enthalten – Keine Kombinationen von ungültigen Repräsentanten verwenden Durch eine Minimierung der Testfälle entsteht eine qualifizierte Auswahl an sinnvollen Testfällen. Bei AutoTest.Net entspricht eine Äquivalenzklasse genau einem Datentyp, z.B. dem Datentyp System.Double. Die enthaltenden Werte der Äquivalenzklasse stellen die Testdaten dar. • Grenzwerttest Ein Grenzwerttest basiert auf der Bildung von Äquivalenzklassen. Hierbei werden Testfälle gebildet, die hauptsächlich Grenzwerte der erwarteten Eingabeparameter beinhalten. Weiterhin werden Testfälle erzeugt, die eine kleinste Einheit größer oder kleiner sind als der Grenzfall. Wenn z.B. eine Methode bis zum ganzzahligen Eingabewert 1.000 einen anderen Rückgabewert liefert als für Eingabewerte gleich oder größer 1.000, so werden als Testfälle neben 1.000 auch 999 und 1.001 ermittelt. Somit wird sogenannten one-off-Fehlern vorgebeugt, die häufig bei nullbasierten Indizes oder auch z.B. durch ein > anstatt ein >= entstehen können. (TS04) 3.4 Whitebox-Tests Unter Whitebox-Tests, Transparentbox-Tests, Glassbox-Tests oder codebasierten Test, versteht man Tests, die auf der Programmstruktur oder Programmlogik basieren. Dem Tester ist also ein Einblick in den Quellcode der Software möglich, um die Testfälle zu erstellen und die Software zu testen und das Verhalten zu analysieren. Somit kann mit Whitebox-Tests tiefer in die Strukturen des Programmes vorgedrungen werden. Vollständige Whitebox-Tests sind aufgrund des hohen Aufwands nur sehr selten bzw. nur bei sehr kleinen Programmen durchführbar. (Möl08) 24 3 Grundlegendes zu Softwaretests 3.4.1 Statische Analyse Die Analyse gehört zu den statischen Tests. Dies sind Tests, bei denen die Software nicht ausgeführt, sondern nur statisch betrachtet und untersucht wird. Als Grundlage für statische Tests dient der Quellcode des Programmes (ebd.). Im Allgemeinen werden bei statischen Tests keine Testdaten benötigt. Die folgenden Verfahren analysieren statisch den Quellcode (TS04): • Review • Code-Inspektion • Walkthrough Da eine solche Analyse meist von mehreren Menschen durchgeführt wird, wird sie auch Gruppenprüfung genannt. Ziele einer solchen gemeinsamen Prüfung sind das Aufspüren von Fehlern, die Optimierung und die Überprüfung der Einhaltung von Standards und Richtlinien. Den Nutzen einer solchen Gruppenprüfung zeigt ein Erfahrungswert der Firma Siemens, bei dem 491 Mängel bei insgesamt 13 Gruppenprüfungen entdeckt wurden. (Ger08) Eine solche Analyse kann automatisch und programmunterstützt ablaufen. Als Beispiel dient hier das Analyse-Tool FxCop (vgl. Abschnitt 4.2.1 auf Seite 42), welches den Quellcode anhand von Microsoft-Richtlinien oder selbst definierten / programmierten Regeln prüft und Fehler im Code moniert. 3.4.2 Strukturelle Analyse Kontrollflussanalyse dient der Analyse der Kontrollstrukturen der Software. Kontrollstrukturen sind Kontroll- oder Steueranweisungen, die auch als Schlüsselwörter bekannt sind. Dazu gehören in C# Anweisungen wie if, else, switch, for, foreach, while oder do. Unterbrechende oder weiterführende Anweisungen wie break, return oder continue spiegeln sich ebenfalls in einem solchen Graph wieder. Für die Analyse dieser Schleifen- und Verzweigungsstrukturen ist ein vorzugsweise automatisch generierter Kontrollflussgraph notwendig. Anhand eines solchen Graphen können Anomalien in der Programmstruktur entdeckt werden. Mögliche Anomalien sind folgende (Ger08): • Sprünge in oder aus einen Schleifenkörper • Methoden mit mehreren Ausgängen • Unerreichbarer Code 25 3 Grundlegendes zu Softwaretests Solche Strukturfehler können harmlose oder überhaupt keine Auswirkungen haben, ebenso sind hier aber viele potentielle Fehler möglich. Das Prüfen der Strukturen einer Software wird heutzutage allerdings von vielen Compilern unterstützt, welcher unerreichbaren Code oder nicht initialisierte Variablen automatisch während des Kompiliervorgangs mit einer Warnung anzeigt. 3.4.3 Überdeckungstests Hauptsächlich handelt es sich bei Whitebox-Tests um kontrollflussorientierte Tests. In einem Kontrollflussgraphen werden Anweisungen oder Anweisungsblöcke als Knoten, und die Richtung eines Kontrollflusses als gerichtete Kanten zwischen Knoten dargestellt. Ein Zweig ist eine Einheit aus einer Kante und den dadurch verbundenen Knoten. Ein Pfad ist eine Sequenz von Knoten und Kanten, die durch einen Start- und Endknoten abgeschlossen sind. Verzweigungen stellen Steueranweisungen wie if oder for dar. Bei kontrollflussbezogenen Verfahren wird das Prinzip der Überdeckung von Kontrollstrukturen verfolgt, indem Kontrollstrukturen gezielt durch geeignete Gestaltung von Testfällen durchlaufen werden. Am Ende wird ein angestrebter und ein erreichter Überdeckungsgrad in % angegeben. Solche Tests werden Überdeckungstests genannt. Im Folgenden wird auf die einzelnen Stufen der Überdeckungstests eingegangen. (Möl08) • Anweisungsüberdeckung C0 (Statement Coverage) Bei einer Anweisungsüberdeckung, welche die schwächste Art darstellt, werden alle Anweisungen mindestens einmal ausgeführt. Sie basiert auf dem Kontrollflussgraphen, der üblicherweise vom Quellcode abgeleitet wird. Bei der Anweisungsüberdeckung werden keine leeren Zweige getestet. Dadurch können auch eventuell fehlende (nicht implementierte) Anweisungen nicht entdeckt werden. Die Überdeckungsrate ermittelt sich wie folgt: (TS04) Anweisungsüberdeckung = Anzahl ausge f ührte Anweisungen Gesamtzahl der Anweisungen • Zweigüberdeckung C1 (Branch Coverage) Bei der Zweigüberdeckung werden alle Zweige („Wegabschnitte“) im Kontrollfluss mindestens einmal durchlaufen. Bei der Zweigüberdeckung wird nicht berücksichtigt, wie oft ein Zweig durchlaufen wird. Mit dieser Methode könnten feh- 26 3 Grundlegendes zu Softwaretests lende (nicht implementierte) Anweisungen entdeckt werden. Die Überdeckungsrate wird ebenfalls in % angegeben und ermittelt sich wie folgt: (ebd.) Zweigüberdeckung = Anzahl ausge f ührte Zweige Gesamtzahl der Zweige Bei der Zweigüberdeckung werden im Allgemeinen mehr Testfälle benötigt, da auch leere Zweige berücksichtigt werden. (ebd.) • Bedingungsüberdeckung C2 (Condition Testing) Bei den bisherigen Überdeckungsmethoden wurden immer nur ganze Bedingungen betrachtet. In der Realität bestehen Bedingungen aber oft aus komplexen, mit booleschen Operatoren vermengten Ausdrücken. Um diese in atomare Teilbedingungen aufzugliedern, wird die Bedingungsüberdeckung eingesetzt. Es werden alle Konstellationen der atomaren Bedingungen genutzt, die insgesamt für jede Teilbedingung einmal false und einmal true ergibt. (ebd.) • Mehrfach-Bedingungsüberdeckung C3 (Branch Condition Combination Testing) Bei der Mehrfach-Bedingungsüberdeckung ergeben sich alle Testfälle, um alle Konstellationen der atomaren Teilbedingungen abzudecken. Deswegen ist die Mehrfach-Bedingungsüberdeckung ein stärkeres Kriterium als die Bedingungsüberdeckung. Nachteil ist allerdings, dass die Anzahl der Testfälle exponentiell steigt (2n mit n atomaren Teilbedingungen). (ebd.) • Pfadüberdeckung C4 (Path Coverage) Die Pfadüberdeckung ist die stärkste aller Formen und abgesehen von sehr kleinen Programmen nur theoretisch möglich. Hierbei werden alle Pfade komplett durchlaufen. Für Schleifen gilt folgende Forderung (ebd.): – enthaltene Schleifen nicht durchlaufen – enthaltene Schleifen nicht oft durchlaufen – enthaltene Schleifen oft durchlaufen Das Ziel von dynamischen Whitebox-Tests ist, eine möglichst hohe Überdeckungsrate zu erreichen, bei der so viele Anweisungen, Zweige oder Pfade wie möglich durchlaufen wurden. Nachteile des Verfahrens sind, dass keine nicht implementierten Anforderungen erkannt werden, da sich die Tests am Quellcode orientieren und nicht an der Spezifikation der Software. Weiterhin können Fehler, die von bestimmten Datenkonstellationen abhängig sind, nur sehr schwer entdeckt werden (Möl08). Als Ergänzung hierfür eignen sich statische Analysen (vgl. Abschnitt 3.4.1 auf Seite 25), um nicht implementierte Features zu entdecken. (TS04) 27 3 Grundlegendes zu Softwaretests 3.5 Testverfahren Bei Testverfahren gibt es die unterschiedlichsten Vorgehensweisen und Kombinationen von verschiedenen Vorgehensweisen. Zu Beginn sollen Top-Down- und Bottom-UpTests erwähnt werden. Bei Top-Down-Tests wird die Software von oben herab getestet. Das heißt, die Tests beginnen an der äußersten Schicht einer Software, was im Allgemeinen die graphische Benutzeroberfläche darstellt. Hierfür eignen sich GUI-Tests (vgl. Abschnitt 3.6.2 auf Seite 33). Bei Bottom-Up-Tests beginnen die Tests unten in der Quellcodestruktur, womit Methoden und Klassen gemeint sind. Hierbei eignen sich Unit-Tests (vgl. Abschnitt 3.6.1 auf Seite 32). (vV08) Es gibt viele Modelle, um Software zu entwickeln. Am Anfang war das Testen kein Bestandteil eines solchen Modells. Zu Beginn wurde das Wasserfallmodell verwendet. Nachdem aber recht schnell bemerkt wurde, dass dieses Modell für Software ungeeignet ist, da sich keine Änderungen auf vorhergehende Phasen im Modell vornehmen lassen, wurde das Wasserfallmodell etwas angepasst. Allerdings reichte auch das nicht. Es entstand das V-Modell, welches sich sehr gut für Software eignete. Erstmals wurden auch Tests direkt in dem Modell ersichtlich (siehe Abbildung 3.1). Abbildung 3.1: Allgemeines V-Modell (Spi08) 28 3 Grundlegendes zu Softwaretests Die enthaltenden Tests verdeutlichen, welchen Umfang oder welche Bereiche der Software zu welchen Zeitpunkten getestet werden sollten. Der Anforderungsdefinition steht der allumfassende Systemtest gegenüber, dem Systementwurf steht der Integrationstest gegenüber und der Spezifikation der Komponenten steht der Komponententest gegenüber. Wichtig ist hierbei zu erwähnen, dass die jeweiligen Spezifikationen die Grundlage der dazugehörenden Tests bilden und nicht die entwickelten Komponenten oder das System an sich. Der größte Nachteil allerdings ist, dass jegliche Testaktivitäten zu spät beginnen. Aus diesem Grund wurde das W-Modell entwickelt, um die Nachteile des VModells zu beheben (siehe Abbildung 3.2). Abbildung 3.2: Allgemeines W-Modell (Spi08) Das W-Modell integriert die Tests und auch die Vorbereitung komplett in das gesamte Projekt. Verschiedene Tests sind zu unterschiedlichen Zeitpunkten möglich, nötig oder sinnvoll, was wiederum bedeutet, dass nicht jeder Test zu jedem Zeitpunkt möglich, nötig oder sinnvoll ist. Die im W-Modell genannten Tests werden im Folgenden kurz erläutert. 29 3 Grundlegendes zu Softwaretests 3.5.1 Komponententest Von der Hierarchie her betrachtet finden Komponententest ganz unten statt. Hier werden einzelne Klassen oder entwickelte Module während der Entwicklung getestet. Klassische Tests hierfür sind Unit-Tests (vgl. Abschnitt 3.6.1 auf Seite 32). Komponenten werden individuell und unabhängig voneinander getestet. Diese Tests sind relativ einfach, allerdings entstehen im Laufe einer Entwicklung sehr viele Unit-Tests (Ger08). Fuzzing (vgl. Abschnitt 3.6.3 auf Seite 33) kann sich ebenfalls sehr gut eignen, um Fehler in Komponenten aufzuspüren. 3.5.2 Integrationstest Diese Art von Tests findet in der Testhierarchie etwas später statt, wenn bestimmte, klar definierte Teile eines Systems fertigentwickelt sind. Bei den Teilsystemen werden Schnittstellen nach außen bzw. zu anderen Teilsystemen sowie das Zusammenspiel von fertigentwickelten Komponenten getestet. Hierbei sind diese Komponenten nicht mehr unabhängig voneinander (ebd.). Auch hierfür können Unit-Tests (vgl. Abschnitt 3.6.1 auf Seite 32) benutzt werden, allerdings werden diese nicht mehr auf die elementaren Bestandteile der Software angewendet, sondern auf Teilsysteme, die andere Teilsysteme benutzen und somit ein Zusammenspiel im Vordergrund steht. Sollten diese Teilsysteme eine graphische Benutzeroberfläche haben, so eignen sich hier auch schon GUI-Tests (vgl. Abschnitt 3.6.2 auf Seite 33). 3.5.3 Systemtest Ein allumfassender Systemtest findet, wie im Modell ersichtlich, am Ende einer Entwicklung statt und befindet sich somit ganz oben in der Hierarchie. Diese Art von Tests kann als Testphase definiert werden, bei der das gesamte System gegen die Spezifikationen getestet wird. Es kann ein funktionaler und ein nicht funktionaler Systemtest durchgeführt werden. Ein funktionaler Systemtest prüft die Software auf Merkmale wie Korrektheit und Vollständigkeit. Ein nicht funktionaler Systemtest testet Merkmale wie die Benutzbarkeit oder die Erlernbarkeit einer Software. Neben den Funktionstests (vgl. Abschnitt 3.3 auf Seite 23) werden weitere, umfassende Tests durchgeführt, welche u.a. im Folgenden erläutert werden (Bal08): • Leistungstest Zu dieser Gruppe gehören u.a. Load-Tests, Stress-Tests, Zeittests oder Massentests. Diese Tests analysieren das Verhalten der Software z.B. beim Laden (Load- 30 3 Grundlegendes zu Softwaretests Test) oder unter einer permanenten Ausführung (Stresstest), was ggf. eine Ausnahmesituation darstellen kann. Somit wird versucht herauszufinden, inwieweit sich die Performance der Software verhält. So können Fehler gefunden werden, die bei der Initialisierung, bei einer permanenten Dauerausführung oder sogar einer Überlastung des Systems entstehen. Ein korrektes Systemverhalten bei bestimmten Speicher- und CPU-Anforderungen soll sichergestellt werden. Auch verteilte Anwendungen und Multi-User-Tests, in dem viele Anwender auf ein System zugreifen, simuliert oder real, fallen unter die Gruppe der Leistungstests. Weiterhin gehört ein simulierter Ausfall von Hard- oder Softwarekomponenten, mit denen das System im Normalbetrieb kommuniziert (z.B. Sensoren), sowie das Eintreffen von ungewöhnlichen oder gar widersprüchlichen Daten, z.B. über eine Schnittstelle, ebenfalls zu den Leistungstests. Massentests prüfen die Datenmenge, die vom System verarbeitet werden kann. Zeittests prüfen die Einhaltung von zeitlichen Restriktionen. • Sicherheitstests Hierbei werden allgemein gültige oder geforderte Sicherheitsmaßnahmen geprüft. Es geht hauptsächlich um sicherheitsrelevante Daten oder Passwörter. Es wird u.a. geprüft, ob Passwörter unsichtbar erfasst und verschlüsselt gespeichert werden, ob der Benutzer sein Kennwort ändern kann, ob die vorgeschriebene Mindestlänge oder enthaltene Zeichen bei Passwörtern eingehalten oder ob Zugriffsrechte- und verbote geprüft, eingehalten und gemeldet werden. • Interoperabilitätstests Interoperabilitätstests testen die Funktionalität bei der Zusammenarbeit voneinander unabhängiger Komponenten. Da Software heutzutage im Allgemeinen mit anderer Software oder anderen Softwareteilen zusammenarbeitet, müssen diese Kommunikationswege ebenso getestet werden. Ein simples Beispiel ist diese Kommunikation (Hineinschreiben und Auslesen) zur Zwischenablage. • Installationstests Installationstests testen die Softwareinstallationsroutinen ggf. in verschiedenen Systemumgebungen (z.B. verschiedene Hardware oder unterschiedliche Betriebssystemversionen). Weiterhin wird auch getestet, ob das System mit der im Handbuch beschriebenen Vorgehensweise korrekt installiert und in Betrieb genommen werden kann. Hier bieten sich ebenfalls GUI-Tests an (vgl. Abschnitt 3.6.2 auf Seite 33). Unit-Tests (vgl. Abschnitt 3.6.1 auf der nächsten Seite) werden auf dieser Ebene eher selten ange- 31 3 Grundlegendes zu Softwaretests wendet, da die Zusammenspiele von Teilsystemen im Gesamtsystem mit GUI-Tests oft besser getestet werden können. All diese Tests werden ohne den Auftraggeber durchgeführt. 3.5.4 Abnahmetest Ähnlich einem Systemtest wird der Abnahmetest oder auch User Acceptance Test (UAT) an der fertigen Software durchgeführt. „Fertig“ ist hier aus Kundensicht zu sehen und Voraussetzung für eine Rechnungsstellung. Der Unterschied zum Systemtest ist, dass hierbei der Auftraggeber mithilft und z.B. echte Kundendaten für die Tests genutzt werden, um die Software unter realen Einsatzbedingungen zu testen. Das Benutzerhandbuch wird nun gänzlich in die Tests mit einbezogen. Auch hierbei bieten sich GUI-Tests an (vgl. Abschnitt 3.6.2 auf der nächsten Seite). Selbst nach der Abnahme befinden sich noch Restfehler im Softwaresystem, sowohl bekannte als auch unbekannte. (Bal08) 3.5.5 Regressionstests Regressionstests sind alle Tests, die jederzeit wiederholt werden können und somit Regressionsfähigkeit besitzen, sofern sich das System nicht grundlegend ändert und Tests angepasst werden müssen. Unit-Tests (vgl. Abschnitt 3.6.1) oder auch GUI-Tests (vgl. Abschnitt 3.6.2 auf der nächsten Seite) sind im Allgemeinen solche Regressionstests. Sie haben das Ziel, bei Änderungen unerwünschte Seiteneffekte auf unmodifizierte Teile der Software zu erkennen (Regressions-Fehler) und die Sicherstellung der Erfüllung der Anforderungen des modifizierten Systems. (Ger08) 3.6 Konkrete Testverfahren Bisher wurden die Tests kategorisiert bzw. verschiedene Testarten vorgestellt. Es sollen nun einige der gängigsten Testmethoden konkret genannt werden. Die hier erwähnten Testmethoden sind nur ein geringer Teil vieler möglicher Tests. Viele Testmethoden werden von Test-Tools unterstützt. Teilweise laufen einige dieser Tests parallel ab. 3.6.1 Unit-Tests Unit-Tests (dt. Komponententests) sind weit verbreitete Tests, mit denen Module oder Komponenten von Software getestet werden können. Sie werden im Quellcode vom Pro- 32 3 Grundlegendes zu Softwaretests grammierer des Testobjekts verfasst und rufen die zu testenden Methoden mit bestimmten Parametern auf. Nach dem Aufruf wird der Rückgabewert mit einem erwarteten Wert verglichen, der vorher mit Hilfe des Testorakel oder des Quellcodes ermittelt wird. In vielen Quellen werden diese Tests eher den Whitebox-Tests zugeordnet (Sil08). Einige Quellen sprechen von einer Schnittmenge und ordnen so Unit-Tests teilweise den WhiteboxTests und teilweise den Blackbox-Tests zu (See08). Wieder andere ordnen sie ausschließlich den Blackbox-Tests zu, da Unit-Tests lediglich den Rückgabewert vergleichen und nicht die internen Strukturen einer Methode analysieren (Dai08). Aufgrund dieser Widersprüchlichkeiten wird festgelegt, dass es sich bei Unit-Tests um Blackbox-Tests handelt, sofern die Testmethoden und auch die Testfälle ohne einen Einblick in den Quellcode des Testobjekts erstellt wurden oder erstellt werden könnten. Um Whitebox-Tests handelt es sich, wenn die Testfälle anhand des Quellcodes des Testobjekts abgeleitet wurden. UnitTests können schon vor dem Implementieren der zu testenden Methoden erstellt werden. Dieses Programmiermodell wird Test Driven Development (TDD) genannt. 3.6.2 GUI-Test Bei GUI-Tests, UI-Test (User Interface) oder Top-Down-Test ist der Testtreiber in der Lage, eigenständig Benutzereingaben zu simulieren. Diese Tests sind Blackbox-Tests, da lediglich die GUI des Programmes als Schnittstelle dient. Die korrekte Zusammenarbeit wird mit simulierten Benutzereingaben und somit realen Testeingaben getestet. Wenn die erwarteten Ergebnisse entstehen, dann ist davon auszugehen, dass alle beteiligten Module und Klassen korrekt arbeiten. Als Grundlage für GUI-Tests dienen sogenannte Handle, welche lediglich als eindeutige Nummern verstanden werden können. Diese Handle identifizieren alle Steuerelemente wie Buttons, Labels oder auch Textboxen. Alle Steuerelemente sind durch ein eindeutiges Handle bestimmt und können über dieses angesprochen werden. So ist es z.B. möglich, an Buttons einen Klick zu senden, als wenn der Benutzer ihn mit der Maus angeklickt hätte. Weiterhin ist es möglich, Text in eine Textbox zu schreiben und auf diese Weise Benutzereingaben zu simulieren. Das Auslesen ist ebenfalls über das Handle möglich. 3.6.3 Fuzz-Test Fuzz-Tests wurden 1989 von Professor Barton Miller an der Universität Wisconsin entwickelt. Miller bemerkte im Herbst 1988, dass bestimmte, zufällig entstehende Geräusche auf einer Telefonleitung diese zum Absturz bringen konnten. Dieses Phänomen 33 3 Grundlegendes zu Softwaretests wollte er damals im Rahmen eines Projektes untersuchen. Er prägte den Begriff „Fuzzing“. (Mil08) Die Fuzz-Tests wurden ursprünglich für Unix-Programme entwickelt. Beim Fuzz-Test werden automatisiert zufällige Zeichenketten in verschiedenen Längen generiert und diese als Eingabeparameter für das Programm benutzt. Nachdem das Programm mit diesen Daten gelaufen ist, wird geprüft, ob es korrekt ausgeführt wurde oder ob es sogar abgestürzt ist. Aus einer Veröffentlichung von Miller und seinen beiden Studenten Fredriksen und So 1990 geht hervor, dass fast ein Drittel aller damals mit dieser Methode getesteten Programme abgestürzt sind, darunter auch renommierte Programme wie vi, telnet und emacs (MFS90). In Tabelle 3.2 auf der nächsten Seite wird eine Auflistung aller Programme gezeigt, die beim damaligen Test abgestürzt sind. Diese Vielzahl von abgestürzten Programmen zeigt, wie wichtig und effektiv diese Testmethode sein kann. Allerdings reichen nur zufällig generierte Daten nicht aus, da viele Programme bei Optionen, die sie nicht unterstützen, gar nicht erst den eigentlichen Programmcode ausführen und somit nicht mit den zufällig generierten Werten arbeiten. Diese Zufallswerte müssen deswegen einer gewissen Logik entsprechen, damit die Tests wirklich sinnvoll durchgeführt werden können. Eine solche Logik ist auch bei heutiger Software nötig, obwohl diese nicht unbedingt auf Startparameter angewiesen ist, wie die früheren Konsolenanwendungen. Fuzzing wird u.a. von Hackern benutzt und diente dazu, Sicherheitslücken aufzudecken und diese destruktiv zu nutzen. Fuzzing an sich ist nicht auf Software begrenzt. Es wurde schon erfolgreich in mehreren Anwendungsgebieten angewendet, um die dortigen Systeme zu testen. Dazu gehören z.B. Netzwerkprotokolle, die mit Fuzz-Tests auf ihre Sicherheit hin untersucht und getestet werden konnten. Es werden, wie beim Fuzzing üblich, diverse zufällig erzeugte Pakete verschickt und geprüft, ob sich das Netzwerk mit diesen ungültigen Paketen richtig verhält. Ähnlich arbeiten auch WEB-Fuzzer, die HTML-Seiten mit zufällig generierten und somit ungültigen HTML-Tags erzeugen. Die Browser müssen in der Lage sein, diese ungültigen Seiten zu behandeln und dürfen auf keinen Fall in einen ungültigen Zustand verfallen, was einer Sicherheitslücke gleich käme. Auf diese Art wurden schon einige Fehler gefunden, u.a. auch in den renommiertesten Browsern wie Microsoft Internet Explorer, Mozilla Firefox oder Opera (Pol08). Weiterhin gibt es auch sogenannte file fuzzers, die ungültige Dateien erzeugen und mit einer bestimmten Zielanwendung öffnen. Der erste Schritt beim Fuzzing ist das Erzeugen der Eingabedaten. Dabei wird unterschieden zwischen dem Generieren von zufälligen Daten (generation Fuzzing) oder 34 3 Grundlegendes zu Softwaretests Utility array/ NCRC pointer adb as bc cb ccom col csh dc deqn deroff diction ditroff emacs eqn f77 ftp indent join lex look m4 make nroff plot prolog ptx refer spell style telnet tsort ul uniq units vgrind vi vshx vxad input subinteraction bad error signed race no source unknown functions processes effects handler characters condition code v x vhxad d vshxad vshra vshra x s vshad vhd vs vsh shx v vshad vshd s vshxad vshxd x h x sh vsh shxd vshd vshxad vhd vshad vshxad vshd vshxad vshxad v v vh Tabelle 3.2: Liste der beim damaligen Test abgestürzten Programme, kategorisiert nach Fehlerursache. (Die Buchstaben geben das System an, auf dem der Absturz hervorgerufen wurde (siehe Tabelle 3.3)) (MFS90) Buchstabe v s h x r a d Hersteller/Modell DEC VAXstation 3200 Sun 4/110 HP Bobcat 9000/320 HP Bobcat 9000/330 Citrus 80386 IBM RT/PC IBM PS/2-80 Sequent Symmetry Prozessor CVAX SPARC 68020 68030 i386 ROMP i386 i386 Kernel 4.3 BSD + NFS (from Wisconsin) SunOS 3.2 & SunOS 4.0 with NFS 4.3BSD + NFS (from Wisconsin), with Sys V shared-memory SCO Xenix System V Rel. 2.3.1 AOS UNIX AIX 1.1 UNIX Dynix 3.0 Tabelle 3.3: Liste der getesteten Systeme (MFS90) 35 3 Grundlegendes zu Softwaretests dem Modifizieren vorhandener Daten (mutation Fuzzing). Beim Modifizieren vorhandener Daten liegen meist schon gültige Daten vor, die dann in einem gewissen Grad verändert werden. Zum Beispiel könnte ein gültiges XML-Dokument dahingehend manipuliert werden, dass die XML Struktur erhalten bleibt und nur die von den Tags eingeschlossenen Daten modifiziert werden. Somit ist sichergestellt, dass diese Daten vom Programm trotzdem als gültig erkannt und verarbeitet werden und nicht von vornherein durch den XML-Parser verworfen werden. (Wei08) Im zweiten Schritt müssen die erzeugten Daten zur Zielsoftware gebracht und dort benutzt werden. Dies geschieht durch ein Tool, welches die Zielsoftware startet und die generierten oder modifizierten Daten benutzt. Im letzten und wichtigsten Schritt muss das Verhalten der Software beobachtet und protokolliert werden. Zu den drei wichtigsten Auswirkungen gehören Abstürze, Speicherlecks und Prozessorauslastungslecks. Abstürze sind mit die schlimmsten Fehler, die in einer Software auftreten können. Speicherlecks entstehen, wenn mehr Speicher reserviert als freigegeben wird. Prozessorauslastungslecks entstehen z.B., wenn sich eine Funktion in einer Endlosschleife verfängt. (Wei08) Der wohl größte Nachteil dieser Technik ist, dass die eigentliche logische Funktion der Software nicht testbar ist. Mit den Eingabeparametern einer Methode kann kein logischer Schluss auf die zu erwartenden Ausgabewerte gezogen werden. Selbst wenn die Methode nicht abstürzt, steht nicht fest, ob sie fehlerfrei ausgeführt wurde. Ein sehr großer Vorteil gegenüber allen anderen Testmethoden ist die hohe Anzahl von Testfällen, die generiert werden kann, ohne dass es den Programmierer vorher viel Zeit kostet, aufwendige Tests zu entwickeln. Durch das Erstellen der Testdaten kann der Test vollautomatisch durchgeführt werden und erfordert keine zeitintensive Vorbereitung oder das manuelle Festlegen von Sollwerten. Deshalb ist hierbei eine nicht unerhebliche Fehlerquelle ausgeschlossen worden; nämlich dass auch der entwickelte Test fehlerhaft sein kann. 3.6.4 Datengesteuerte Tests Bei datengesteuerten Tests (engl. data-driven tests) wird eine Anwendung mit simulierten Daten getestet. Daten, wie z.B. Namen, Adressen o.ä., werden generiert und in einer Datenquelle, z.B. einer Datenbank oder einer Datei, abgelegt. Anwendungen, z.B. mit einer graphischen Benutzeroberfläche, werden dann mit Hilfe dieser Daten getestet. Der Testtreiber setzt mittels eines GUI-Tests (vgl. Abschnitt 3.6.2 auf Seite 33) diese Da- 36 3 Grundlegendes zu Softwaretests ten in die entsprechenden Felder der Maske ein. Eine andere Art von Test wäre, wenn diese Daten mittels eines Fuzz-Tests (vgl. Abschnitt 3.6.3 auf Seite 33) als Parameter für eine Methode genutzt werden. Denkbar wäre eine Methode, die Daten wie z.B. einen Kunden mit Name und Adresse in einer Tabelle einer Datenbank speichert. Der Testtreiber gibt diese Daten vor und prüft nach dem Test, ob diese Daten korrekt wie erwartet in der Tabelle eingetragen wurden. 3.7 Zusammenfassung Es existieren sehr viele unterschiedliche Arten und Verfahren von Tests. Ebenso existieren viele Mischformen oder Überschneidungen von Testverfahren. Alle hier vorgestellten Verfahren sind automatisierbar. Je nach Grad der Automatisierung werden viele sonst aufwendige Aufgaben übernommen und automatisch durchgeführt. Der wohl größte Vorteil ist dabei, dass automatisierte Tests jederzeit konsistent sind. Der Tester, also meist eine Software, wird niemals müde oder unkonzentriert. Weiterhin werden menschliche Fehler wie Tippfehler vermieden (unter der Annahme, dass keine solchen Fehler bei der Bereitstellung der Testdaten vorliegen). Aus dieser Arbeit, vornehmlich diesem Kapitel, resultiert die Erkenntnis, dass Testen ein sehr wichtiger Bestandteil des Softwareentwicklungsprozesses ist. Weiterhin gilt, dass, außer bei sehr kleinen und unkomplizierten Programmen, sehr selten alle Fehler gefunden werden können. Selbst wenn alle hier genannten Testverfahren intensiv, gewissenhaft und erfolgreich durchgeführt wurden, so kann die Software immer noch Fehler enthalten. Jeder Fehler, der während des Testens gefunden wird, ist ein kleiner Erfolg. Tritt dieser Fehler erst später beim Kunden auf, so entsteht meist ein wesentlich höherer finanzieller Schaden. 37 Vorhandene Testsysteme [4] 4.1 „Jedes Denken wird dadurch gefördert, dass es in einem bestimmten Augenblick sich nicht mehr mit Erdachtem abgeben darf, sondern durch die Wirklichkeit hindurch muss.“ Albert Einstein, dt.-amerik. Physiker TestFrameworks 4.2 Statische Codeanalyse Auf dem heutigen Softwaremarkt gibt es eine Fülle unterschiedlichster Testsysteme. 4.3 Das Spektrum reicht von kostenlos bis „unbezahlbar“. Einige dieser Testsysteme sollen GUI-Tests hier betrachtet und Vor- und Nachteile analysiert werden. Zu den bekanntesten Testsys- 4.4 temen gehören einerseits Test-Frameworks und andererseits GUI-Tests. Zudem gibt es Zusammen- noch statische Codeanalysen. fassung 4.1 Test-Frameworks In diesem Kapitel werden die Test-Frameworks etwas genauer betrachtet. Sie dienen als Unterstützung bei der Durchführung der Tests und sollen bei der Auswertung der Tests helfen, entstandene Fehler zu protokollieren. Zudem sollen sie viele lästige Schritte beim Testen abnehmen (SGA07). 4.1.1 NUnit / JUnit NUnit und JUnit sind Test-Frameworks, die Module oder Komponenten einer Software auf ihre Korrektheit hin prüfen können. Dafür erstellt der Programmierer zu jeder seiner programmierten Klassen eine Testklasse, welche dann die eigentliche Quellklasse testet. NUnit war bisher eine externe Anwendung, die alle in einer Assembly enthaltenden Testmethoden ausführt und Testergebnisse anzeigt (siehe Abbildung 4.1 auf der nächsten Seite). 38 4 Vorhandene Testsysteme Abbildung 4.1: NUnit - Erfolgreicher Gesamttest Eine solche Testklasse wird mit gesonderten Attributen versehen, genauso wie die enthaltenen Testmethoden. Idealerweise befinden sich diese Klassen in einem separaten Projekt, da sich sonst Quellcode und Testcode zu stark vermischen. Ob die Klasse korrekt funktioniert, erfährt der Programmierer direkt nach dem Ausführen des Unit-Tests. So können Fehler schon frühzeitig erkannt und direkt behoben werden. Auch bei späteren Änderungen oder weiteren Implementierungen ist zu jedem Zeitpunkt sichergestellt, dass die Methoden weiterhin richtig arbeiten und sich die aktuellen Änderungen nicht negativ auf schon bestehende Klassen auswirken. Ab Microsoft Visual Studio 2005 Team Edition for Developer / Tester und Microsoft Visual Studio 2008 wird die Erstellung von NUnit-Testklassen automatisch durch einen integrierten Assistenten unterstützt. Der Assistent eröffnet dem Programmierer die Möglichkeit frei zu entscheiden, welche Klassen, Methoden oder auch welche Properties getestet werden sollen. Der Assistent erzeugt ein Grundgerüst, welches auf die Quellklasse zugeschnitten ist. Eine häufig diskutierte Frage war, ob private Methoden getestet werden sollten. Dies war bisher mit NUnit nur sehr schwer möglich. Dieser Nachteil wurde durch die Integration ausgeräumt. Der Assistent erzeugt bei einer privaten Methode eine Klasse, welche die Methode kapselt. Somit kann über einen Umweg, den der Programmierer allerdings gar nicht bemerkt, auch diese private Methode getestet werden. Die eigentlichen 39 4 Vorhandene Testsysteme Inhalte dieser Tests müssen natürlich vom Programmierer selbst implementiert werden, da der Assistent von sich aus keine Testwerte festlegt. Nach Fertigstellung einer Klasse kann einfach per Rechtsklick ein komplettes NUnit-Testprojekt erstellt werden (siehe Abbildung 4.2). Abbildung 4.2: Erzeugen eines NUnit-Testprojektes mit Hilfe des Assistenten Mit steigender Komplexität der Quellsoftware steigt ebenfalls die Komplexität der Tests sehr stark. Ein solcher Test kann sehr umfangreiche Ausmaße annehmen. Die Praxis zeigt, dass es oft um ein Vielfaches länger dauert, einen Test für eine Quellklasse zu schreiben, als die Quellklasse selbst. Genauso wie die Quellklasse Fehler enthalten kann, kann auch die Testklasse fehlerhaft sein. Somit könnte in einer ungünstigen Konstellation eine fehlerhafte Quellklasse, die mit einer fehlerhaften Testklasse getestet wird, wieder ein erfolgreiches Ergebnis liefern, obwohl die Klasse nicht ordnungsgemäß arbei- 40 4 Vorhandene Testsysteme tet. Dieser Fall ist zwar äußerst selten, soll aber im Rahmen dieser Arbeit nicht gänzlich vernachlässigt werden. Ein weiterer Nachteil ist, dass sich der Test-Code zu sehr mit dem eigentlichen Quellcode vermischt. Jede Zeile Code, die in den ursprünglichen Quellcode eingefügt werden muss, um diesen zu testen, macht ihn unübersichtlicher und schwerer zu warten. Es gibt Ansätze bei NUnit, wo teilweise die Testklassen von den Quellklassen erben, um so tiefgreifendere Tests durchführen zu können (HT04). Dieser Nachteil ist zwar größtenteils durch die Integration in Microsoft Visual Studio überwunden worden, allerdings noch immer erwähnenswert. Was ebenso ein Problem darstellt, ist die falsche Benutzung des Test-Frameworks. Je komplexer die Software ist, desto schwieriger ist es die Übersicht zu behalten. So können durchaus Methoden oder Properties von Klassen auf ihre Funktionalität hin getestet werden, die aufgrund von Vererbung ursprünglich vom .NET Framework bereitgestellt wurden. Die investierte Zeit für die Erstellung der Testklasse und die Durchführung der Tests erhöht nicht die Qualität der Software, obwohl dies bei erfolgreichem Durchlaufen der Tests den Anschein hat. „Wenn irgendein Teil einer Maschine falsch eingebaut werden kann, so wird sich immer jemand finden, der dies auch tut.“ Murphy’s Gesetze 4.1.2 Fuzzing Eine weitere Methode für automatische Tests ist das sogenannte Fuzzing-Framework. Es beschreibt die automatisierte Suche nach Programmierfehlern mit zufällig generierten Werten als Eingangsparameter für die zu testenden Programme. Es übernimmt auch Aufgaben wie das Protokollieren der Ausgabewerte oder das Verhalten der Testobjekte. Diese Aufgaben sind ein essentieller Teil beim Testen. Ein solches Fuzzing-Framework ist die „Peach Fuzzing Platform“. Diese Software kann laut eigenen Angaben folgende Software mit Fuzzing testen (Edd08): • Netzwerkprotokoll-Parser • X-Schichten-Anwendungen • Datei-Parser • COM und ActiveX-Komponenten 41 4 Vorhandene Testsysteme Auch Microsoft hat Teile ihrer Software mit Fuzzing getestet. Einem Bericht von Microsoft Research zufolge wurde eine Whitebox-Fuzzing-Technik genutzt, welche auf einer kontextfreien Grammatik beruht. Damit wurde der JavaScript-Interpreter vom Internet Explorer 7 getestet. Das Problem bei einer Software, wie z.B. einem Interpreter ist, dass bevor ein Text interpretiert werden kann, dieser genau untersucht wird, ob jegliche Regeln beachtet wurden. Bei diesen Regeln handelt es sich um JavaScript. Das bedeutet, dass erst nach sehr vielen Prüfungen, die ein Lexer oder ein Parser vornimmt, der eigentliche Interpreter ausgeführt wird. Sollten vorher irgendwelche Unstimmigkeiten in dem zu interpretierendem Text enthalten sein, wird dieser sofort abgelehnt. Microsoft machte den Quelltext zur Grundlage und entwickelte einen Fuzzer, der anhand einer Grammatik einen von der Grammatik her gültigen Ausgabetext erzeugte, mit der der Interpreter getestet werden konnte. Laut eigenen Angaben wurde eine Überdeckungsrate von 81% erreicht. Herkömmliche Methoden erreichten dabei nur eine Überdeckungsrate von 51%. (PGL08) 4.2 Statische Codeanalyse Die statische Codeanalyse unterstützt einen Entwickler, um potentielle Fehler zu finden und vor allem vorzubeugen. Bestimmte Werkzeuge helfen dabei, den Quellcode zu analysieren und weisen auf bestimmte Aspekte oder Konstrukte hin, die vermieden oder anders programmiert werden sollten. Bei der Analyse an sich werden lediglich die Quellcodestrukturen analysiert und Tipps sowie Hinweise auf mögliche Fehlerquellen im Code gegeben. Die Methoden der Klassen werden dabei nicht ausgeführt, sondern, wie der Name schon sagt, nur analysiert. 4.2.1 Microsoft FxCop Das bekannteste in der .NET-Szene ist FxCop, ein frei verfügbares Tool zum Analysieren einer .NET Assembly. Es wird also nicht direkt der Quellcode analysiert, sondern die binäre „Common Intermediate Language“ (CIL), welche vom Compiler erzeugt wird (Kre08). Entgegen anderslautenden Quellen ist FxCop zwar ein frei verwendbares Tool von Microsoft, allerdings ist es kein Open Source-Projekt, und das war es auch zu keinem Zeitpunkt (Kea08). Die aktuelle Version 1.36 (Stand 2 Quartal 08) ist ebenfalls mit .NET entwickelt worden und benutzt die Technik Introspection, welche ähnlich funktioniert wie Reflection. Zwar bietet Introspection ausschließlich die Möglichkeiten des Lesens und nicht des Schreiben wie Reflection, allerdings sind diese Lesefähigkeiten dafür um 42 4 Vorhandene Testsysteme so tiefgreifender. Eine Assembly kann wesentlich genauer und detailreicher analysiert werden. Die gesamte Struktur einer Assembly mit allen Objekten, enthaltenen Ressourcen, Enumerationen und Klassen, welche wiederum Methoden, Variablen, Properties und anderen Objekte enthalten können, werden analysiert und mit vorhandenen Regeln verglichen. Diese Regeln sind an die Microsoft Design Standards angelehnt und gliedern sich in die folgenden Kategorien (CA05): • Design Rules • Globalization Rules • Interoperability Rules (COM) • Mobility Rules • Naming Rules • Performance Rules • Portability Rules • Security Rules • Usage Rules Das Ergebnis der Analyse ist nach kurzer Zeit sichtbar. Es werden übersichtlich alle Regelverstöße aufgelistet mit zusätzlichen Informationen, wie der Schwere dieser Regelverletzung, das Objekt, welches die Verletzung verursacht hat und der Grund, weshalb diese Regel verletzt wurde sowie auch ein Lösungsvorschlag. Eine Regel wird verletzt, wenn z.B. bestimmte Namenskonventionen nicht eingehalten oder bestimmte Programmierkonstrukte verwendet werden, die eine potentielle Fehlerquelle darstellen. Dazu gehören auch veraltete Befehle, die vom Compiler als „obsolet“ oder „veraltet“ markiert werden. Ungenutzte Variablen oder Parameter führen ebenfalls eine Regelverletzung nach sich. Der wohl größte Vorteil liegt in der Möglichkeit, eigene Regeln erstellen zu können. Beim Abbilden von code conventions in FxCop-Regeln entsteht ein einheitlicher Programmierstil und menschliche Fehler würden sofort erkannt und korrigiert werden können. Durch die umfangreichen Möglichkeiten, die Introspection gegenüber Reflection bietet, können u.a. folgende Prüfungen realisiert werden (Kre08): • Sicherstellen, dass Namenskonventionen bei Steuerelementen auf Masken oder Webseiten eingehalten werden. • Sicherstellen, dass bestimmte Komponenten oder Steuerelemente benutzt werden. • Prüfen von Werten, die als Parameter an Methoden übergeben werden. 43 4 Vorhandene Testsysteme • Begutachten von Codestrukturen, wie z.B. Schleifen oder Bedingungen. • Herausfinden aller Aufrufer einer Methode. • Überprüfung der sprachlichen Richtigkeit von Namen jeglicher Objekte. • Prüfen der XML-Kommentardokumentation auf Vollständigkeit. Ein großer Vorteil von FxCop ist, dass Tests jederzeit im Nachhinein durchgeführt werden können, ohne dass auf irgendwelche besonderen Test-Notationen im Quellcode geachtet werden muss. Was FxCop allerdings nicht leisten kann, ist das Ausführen der Methoden zum Testen des Verhaltens. Dies ist allerdings kein Nachteil, da es sich hierbei um ein Analyse-Tool handelt. Zusammenfassend kann gesagt werden, dass FxCop immer eingesetzt werden sollte, um Fehlern vorzubeugen. Der Quellcode wird einheitlich und viele potentielle Fehlerquellen können schon von vornherein ausgeschlossen werden. 4.3 GUI-Tests Es gibt sehr viele verschiedene Programme, mit denen GUI-Tests durchgeführt werden können. 4.3.1 Ranorex Eines solcher GUI-Testerprogramme ist das Tool Ranorex, welches die gleichnamige Firma entwickelt hat. Im Rahmen dieser Arbeit soll die Ranorex Free Edition kurz vorgestellt werden. Hiermit kann ein GUI-Test an einer Software vorgenommen werden, indem Benutzereingaben simuliert und Ergebnisse abgelesen und ausgewertet werden (siehe Abbildung 4.3 auf der nächsten Seite): Zusammenfassend kann zu Ranorex gesagt werden, dass es eine sehr gute Unterstützung ist, um Software zu testen. Alle Tests können mehrfach wiederholt werden. Ein nicht zu unterschätzender Vorteil von Ranorex ist die kurze Einarbeitungszeit. Nach einer einmaligen Einarbeitungszeit von ca. einer Stunde kann ein simpler Test (z.B. das Addieren beim Windows-Taschenrechner) innerhalb von Minuten erstellt werden. Würden nun allerdings alle Funktionen getestet werden, wäre das ein wesentlich größerer Aufwand. Selbst mit dem RanorexRecorder, mit dem der Ablauf aufgezeichnet werden kann, müssen die Validierungen manuell nachimplementiert werden. Abgesehen davon ist der Windows-Taschenrechner eines der am besten zu testenden Anwendungen. Sollte z.B. der Explorer getestet werden, so wäre dies eine wesentlich größere Herausforderung. Dort wird nicht mit eindeutigen Zahlen hantiert, sondern mit Listen von Dateinamen. Es 44 4 Vorhandene Testsysteme Abbildung 4.3: RanorexSpy liest Daten des Buttons 2 des Windows-Taschenrechners aus müsste z.B. geprüft werden, ob ein angelegter Ordner wirklich vorhanden ist oder ob nach dem Umbenennen einer Datei diese wirklich mit dem ihr nun zugewiesenen Programm geöffnet wird. In der Praxis gibt es weitaus schwerere Testfälle, die es zu validieren gilt, wie z.B. Videos, Bilder, PDF-Dokumente oder auch Audiowiedergaben. Neben dem Aufwand ist der größte Nachteil, dass während der Tests der Rechner nicht benutzt werden darf, da Ranorex die Maus und Tastatur benutzt, um so echte Benutzereingaben zu simulieren. Wenn während der Tests die Maus bewegt werden würde, könnte es sein, dass ein Test verfälscht wird. Weiterhin darf auch kein Programm das zu testende Programm überlagern, da sonst die Mausklicks nicht beim zu testenden Programm ankommen. In der freien Version eignet sich Ranorex am besten, um Kalkulationsprogramme zu testen. Sofern sich die Zielanwendung mit Ranorex testen lässt, sollte dies auf jeden Fall getan werden. 4.3.2 TestComplete TestComplete ist laut Aussage der Herstellerfirma AutomatedQA ein automatischer Test-Manager, welcher aufgrund der umfangreichen Funktionalität schon mehrfach von unterschiedlichen Zeitschriften ausgezeichnet wurde. Zu den durchführbaren Tests gehören Unit-Tests, Web-Tests, HTTP-Performance-Tests, datengesteuerte Tests und natürlich auch GUI-Tests. Wie bei allen GUI-Testern müssen die zu testenden Programme 45 4 Vorhandene Testsysteme und Steuerelemente identifiziert werden. Hierfür ist ebenfalls ein Recorder vorhanden, mit dem der Testlauf einfach zusammengeklickt werden kann. Im Folgenden sind die Eigenschaften des Buttons 2 des Windows-Taschenrechners analysiert worden (vgl. Abschnitt 4.3 auf der vorherigen Seite), allerdings diesmal mit TestComplete (siehe Abbildung 4.4): Abbildung 4.4: Properties des Buttons 2 des Windows-Taschenrechners, ausgelesen von TestComplete In diesem Test wird die Methode Test1 ausgeführt. Sie startet den WindowsTaschenrechner und simuliert die Aufgabe 2 + 5 = 7, als wenn sie von einem Benutzer eingegeben worden wäre. Am Ende wird das Ergebnis validiert, welches der Taschenrechner in der Textbox darstellt (siehe Listing 4.1 auf der nächsten Seite). 46 4 Vorhandene Testsysteme Listing 4.1: Beispiel eines TestComplete-Tests des Taschenrechners, ob 2 + 5 = 7 ist 1 function Main () 2 { 3 try 4 { 5 Test1 () ; 6 } 7 catch ( exception ) 8 { 9 Log [" Error " ]( " Exception " , exception [" description " ]) ; 10 } 11 } 12 13 function Test1 () 14 { 15 var 16 w1 = Sys [" Process " ]( " calc ")[" Window " ]( " SciCalc " , " Rechner "); 17 w1 [" Window " ]( " Button " , "2")[" ClickButton " ]() ; 18 w1 [" Window " ]( " Button " , "+")[" ClickButton " ]() ; 19 w1 [" Window " ]( " Button " , "5")[" ClickButton " ]() ; 20 w1 [" Window " ]( " Button " , "=")[" ClickButton " ]() ; w1 ; 21 22 if ( Sys [" Process " ]( " calc ")[" Window " ]( " SciCalc " , " Rechner " , 1) [" Window " ]( " Edit " 23 Log [" Error " ]( " The property value does not equal the template value ."); , "" , 1) [" wText "] != "7, ") 24 } Sollte das Ergebnis ungleich 7 sein, so wird ein Fehler im Protokoll vermerkt. Das Protokoll kann sehr detailliert geführt werden. Fähigkeiten wie das Gruppieren von Einträgen über das Erfassen von Umgebungssituationen, wie Speicherauslastung oder Prozessorauslastung, bis hin zum Hinzufügen von Bildern (Screenshots) lassen das Protokoll stets übersichtlich erscheinen. Es gibt viele Events, die ausgelöst werden können, wie z.B. UnexpectedWindow, welches ausgelöst wird, wenn ein unbekanntes Fenster erscheint. Standardmäßig kann z.B. immer die Cancel-Taste gedrückt werden. Somit kann ein kleiner Fehler niemals den gesamten Testlauf unterbrechen. Die Ergebnisse können exportiert oder per Mail versendet werden, was verdeutlicht, dass Testen auch eine wichtige Sache im Team ist. 4.4 Zusammenfassung GUI-Tests haben den Vorteil, dass sie über .NET hinaus einsetzbar sind. Durch die simulierten Benutzereingaben jeglicher Art ist eine qualitativ hochwertige Aussage über das Verhalten der Software möglich. Ein weiterer, sehr großer Vorteil ist, dass der GUI- 47 4 Vorhandene Testsysteme Tester im Vergleich zum Menschen nicht müde oder unkonzentriert wird. Zwar bieten diese Tools sehr viele Werkzeuge und Möglichkeiten, es dem Benutzer so einfach wie möglich zu machen, allerdings ist hier nachteilig zu erwähnen, dass diese Tests sehr umfangreich und kompliziert werden können. FxCop dient der statischen Codeanalyse und eignet sich hervorragend für die Prüfung des Quellcodes in jeder Phase des Entwicklungsprozesses. Somit können potentielle Fehlerquellen schon im Vorfeld ausgeschlossen und kritische Codestellen korrigiert werden. Die Fähigkeit, eigene Regeln zu implementieren, macht es zu einem idealen Werkzeug, um code conventions durchzusetzen. Somit wird der Code sehr konsistent und ist wesentlich leichter zu warten. Da FxCop schnell installiert, leicht zu bedienen und auch noch kostenlos ist, sollte es in keinem Softwareprojekt fehlen. Durch die Integration von NUnit in Microsoft Visual Studio 2008 wurde die Mächtigkeit stark erhöht. Viele Nachteile, wie das aufwendige Vorbereiten von Tests, wurden durch Assistenten stark vereinfacht. Weiterhin wurden die Tests stärker gekapselt als früher. Durch das Erstellen eines separaten Testprojektes entsteht keine allzu starke Vermischung zwischen dem Quellcode und dem Testcode. Zusätzlich wurde das Problem gelöst, dass NUnit keine Objekte oder Objektteile testen konnte, auf die von außen nicht zugegriffen werden konnte. Der Nachteil von Fuzzing ist, dass selbst wenn kein Fehler festgestellt werden konnte, dies keine Garantie dafür ist, dass die Methode ordnungsgemäß arbeitet. Fuzzing soll grobe Fehler und Abstürze aufspüren. Nur in den seltensten Fällen werden in einem komplexen Entwicklungszyklus die geschriebenen Methoden und die Eingabeparameter ausschließlich für den ursprünglich entwickelten Zweck verwendet. Deswegen müssen Parameter auch auf Werte hin validiert werden, für die sie im ersten Moment nicht konzipiert waren. Der größte Vorteil dieser Technik ist das Generieren der Testfälle. Video- oder Audiowiedergaben kann keines der hier aufgeführten Systeme testen, da es sehr schwer zu automatisieren ist. Es wäre so aufwendig, dass es wesentlich effizienter ist, wenn solche Tests immer noch von Menschen erledigt werden (Wel08). Jedes Testsystem deckt einen bestimmten Testbereich ab. Teilweise gibt es Überschneidungen der Testbereiche. Jedes Testsystem hat seine Vor- und Nachteile sowie Stärken und Schwächen. Je mehr getestet wird, desto fehlerfreier wird im Allgemeinen die Software. Deswegen sollten so viele Testsysteme wie möglich zum Einsatz kommen. 48 Projekt AutoTest.Net [5] 5.1 „Das Ganze ist mehr als die Summe seiner Teile.“ Soll-Zustand Aristoteles 5.2 Optionale Funktionalitä- Dieses Kapitel beschreibt die Anforderungen an AutoTest.Net sowie optionale Featu- ten res. Jegliche Klassendiagramme werden mit „MicroTool ObjectIf 7“ erstellt. Obwohl die 5.3 von Microsoft Visual Studio 2008 erzeugten Klassendiagramme wesentlich übersichtli- Aufbau der Be- cher aufgebaut sind, werden hier ausschließlich die Diagramme von „MicroTool Objec- dienoberfläche tIf“ verwendet, da sich diese an der UML-Norm orientieren. 5.4 Arbeitsablauf in 5.1 Soll-Zustand AutoTest.Net Hier werden alle Anforderungen an AutoTest.Net gestellt. In der ersten Version, welche AutoTest.Net 2008 heißen wird, soll die Software folgende Funktionalitäten aufweisen: • Laden und Analysieren des Aufbaus einer .NET-Assembly Mit einem „Datei|Öffnen“-Dialog soll eine .NET-Assembly ausgewählt und geladen werden. Dabei wird mittels Reflection die interne Struktur analysiert und dem Benutzer dargestellt. • Durchführung der Tests Die Funktionstests werden mittels Fuzzing durchgeführt. Jeder Konstruktor und jede Methode des ausgewählten Objektes wird ausgeführt. Als Parameter sollen alle Werte einer jeden Äquivalenzklasse benutzt werden. Enthält also die Signatur einer Methode einen Parameter z.B. vom Typ DateTime, so wird die Methode mit jedem Wert der Äquivalenzklasse System.DateTime einmal aufgerufen. Die Signatur einer Methode besteht aus dem Namen der Methode sowie allen Parametertypen. 49 5.5 Lösungskonzeption 5 Projekt AutoTest.Net • Ausgabe eines Protokolls Es werden alle abgestürzten Methoden, zusammen mit den verursachenden Parameterwerten und den abgefangenen Exceptions aufgelistet. • Konfigurieren der Exceptions Hier können erwartete Exceptions eingestellt werden. Sollte eine erwartete Exception, wie z.B. eine System.ArgumentNullException geworfen werden, so wird diese nicht im Fehlerprotokoll aufgeführt. • Konfigurieren der Äquivalenzklassen Alle in den Äquivalenzklassen hinterlegten Werte können hier betrachtet und verändert werden. Neue, zu dieser Klasse passende Werte können hinzugefügt oder vorhandene gelöscht werden. AutoTest.Net soll als stand-alone-Anwendung implementiert werden, da so vorhandene Assemblies geladen und getestet werden können, selbst wenn kein Visual Studio installiert ist. Ein Vorteil einer Integration wäre eine leichtere Handhabung, da keine externe, separate Anwendung benötigt würde. Aus diesem Grund wird die Integration als optionales Feature bedacht. Um eine Übersicht über die Funktionalität der Software zu erhalten, lässt sich anhand der verbalen Beschreibung des Systems folgendes Anwendungsfalldiagramm (Use-CaseDiagramm) erstellen (siehe Abbildung 5.1): Abbildung 5.1: Anwendungsfalldiagramm für AutoTest.Net Als Akteur ist hier der Tester zu nennen, der mithilfe von AutoTest.Net eine .NETAssembly testet. Use-Cases werden mittels einer Schablone ähnlich einer Checkliste detailliert mit Ziel, Vor- und Nachbedingungen, dem Akteur, der diesen Use-Case benutzt, 50 5 Projekt AutoTest.Net sowie Alternativen beschrieben (Bal08). Im Folgenden werden die einzelnen Use-Cases von AutoTest.Net detailliert beschrieben: • Use-Case: Testen Ziel: Testen von Methoden einer Klasse in einer .NET-Assembly Vorbedingungen: Gültige zu testende .NET-Assembly vorhanden Nachbedingung Erfolg: Ausgewählte Methoden der Assembly getestet Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen im Log-Bereich Akteure: Tester (Benutzer der Software) Beschreibung: 1. Testen einer .NET-Assembly Erweiterungen: 1a. Laden/Analysieren einer .NET-Assembly 1b. Tests durchführen Alternativen: - • Use-Case: Laden/Analysieren einer .NET-Assembly Ziel: Laden und Analysieren einer .NET-Assembly sowie übersichtliches Darstellen der internen Struktur der Assembly Vorbedingungen: Gültige zu testende .NET-Assembly vorhanden Nachbedingung Erfolg: Das System identifiziert alle Typen der Assembly und listet sie auf Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen im Log-Bereich Akteure: Tester (Benutzer der Software) Beschreibung: 1. Das System wartet auf eine mittels eines „Datei|Öffnen“-Dialoges vom Akteur ausgewählte .NET-Assembly 2. Das System lädt die ausgewählte .NET-Assembly in den Speicher 3. Das System analysiert die interne Struktur der Assembly und listet alle enthaltenden Klassen auf 4. Das System lässt den Benutzer eine Klasse zum Testen auswählen Erweiterungen: 3a. Das System listet nur Klassen auf. Weitere Typen wie z.B. Interfaces werden nicht aufgelistet, da diese nicht getestet werden können 51 5 Projekt AutoTest.Net Alternativen: 2a. Wenn die vom Akteur ausgewählte Datei keine gültige .NET-Assembly ist, dann Ausgabe von Fehlermeldungen im Log-Bereich • Use-Case: Tests durchführen Ziel: Durchführung der Tests an allen ausgewählten Konstruktoren und Methoden Vorbedingungen: Ein zu testendes Objekt wurde vom Akteur ausgewählt Nachbedingung Erfolg: Das System gibt alle fehlgeschlagenen Methoden und die Parameterwerte, die zum Absturz der Methode führten, auf dem Bildschirm aus Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen im Log-Bereich Akteure: Tester (Benutzer der Software) Auslösendes Ereignis: Akteur startet die Tests Beschreibung: 1. Das System bedient sich der im Speicher vorhandenen Auflistung der zu testenden Konstruktoren 2. Erkennen der benötigten Werte aus den Äquivalenzklassen anhand der Parameter des zu testenden Konstruktors 3. Durchführen des Funktionstests an den Konstruktoren (Instanziieren eines Objektes mit dem zu testenden Konstruktor; auftretende Exceptions abfangen) 4. Erzeugen einer Instanz für die Funktionstests an den Methoden 5. Erkennen der benötigten Werte aus den Äquivalenzklassen anhand der Parameter der zu testenden Methoden 6. Durchführen des Funktionstests an den Methoden (Ausführen der Methode mit der erstellten Instanz; auftretende Exceptions abfangen) Erweiterungen: 4a. Erzeugen einer Instanz des Objektes mittels des Standardkonstruktors 4a1. Ist kein Standardkonstruktor vorhanden, so versucht das System einen anderen Konstruktor zu instanziieren Alternativen: 3a. Wenn z.B. aus Sicherheitsgründen ein Fehler auftritt, der die Durchführung der Tests an Konstruktoren verhindert, dann Ausgabe einer Fehlermeldung im Log-Bereich 52 5 Projekt AutoTest.Net 4a. Wenn das System nicht in der Lage ist, eine Instanz zu erzeugen, dann Ausgabe einer Fehlermeldung im Log-Bereich 5a. Wenn z.B. aus Sicherheitsgründen ein Fehler auftritt, der die Durchführung der Tests an Methoden verhindert, dann Ausgabe einer Fehlermeldung im Log-Bereich • Use-Case: Ausgabe der Testergebnisse Ziel: Ausgabe der beim Test abgestürzten Methoden und die Parameterwerte, die zum Absturz führten sowie die entstandene Exception Vorbedingungen: Das System hat erfolgreich alle Tests durchgeführt Nachbedingung Erfolg: Ausgabe von Testergebnissen auf dem Bildschirm Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen im Log-Bereich Akteure: Tests durchführen Auslösendes Ereignis: Beendigung der Tests Beschreibung: 1. Das System zeigt die Testergebnisse an. Dargestellt werden die abgestürzte Methode, die zum Absturz geführten Parameterwerte sowie die aufgetretene Exception Erweiterungen: 1a. Gruppierungsmöglichkeit nach abgestürzten Methoden sowie nach geworfener Exception 1b. Falls keine Exception auftritt, Ausgabe einer Meldung im Ergebnissfenster („Keine Exception aufgetreten“) Alternativen: 1a. Wenn Fehler bei der Visualisierung der Testergebnisse aufgetreten, dann Ausgabe einer Fehlermeldung im Log-Bereich • Use-Case: Exceptions verwalten Ziel: Das System bietet eine Auswahlmöglichkeit für alle Exceptions des Namespace System an Vorbedingungen: Nachbedingung Erfolg: Das System speichert die Auswahl. Nur ausgewählte Exceptions werden bei der Ausgabe der Ergebnisse berücksichtigt Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen im Log-Bereich 53 5 Projekt AutoTest.Net Akteure: Tester (Benutzer der Software) Auslösendes Ereignis: Öffnen der Exception-Maske Beschreibung: 1. Das System startet die Exception-Maske, welche alle Typen des Namespace System auflistet, die von System.Exception erben 2. Wartet auf das „Checked“-Event Erweiterungen: 2a. Sofortiges Speichern der Auswahl Alternativen: 2a. Wenn die Speicherung der Auswahl fehlschlägt, dann Ausgabe einer Fehlermeldung im Log-Bereich • Use-Case: Äquivalenzklassen verwalten Ziel: Anpassen der vordefinierten Äquivalenzklassen Vorbedingungen: Nachbedingung Erfolg: Äquivalenzklassen sind angepasst Nachbedingung Fehlschlag: Ausgabe von Fehlermeldungen Akteure: Tester (Benutzer der Software) Auslösendes Ereignis: Öffnen der Äquivalenzklassen-Maske Beschreibung: 1. Das System stellt die vorhandenen Äquivalenzklassen dar 2. Das System wartet auf eine Auswahl 3. Das System stellt alle enthaltenen Werte der Äquivalenzklasse dar 4. Das System bietet Verwaltungsmöglichkeiten an Erweiterungen: 4a. Das System wartet auf eine Eingabe zum Hinzufügen neuer Werte zu einer Äquivalenzklasse 4b. Das System löscht ausgewählte Werte einer Äquivalenzklasse Alternativen: 4a. Wenn der hinzugefügte Wert nicht der Äquivalenzklasse entspricht, dann Ausgabe einer Fehlermeldung im Log-Bereich 54 5 Projekt AutoTest.Net 5.2 Optionale Funktionalitäten Neben dem Soll-Zustand können viele weitere Funktionen implementiert werden. In der folgenden Tabelle werden optionale Features aufgelistet (siehe Tabelle 5.1): optionale Feature Beschreibung Prüfen von Ressourcenauslas- Performance des Systems überwachen und heraus- tung wie CPU oder Speicher finden, ob Speicher- oder CPU-Leaks auftreten. Benutzen komplexer Testfälle Neben den simplen Datentypen können im Testsystem komplexe Objekte (z.B. Klassen) erstellt und als Eingangswerte für die Tests genutzt werden. Benutzen der Rückgabewerte als Die Rückgabewerte einer getesteten Methode kann Testfälle als Eingangswert für den nächsten Test genutzt werden. Sicherheitsbereiche einstellen Eventuell werden bei einem Test ungewollte Ressourcen wie z.B. Zugriff auf das Dateisystem benutzt. Mittels einer Sandbox könnte dies verhindert werden. In Microsoft Visual Studio 2008 Eventuell zusätzlich zur externen Anwendung kann als Plug-In integrieren AutoTest.Net in Microsoft Visual Studio 2008 integriert werden. Ausgabeprotokoll exportieren Das Ausgabeprotokoll kann z.B. in eine Textdatei oder als HTML-Ansicht exportiert werden. Tabelle 5.1: Liste der optionalen Features Beim Betrachten dieser optionalen Features fällt keines auf, dass besonderer Voraussetzungen bedarf. Alle sind ohne großen Aufwand im Nachhinein implementierbar. Wie viele und welche dieser optionalen Features implementiert werden können, wird sich im Laufe der Implementierung zeigen. 5.3 Aufbau der Bedienoberfläche Um die Anwendung stets übersichtlich zu halten, soll sie mittels andockbaren Fenstern arbeiten. Geplant sind neben einer Menü- und Schnellstartleiste folgende Elemente: 55 5 Projekt AutoTest.Net • Log-Bereich In diesem Fenster werden jegliche Fehler, Warnungen oder normale Informationsmeldungen ausgegeben (siehe Abbildung 5.2). Abbildung 5.2: Log-Bereich in AutoTest.Net 2008 Die Verwendung von MessageBoxen soll, sofern möglich, vermieden werden, da sie den Arbeitsfluss behindern. • Verwaltungsbereich für die Äquivalenzklassen Die Äquivalenzklassen stellen Testdaten dar. Mit ihnen wird festgelegt, zu welchem Typ welche Werte als Testdaten in Frage kommen. Hier wird es ein StandardRepertoire an Werten geben, welches jederzeit beliebig angepasst werden kann (siehe Abbildung 5.3). Diese Maske arbeitet unabhängig von anderen Programmteilen und ist angelehnt an den Use-Case „Äquivalenzklassen verwalten“. Abbildung 5.3: Verwaltungsbereich für die Äquivalenzklassen in AutoTest.Net 2008 • Verwaltungsbereich für die Exceptions Exceptions, die keinen Fehlerfall darstellen und somit nicht im Fehlerprotokoll aufgelistet werden, können hier verwaltet werden. Dies wird wie die Äquivalenzklassenverwaltung eine separate Maske sein, die unabhängig von anderen Programmteilen arbeitet. Es werden alle Exceptions des Namespace System zur Auswahl 56 5 Projekt AutoTest.Net aufgelistet. Änderungen werden mit dem „Checked“-Event sofort übernommen, wie im Use-Case „Exceptions verwalten“ beschrieben (siehe Abbildung 5.4): Abbildung 5.4: Verwaltungsbereich für die Exceptions in AutoTest.Net 2008 • Hauptfenster Das Hauptfenster beinhaltet im ersten Schritt die verwaltenden Elemente für die Vorbereitungen der Tests (wird im Folgenden Startmaske genannt). Dazu gehören das Laden der Assembly und das Auswählen der zu testenden Methoden und Konstruktoren. Nach dem Start werden im Hauptfenster als weiteres Tab die Ergebnisse präsentiert (wird im Folgenden Ergebnismaske genannt). 5.4 Arbeitsablauf in AutoTest.Net Nach dem Start der Anwendung kann der Benutzer mittels der Startmaske eine .NETAssembly einlesen. Selbstständig erkennt die Software schon vor dem Laden, ob es sich um eine gültige .NET-Assembly handelt. Andernfalls wird ein Eintrag im Log-Bereich erstellt. Nach dem Laden werden alle enthaltenden Klassen aufgelistet und der Benutzer kann sich eine zum Testen auswählen. Jegliche Einstellungen wie z.B. das detaillierte Auswählen der zu testenden Methoden können vor dem Test vorgenommen werden. Ohne weitere Einstellungen werden standardmäßig alle in der Klasse vorhandenen Konstruktoren und Methoden mit den Standard-Äquivalenzklassen getestet. Nach dem Test wird die Ergebnismaske angezeigt, welche alle Fehlerfälle auflistet sowie Gruppierungsmöglichkeiten anbietet. Im einfachsten Fall lädt der Benutzer eine Assembly, wählt eine Klasse aus und startet sofort die Tests. 57 5 Projekt AutoTest.Net 5.5 Lösungskonzeption Die wichtigsten Klassen sind die Klassen Test, ExceptionManagement, EquivalenceClass<T> und TestResult. Im Folgenden wird näher auf die einzelnen Klassen und deren Zusammenspiel eingegangen. 5.5.1 Äquivalenzklassenverwaltung Um die Äquivalenzklassen verwalten zu können, sind Funktionen wie Hinzufügen oder Löschen von Klassenwerten nötig. Ein vordefinierter Satz an Äquivalenzklassen steht permanent zur Verfügung. Diese Klasse ist aufgrund ihrer Typabhängigkeit eine generische Klasse vom Typ T (gelber Balken) (siehe Abbildung 5.5). Das bedeutet, dass der Typ der Klasse erst zur Zeit der Instanziierung angegeben wird. Interne Berechnungen oder Sortierungen geschehen jederzeit typsicher. Das Abspeichern und Laden der Äquivalenzklassen soll ebenfalls von dieser Klasse abgedeckt werden. Abbildung 5.5: Equivalenzklasse für AutoTest.Net Das Feld fileName ist der Name einer Datei, in der die Äquivalenzklassen abgelegt sind. Sollte diese Datei nicht existieren, wird sie erstellt und die Standardklassen mit enthaltenen Werten eingetragen. Die Property Values beinhaltet alle Werte dieser Klasse. Die vom Interface IEquivalenceClass importierte Property Values gibt die Auflistung Values ohne generische Typsicherheit, also vom Typ object, zurück. Die statische Methode GetEquivalenceClasses liefert alle in der Datei gefundenen Klassen zurück. Die statische Methode SaveEquivalenceClasses(List<IEquivalenceClass>) speichert alle Äquivalenzklassen zurück in die Datei. 58 5 Projekt AutoTest.Net 5.5.2 Exceptionverwaltung Jede Methode kann Exceptions werfen, wovon einige erwartet werden. Wenn z.B. eine System.ArgumentNullException anstatt einer System.NullReferenceException auftritt, so wird deutlich, dass der Programmierer den eventuell eintretenden Fall eines übergebenen null-Parameters berücksichtigt hat. Hier eingestellte Exceptions erscheinen nicht im Ausgabeprotokoll, da davon ausgegangen werden kann, dass diese Methode bei unbekannten oder ungültigen Parameterwerten nicht abstürzt. Als Funktionalität muss das Hinzufügen und Entfernen von Exceptions möglich sein. Das folgende Klassendiagramm zeigt die Klasse, die für diese Verwaltung zuständig ist (siehe Abbildung 5.6): Abbildung 5.6: Exceptionverwaltungsklasse für AutoTest.Net Diese Klasse ist ein Singleton, weswegen der Konstruktor private ist und über die statische Property mit dem Namen Instance auf das Objekt zugegriffen werden kann (siehe Listing 5.1). Die Exceptions werden in einer Schlüsselwertpaar-Auflistung gespeichert. Das Feld exceptions ist vom Typ Dictionary<Type, bool> und speichert zu jedem Typ einen booleschen Wert. Der Wert true gibt an, dass die Exception in der Ergebnisauflistung erscheint, wohingegen false dies unterbindet. Listing 5.1: Singleton-Pattern der ExceptionManagement-Klasse 1 class ExceptionManagement 2 { 3 private static readonly ExceptionManagement instance = new ExceptionManagement () ; 4 public static ExceptionManagement Instance { get { return instance ; } } 5 } 59 5 Projekt AutoTest.Net 5.5.3 Testdurchführung Die Klasse Test stellt den Kern von AutoTest.Net dar und beinhaltet alle Einstellungen, die in der gesamten Anwendung vorgenommen werden können. Deswegen unterliegt auch diese Klasse dem Singleton-Pattern. Die folgenden Auflistungen der Klasse Test beinhalten die jeweils beschriebenen Objekte: • EquivalenceClasses vom Typ List<IEquivalenceClass> Beinhaltet alle Äquivalenzklassen. • TestResults vom Typ List<TestResult> Beinhaltet alle Testergebnisse. • MethodCollection vom Typ Dictionary<MethodInfo, bool> Beinhaltet alle Methoden von Typen, die in der TypeCollection enthalten sind. Der boolesche Wert gibt an, ob diese Methode bei den Tests berücksichtigt wird. • ConstructorCollection vom Typ Dictionary<ConstructorInfo, bool> Beinhaltet alle Konstruktoren von Typen, die in der TypeCollection enthalten sind. Der boolesche Wert gibt an, ob dieser Konstruktor bei den Tests berücksichtigt wird. • AssemblyCollection vom Typ List<Assembly> Beinhaltet alle geladenen .NET-Assemblies. • TypeCollection vom Typ List<System.Type> Beinhaltet alle vom Benutzer ausgewählten Klassen. Nur Konstruktoren und Methoden hier hinzugefügter Typen werden in die ConstructorCollection respektive MethodCollection übernommen. • AllTypeCollection vom Typ List<System.Type> Beinhaltet alle Klassen, die in allen geladenen Assemblies der AssemblyCollection vorhanden sind. Zu jeder Auflistung gibt es entsprechende Add- oder Remove-Methoden, um die Auflistungen zu verändern. Das Interface IEquivalenceClass wird genutzt, um nicht generisch, d.h. alle Werte sind vom Typ object, durch die Auflistung der Äquivalenzklassen durchiterieren zu können. Die Flag-Properties der Klasse Test geben an, ob die entsprechenden Methoden und Konstruktoren berücksichtigt werden sollen. Zum Beispiel gibt das Public-Flag an, ob öffentliche Konstruktoren und Methoden getestet werden. Die Property TestConstructor gibt an, welcher Konstruktor für die Tests der Methoden benutzt werden soll. Wird dieser nicht angegeben, wird der Standardkonstruktor benutzt. 60 5 Projekt AutoTest.Net Die Resultate werden als eigene Objekte modelliert, da sie komplexe Datentypen sind, die weitere Informationen enthalten. Das folgende Klassendiagramm zeigt übersichtlich die bei einem Testlauf beteiligten Klassen (siehe Abbildung 5.7): Abbildung 5.7: Klassendiagramm für AutoTest.Net Blaue Kanten in der rechten unteren Ecke des Klassennamens bedeuten, dass dies eine abstrakte Klasse ist und somit keine Instanz erstellt werden kann. Die Methode AbortTest beendet den Thread, in dem die Tests durchgeführt werden. Die beiden statischen Methoden GetAccessType und GetParameters liefern jeweils einen String zurück, der kommagetrennt die Zugriffsmodifizierer (z.B. „Public, Static“) bzw. die Parameter (z.B. „System.String, System.Int32“) zurückgibt. Diese sind statisch, weil sie nicht nur von der Klasse Test, sondern auch von der Oberfläche zur Darstellung genutzt werden. Der Parameter beider Methoden ist vom Typ MethodBase, welcher die Basisklasse für ConstructorInfo und MethodInfo darstellt. Deswegen können beide Methoden zur Ermittlung der Werte beider Typen, Konstruktoren und Methoden, genutzt werden. 61 Implementierung des Testsystems [6] 6.1 Die Software kann nun anhand der Konzeption und der vorangegangenen Diagramme Implementierung und Spezifikationen implementiert werden. Es wird als „2-Schicht“-Anwendung (engl. der GUI 2-Tier-Application) implementiert (siehe Abbildung 6.1). 6.2 Implementierung der Use-Cases 6.3 Fertigstellung von AutoTest.Net Abbildung 6.1: Anwendungsschichten von AutoTest.Net Die Logikschicht beinhaltet die gesamte Logik der Anwendung. Jegliche Visualisierungen werden in der GUI-Schicht gekapselt. Zwischen den Schichten werden lediglich simple Daten ausgetauscht, d.h. keine GUI-spezifischen Datentypen wie ListViewItem oder TreeNode. Der Namespace System.Windows.Forms, welcher jegliche visuellen Objekte enthält, wird nicht in die Logikschicht eingebunden. Somit ist es möglich, die gesamte graphische Benutzeroberfläche auszutauschen, ohne die eigentlichen Logikklassen zu verändern. Im ersten Schritt wird die GUI erstellt, welche anfangs gänzlich ohne Funktionalität ist. So hätte ein Auftraggeber schon sehr früh die Möglichkeit, das entstehende Produkt zu begutachten und ggf. Mängel schon sehr früh zu monieren. Nachdem die GUI als Prototyp fertiggestellt ist, wird jeder Use-Case einzeln betrachtet und im- 62 6 Implementierung des Testsystems plementiert. Die GUI dient dazu als Testumgebung, um die implementierten Use-Cases anzusprechen und auszuführen. 6.1 Implementierung der GUI Die gesamte GUI besteht aus den folgenden Masken (siehe Abbildung 6.2): Abbildung 6.2: GUI von AutoTest.Net Durch Rapid Prototyping wird ein erstes Layout erstellt. Diese Form des Vorgehens erlaubt es, schnell eine Dummy-Anwendung zu erstellen, welche ohne Funktionalität die Oberfläche zeigt, wie es später sein könnte. Im Zuge dessen können schon sehr früh Ecken und Kanten im Layout der Software festgestellt werden. Oben befindet sich die Menüleiste. Direkt darunter die Schnellstartleiste zum Sofortstart oft benötigter Funktionen. Auf der linken Seite befindet sich die Verwaltung der Äquivalenzklassen. Im Hauptfenster werden alle vorbereitenden Aktivitäten durchgeführt. Weiterhin werden in verschiedenen Reitern die Ergebnisse präsentiert. An der unteren Seite befindet sich das Log-Panel, welches alle Statusmeldungen mit Zeit anzeigt. Alle Fenster können abgedockt, frei verschoben und an jeder beliebigen Seite wieder angedockt werden (siehe Abbildung 6.3 auf der nächsten Seite). 63 6 Implementierung des Testsystems Abbildung 6.3: Erstes Layout von AutoTest.Net 2008 Beim Bau der eigentlichen Masken ist auf unterschiedliche Fluchtlinien sowie allgemeine Einheitlichkeit zu achten. Eine Anwendung wirkt unterbewusst umso beruhigender und somit angenehmer, desto weniger unterschiedliche Fluchtlinen vorhanden sind. Texte müssen sich, sofern möglich, auf gleicher Höhe befinden. Um dies einfach umzusetzen, bietet Microsoft Visual Studio sogenannte Snaplines an, mit deren Hilfe Steuerelemente exakt ausgerichtet werden können. Snaplines können für eigene Steuerelemente selbst implementiert werden. Es gibt blaue Snaplines für Ränder und pinkfarbene Snaplines für Texte (siehe Abbildung 6.4). Abbildung 6.4: Unterstützende Snaplines beim Bau einer Maske 64 6 Implementierung des Testsystems Die Maske zum Laden und Analysieren einer .NET-Assembly sowie weitere vorbereitende Einstellungen wird als Prototyp wie folgt aussehen (siehe Abbildung 6.5): Abbildung 6.5: Maske zur Analyse der Assembly Eine Assembly kann mithilfe des „Durchsuchen“-Buttons ausgewählt werden. Es werden alle vorhandenen Typen aufgelistet. Sobald der Benutzer den zu testenden Typ ausgewählt hat, werden alle enthaltenen Methoden und Konstruktoren ermittelt. 6.2 Implementierung der Use-Cases Nachdem eine GUI erstellt wurde, können nun die Use-Cases, die die Funktionalität der Software darstellen, implementiert werden. Der allumfassende Use-Case „Testen“ enthält die drei Hauptbestandteile von AutoTest.Net. Als Nebenbestandteile sind die Verwaltung der Exceptions sowie die Verwaltung der Äquivalenzklassen zu nennen. Bei der Umsetzung wurden Microsoft-Designrichtlinien beachtet. Diese geben z.B. an, dass private Variablen mittels Properties gekapselt werden und somit der Zugriff ausschließlich über diese Properties möglich ist. Im Folgenden werden die einzelnen Use-Cases implementiert, wobei deren Funktionalität anhand der GUI getestet werden kann. 6.2.1 Implementierung Use-Case „Laden/Analysieren der Assembly“ Der benötigte „Datei|Öffnen“-Dialog wird direkt in der GUI erstellt. Er listet „.exe“ und „.dll“-Dateien auf. Nach dem Auswählen der Dateien werden diese an die AssemblyCollection der Klasse Test übergeben. Nachdem alle gewählten Assemblies 65 6 Implementierung des Testsystems übergeben sind, können sämtliche enthaltenen Typen in der AllTypeCollection der Klasse Test abgerufen und in der GUI angezeigt werden. Mit Assembly.LoadFrom( path ) wird eine Assembly geladen. Sollte es sich um keine gültige .NET-Assembly handeln, so wird hierbei eine Exception geworfen. Geladen werden alle enthaltenden Typen mit der Methode GetTypes einer jeden Instanz der Klasse System.Reflection. Assembly. 6.2.2 Implementierung Use-Case „Tests durchführen“ Die Tests werden in einem separaten Thread durchgeführt. Für die Tests sind die ausgewählten Objekte, Konstruktoren und Methoden sowie die Äquivalenzklassen notwendig. Bevor die Tests durchgeführt werden können, müssen alle Typen der Parameter ermittelt und die Parameterlisten für die Tests vorbereitet werden. Bei Enumerationen werden alle enthaltenden Werte für die Tests benutzt. Das folgende Listing zeigt den Ausschnitt der Methode BuildParamArray(ParameterInfo[]), der speziell Enumerationen behandelt. Es wird eine Instanz des Enumerations-Objektes mithilfe von Reflection erzeugt und jeweils alle Werte einer Enumeration übergeben (siehe Listing 6.1): Listing 6.1: Speichern aller Werte einer Enumeration 1 public List < Object [] > BuildParamArray ( ParameterInfo [] parameters ) 2 { 3 List < Object [] > objList = new List < Object [] >() ; 4 foreach ( ParameterInfo par in parameters ) 5 { 6 List < Object > obj = new List < Object >() ; 7 if ( par . ParameterType . IsEnum ) 8 { 9 // Parameter ist eine Enumeration Type type = par . ParameterType ; // Typ des Parameters zuweisen 10 Array values = Enum . GetValues ( type ); // Alle Werte als Array ablegen 11 Object tempObj ; // Temporäres Objekt 13 foreach ( Object entry in values ) // Alle Enumerationen durchlaufen 14 { 12 15 tempObj = Activator . CreateInstance ( type ); 16 tempObj = entry ; // Wert zuweisen obj . Add ( tempObj ); // Zur Auflistung der Parameter hinzufügen 17 // Instanz erzeugen 18 } 19 objList . Add ( obj . ToArray () ); // Als Gesamtparameter abschliessen 20 continue ; 21 22 // Nächsten Parameter bearbeiten } } 23 } 66 6 Implementierung des Testsystems Eine große Herausforderung war das Beachten aller Sonderfälle bei den Tests. Spezialfälle wie Arrays oder Enumerationen mussten separat behandelt werden. Für Arrays werden automatisch leere Arrays generiert, die für die Tests genutzt werden. Für simple Datentypen werden die Werte aus den Äquivalenzklassen genutzt. Bei mehreren Parametertypen werden alle Permutationen erzeugt. Nachdem alle Parameter vorbereitet sind, werden die einzelnen Methoden des Testobjekts mit der Methode Invoke, die jede Instanz eines MethodInfo-Objekts enthält, ausgeführt. Die Parameter werden dabei als Array des Typs object übergeben. Die Ausführung ist von einem try-catch-Block umgeben, um alle Exceptions abzufangen. Sollte eine Exception auftreten, so wird diese einem TestResult-Objekt zugeordnet und steht später zur Auswertung zur Verfügung. 6.2.3 Implementierung Use-Case „Ausgabe der Testergebnisse“ Die Ausgabe der abgefangenen Exceptions kann nach Exception oder nach Methode gruppiert werden. Für diese Gruppierung bietet sich das neue .NET 3.5 Feature LINQ an. Das folgende Listing zeigt die Gruppierung der Testergebnisse nach Exceptions (siehe Listing 6.2): Listing 6.2: Gruppierung von Objekten mit LINQ 1 var query = from result in Test . Instance . Results 2 where result . HasException 3 group result by result . Exception . GetType () into g 4 select g; Die Darstellung geschieht in einem Steuerelement, welches eine Mischung zwischen TreeView und ListView darstellt (siehe Abbildung 6.6): Abbildung 6.6: Darstellung von gruppierten Ergebnissen Im oberen Teil ist die Gruppierung nach der geworfenen Exception dargestellt. Im unteren Teil ist die Gruppierung nach der Methode dargestellt, die die Exception geworfen hat. 67 6 Implementierung des Testsystems 6.2.4 Implementierung Use-Case „Exceptions verwalten“ Die Klasse ExceptionManagement verwaltet die Exceptions, die bei der Ausgabe der Testergebnisse ignoriert werden. Als Objekt für die Speicherung bietet sich ein Dictionary <Type, bool> an. Es beinhaltet Schlüsselwertpaare und speichert so zu jedem Exception-Typ einen booleschen Wert, der angibt, ob die Exception berücksichtigt werden soll. Mit der Methode IsSubclassOf des Objektes System.Type werden rekursiv alle abgeleiteten Typen des Typs System.Exception ermittelt (siehe Listing 6.3): Listing 6.3: ExceptionManagement. Auflistung aller Exceptions des Namespace System 1 private ExceptionManagement () 2 { 3 exceptions = new Dictionary < Type , bool >() ; 4 List < Type > list = new List < Type >() ; 5 Assembly a = typeof ( Exception ). Assembly ; 6 Type [] types = a. GetTypes () ; 7 foreach ( Type t in types ) 8 { 9 // Exception - Assembly ermitteln // Alle enthaltenden Typen ermitteln // Ist Typ t abgeleitet von System . Exception ? 10 if (t. IsSubclassOf ( typeof ( System . Exception ) )) 11 list . Add ( t ); // Wenn ja , füge ihn der Auflistung hinzu 12 } 13 list . Sort ( new Comparison < Type >( ( t1 , t2 ) => t1 . FullName . CompareTo ( t2 . 14 foreach ( var item in list ) FullName ) ) ); 15 // Sortiere Exceptions nach Name exceptions . Add ( item , true ); 16 17 // Folgende Exceptions standardmäßig ausschalten 18 exceptions [ typeof ( ArgumentException )] = false ; 19 exceptions [ typeof ( ArgumentNullException )] = false ; 20 exceptions [ typeof ( ArgumentOutOfRangeException )] = false ; 21 exceptions [ typeof ( NotImplementedException )] = false ; 22 exceptions [ typeof ( NotSupportedException )] = false ; 23 } 6.2.5 Implementierung Use-Case „Äquivalenzklassen verwalten“ Die Äquivalenzklassen werden in einer XML-Datei abgelegt, um auch ein externes Bearbeiten zu ermöglichen. Der Namespace System.Xml bietet Objekte für das Laden, Analysieren sowie Schreiben von XML-Daten an. Sollte beim ersten Start des Programmes keine Äquivalenzklassen-Datei existieren, so wird die Datei „equivalence.xml“ aus den internen Ressourcen herauskopiert und in dem Verzeichnis von AutoTest.Net angelegt. Auf eine interne Ressource wird wie folgt zugegriffen (siehe Listing 6.4): 68 6 Implementierung des Testsystems Listing 6.4: Zugriff auf interne eingebettete Ressourcen 1 XmlDocument xmlDoc = new XmlDocument () ; 2 Assembly asm = Assembly . GetExecutingAssembly () ; // aktuelle Assembly laden 3 Stream XMLStream = asm . GetManifestResourceStream ( asm . GetName () . Name + ". equivalence . xml " ); 4 XmlReader xmlr = XmlReader . Create ( XMLStream ); // Erstelle Stream der xml - Datei 5 try 6 { 7 if (! File . Exists ( fileName )) 8 { 9 10 // Falls Datei nicht vorhanden xmlDoc . Load ( xmlr ); // Interne Ressource laden xmlDoc . Save ( fileName ); // Datei anlegen 11 } 12 xmlDoc . Load ( fileName ); // Externe Datei laden 13 } 14 catch ( Exception ex ) // Im Fehlerfall 15 { 16 Logger . Error ( ex . Message ); // Fehler protokollieren 17 return new List < EquivalenceClass >() ; // Leere Äquivalenzklassen zurückgeben 18 } Beim Hinzufügen eines neuen Wertes zu einer Äquivalenzklasse kann ein Zufallswert im angegebenen Typ generiert werden. Die folgende Abbildung zeigt die Eingabemaske zum Anlegen neuer Äquivalenzklassen-Werte nach betätigen des „Zufallswert“-Buttons, welcher einen Zufallswert in die Textbox schreibt (siehe Abbildung 6.7): (a) Zufallswert DateTime (b) Zufallswert Double Abbildung 6.7: Hinzufügen eines Wertes zu einer Äquivalenzklasse Eine Herausforderung war das Erzeugen zufälliger Werte beim Datentyp double. Die Klasse System.Random erzeugt Double-Zufallszahlen zwischen 0 und 1. Für Werte in einem anderen Min-Max-Bereich wird folgende Berechnung durchgeführt: (Max − Min) ∗ rnd + Min mit rnd = Zufallszahl zwischen 0 und 1. Für Max = Double.MaxValue und Min = Double.MinValue ergibt der Ausdruck (Max - Min) den Wert Infinity, welcher Unendlich symbolisiert. Durch weiterführende Additionen und Subtraktionen verändert sich dieser Wert nicht mehr, weswegen die Double-Zufallszahlen immer das konstante Sym- 69 6 Implementierung des Testsystems bol Infinity wiederspiegelten. Aus diesem Grund wurde eine Long-Zufallszahl generiert und mittels double rn = BitConverter.Int64BitsToDouble( longValue ); direkt im Speicher in einen Double-Wert konvertiert. Somit steht der gesamte DoubleBereich zur Verfügung und jede Zahl konnte generiert werden. 6.3 Fertigstellung von AutoTest.Net Nachdem die Methoden implementiert wurden, entstanden erste Herausforderungen bezüglich der Benutzbarkeit der Software. Objekte und detaillierte Informationen zu Objekten müssen auch bei größenveränderbaren Masken übersichtlich dargestellt werden. Weiterhin muss zu jedem Zeitpunkt klar werden, wo sich der Benutzer befindet und welche Schritte als nächstes zu tun sind. Das folgende Bild zeigt das Hauptprogramm mit Funktionalitäten wie dem Laden und Analysieren des Objektes System. ComponentModel.BackgroundWorker der Assembly „System.dll“ sowie den Arbeitsfluss, den die Software dem Benutzer vorgibt, um ihn durch das Programm zu leiten (siehe Abbildung 6.8): Abbildung 6.8: Analyse des Backgroundworkers mit AutoTest.Net 2008 Nach dem Laden einer Assembly stehen alle enthaltenen Klassen zur Verfügung. Nachdem ein zu testender Typ ausgewählt wurde, erscheint dieser in der Auflistung auf der rechten Seite. Gleichzeitig füllen sich beide Auflistungen im unteren Bereich der Anwendung mit den gefundenen Konstruktoren sowie den gefundenen Methoden. Nach dem 70 6 Implementierung des Testsystems Auswählen der zu testenden Methoden und Konstruktoren können die Tests gestartet werden. Sofort wird die Ergebnissmaske angezeigt, zu Beginn allerdings ohne Ergebnisse. Während der Tests wird der Fortschritt in % mittels eines „Progressbars“ dargestellt, der sich auf der Ergebnissmaske befinden. Sind die Tests abgeschlossen, werden die Ergebnisse aufgelistet. 71 Sollanalyse - Testen des Testsystems [7] 7.1 In diesem Kapitel werden die Ergebnisse, die mit AutoTest.Net erzielt wurden, darge- Testergebnisse stellt. Getestet werden eigens programmierte Test-Assemblies sowie vorhandene .NET- mit selbstent- Assemblies. wickelter Test-Assembly 7.1 Testergebnisse mit selbstentwickelter Test-Assembly 7.2 Testergebnisse Zu Beginn wird eine Assembly mit einer Klasse TestClass entwickelt. Mit dieser Assembly sollen folgende Fehler gefunden werden: • Zugriffe auf Objekte, die null (nothing in Visual Basic) sind • Überlaufsfehler / Umwandslungsfehler • Zugriff auf nicht vorhandene Klassenmember Sollte kein Konstruktor angegeben sein, so enthält eine Klasse automatisch einen öffentlichen Standardkonstruktor. Die erste Test-Methode GetStringLength soll die Länge eines Strings zurückgeben. Die Länge des Strings wird mittels der Property Length ermittelt (siehe Listing 7.1): Listing 7.1: Test-Assembly (C#) - Methode GetStringLength 1 namespace TestLib 2 { 3 public class TestClass 4 { 5 public int GetStringLength ( string s ) 6 { 7 return s. Length ; 8 9 // Länge des Strings zurückgeben } } 10 } 72 mit SystemAssembly 7.3 Vergleich mit NUnit 7.4 Zusammenfassung 7 Sollanalyse - Testen des Testsystems Sollte allerdings null übergeben werden, was ein gültiger Wert für einen String ist, so wird eine System.NullReferenceException geworfen. Die folgende Abbildung zeigt die Auflistung der Ergebnisse (siehe Abbildung 7.1): Abbildung 7.1: Ergebnisse des Tests der Methode GetStringLength Der Test verlief erfolgreich und der vermeidliche Fehler wurde von AutoTest.Net erkannt. Zwei weitere Methoden, Round und ArrayTest, sollen getestet werden. Die Methode Round beinhaltet eine Umwandlung von einem großen in einen kleineren Datentyp und erinnert an die Funktion, die die „Ariane 5“-Rakete zum Absturz brachte (vgl. Abschnitt 2.3.2 auf Seite 13). Die Methode ArrayTest iteriert durch ein Array vom Typ string und verarbeitet die einzelnen Werte. Diese Methoden sind durchaus in der Praxis denkbar und sollen zeigen, dass auch nicht offensichtliche Fehler gefunden werden können (siehe Listing 7.2): Listing 7.2: Test-Assembly (C#) - Methoden GetStringLength, Round und ArrayTest 1 using System ; 2 namespace TestLib 3 { 4 public class TestClass 5 { 6 public int GetStringLength ( string s ) 7 { 8 return s. Length ; 9 // Länge des Strings zurückgeben } 10 11 public int Round ( double roundValue ) 12 { 13 // Double - Wert nach Ganzzahl runden return ( int ) Math . Round ( ( decimal ) roundValue ); 14 } 15 16 public void ArrayTest ( string [] values ) 17 { // Durchiterieren aller Werte des Arrays 18 foreach ( string item in values ) 19 { 20 Console . WriteLine ( item ); 21 } 22 23 } } 24 } 73 7 Sollanalyse - Testen des Testsystems Der Fehler in der Methode Round ist die Umwandlung von double nach decimal. Für kleine Werte, die auch im Bereich von decimal liegen, funktionieren die Rundungen. Für zu große oder zu kleine Werte schlägt diese Methode fehl. Die Methode ArrayTest stürzt ab, wenn null übergeben wird. Das Resultat der Tests wird von AutoTest.Net wie folgt dargestellt (siehe Abbildung 7.2): Abbildung 7.2: Ergebnisse des Tests der Methoden GetStringLength, Round und ArrayTest Das nächste Beispiel wird in Visual Basic dargestellt. Die Klasse VBTestClass beinhaltet die Methode DateToString, welche ein DateTime-Objekt erwartet und das enthaltene Datum als String zurückgibt (siehe Listing 7.3). Listing 7.3: Test-Assembly (VB) - Methode DateToString 1 Public Class VBTestClass 2 3 4 Function DateToString ( ByVal dateParam ) ’As DateTime DateToString = dateParam . Date . ToString () End Function 5 End Class Der Fehler in diesem Beispiel ist die fehlende Typspezifizierung hinter dem Parameter „dateParam“ („As DateTime“). Der Compiler interpretiert diesen Parameter automatisch als object, weswegen kein Compilerfehler entsteht. AutoTest.Net erkennt diesen Fehler, weil ein erwartetes Objekt mit new erzeugt und als Parameterwert verwendet wird. Ein Objekt vom Typ object beinhaltet allerdings keinen Member „Date“, weshalb eine MissingMemberException geworfen wird (siehe Abbildung 7.3): Abbildung 7.3: Ergebnisse des Tests der Methode DateToString Durch das Hinzufügen der Typsicherheit wird diese Methode wesentlich robuster. 74 7 Sollanalyse - Testen des Testsystems 7.2 Testergebnisse mit System-Assembly Nachdem die ersten Ergebnisse mit simplen Assemblies vorhanden sind, sollen nun komplexe Objekte aus dem Namespace System getestet werden. Dieser Namespace beinhaltet von insgesamt 1955 Typen exakt 1367 Klassen. Die Klasse ByteConverter wurde mit den folgenden Resultaten getestet (siehe Abbildung 7.4): Abbildung 7.4: Ergebnisse der Tests des Objektes ByteConverter Die Methoden FromString und ToString werfen insgesamt 3 unterschiedliche Exceptions bei ungültigen Parametern. Eine NullReferenceException deutet auf ein unberücksichtigtes Fehlverhalten hin, wohingegen eine FormatException und eine InvalidCastException durchaus für diese Parameter beabsichtigt sein könnten. Im Zweifelsfall muss der Entwickler seine Methoden mit diesen Parametern aufrufen und den Code verfolgen. Die ArgumentException und ArgumentNullException wurden hierbei nicht aufgelistet, da sie zu den erwarteten Exceptions gehören. 7.3 Vergleich mit NUnit Um die beiden Methoden der selbsterstellten Test-Assembly GetStringLength( string) und Round(double) (siehe Listing 7.2 auf Seite 73) mit NUnit zu testen, muss mittels des Assistenten ein Test-Projekt erzeugt werden. Das Projekt ist zwar schnell erzeugt, aber das korrekte Einsetzen von Testwerten und erwarteten Werten nimmt relativ viel Zeit in Anspruch. Das sorgfältige Implementieren aller Testdaten für diese beiden 75 7 Sollanalyse - Testen des Testsystems Methoden, die in AutoTest.Net hinterlegt sind, dauert ca. 10 Minuten. Mit AutoTest.Net können diese Fehler in weniger als 10 Sekunden gefunden werden, da die Datei lediglich geladen und die zu testenden Methoden ausgewählt werden müssen. Im Durchschnitt liegen die Zeiten für Tests einer Klasse mit allen Konstruktoren und Methoden zwischen wenigen Sekunden und ca. fünf Minuten. Abgesehen vom zeitlichen Aufwand ist auch der Umfang ein erwähnenswertes Kriterium. Das Listing dieser NUnit-Tests ist aufgrund der Größe im Anhang zu finden (siehe Listing 1 auf Seite XIII). 7.4 Zusammenfassung Die Testreihen stellen einen großen Erfolg für AutoTest.Net dar. Der Aufwand, mit dem Abstürze von Methoden gefunden werden können, ist im Gegensatz zum Aufwand des Findens der Abstürze mit NUnit minimal. Selbst ein Fehler in Form dessen, der die „Ariane 5“-Rakete zum Absturz brachte, konnte mit AutoTest.Net identifiziert werden. Die am häufigsten auftretende Exception ist die NullReferenceException. Besonders bei String-Parametern wird dies oft übersehen. Tests können auf Knopfdruck ohne besondere Vorbereitung durchgeführt werden und geben dem Entwickler einen guten Anhaltspunkt für nicht bedachte Fälle. 76 Fazit [8] 8.1 Im Rahmen dieser Arbeit wurden viele Softwarefehler mit teilweise fatalen Auswir- Zusammen- kungen vorgestellt und deren Ursachen analysiert. Ebenfalls wurden Fehler aus der Theo- fassung rie der Programmierung erörtert. Der Kern der Arbeit beschäftigte sich aber mit der Ent- 8.2 wicklung eines eigenen Testsystems. Grenzen 8.3 Ausblick 8.1 Zusammenfassung 8.4 Es wurde gezeigt, welche Auswirkungen Softwarefehler haben können. Die Implementierung von AutoTest.Net, das Testsystem, welches potentielle Fehler ohne viel Aufwand finden kann, erfolgte größtenteils ohne Probleme. Nach dem mehrfachen Benutzen von AutoTest.Net mit unterschiedlichen Testobjekten wurde AutoTest.Net immer weiter optimiert und benutzerfreundlicher gestaltet. Die Ergebnisse, die mit AutoTest.Net erzielt werden konnten, waren weitaus aussagekräftiger, als zu Beginn geplant. Die Einfachheit, mit der die Tests durchgeführt werden können, überragt alle bisher vorgestellten Testwerkzeuge. 8.2 Grenzen Durch das Verfahren, auf dem AutoTest.Net basiert, ist es nicht möglich, Rückschlüsse auf die Ausgabewerte, bezogen auf die Eingangswerte, zu ziehen. Somit ist das erfolgreiche Durchlaufen eines Tests bei weitem keine Aussage dafür, dass die Methode ordnungsgemäß arbeitet. Die Rückgabewerte von Methoden werden nicht berücksichtigt. Weiterhin sind unerwartet auftretende Fenster wie MessageBoxen ein Problem, da so die Tests unterbrochen werden. Dieses Problem wurde von anderen Testwerkzeugen bereits gelöst. 77 Schlusswort 8 Fazit 8.3 Ausblick AutoTest.Net birgt noch viel Potential. Sollte ein Einsatz bei Firmen stattfinden, so werden sich viele weitere Wünsche an die Software ergeben. Die Liste von optionalen Features (vgl. Abschnitt 5.2 auf Seite 55) kann zur Komplettierung der Software gänzlich umgesetzt werden. Auch bei Optimierungen gibt es mehrere Möglichkeiten. Bisher werden alle Tests in einem Thread abgearbeitet. Dies könnte durch mehrere Threads stärker parallelisiert und effizienter gestaltet werden. Die Rückgabewerte von aufgerufenen Methoden könnten ausgegeben und durch den Tester analysiert werden. Die Anbindung an datengetriebene Tests mithilfe einer Datenbank könnte ebenfalls umgesetzt werden. Durch die Anbindung einer Datenbank würde sich aber das 2-Schichten-Modell zu einem 3- oder sogar 5-Schichten-Modell entwickeln, je nach Grad der Komplexität. 8.4 Schlusswort Die größten Herausforderungen dieser Diplomarbeit waren das Planen von AutoTest.Net sowie das Behandeln der bei der Implementierung auftretenden Ausnahmefälle. AutoTest.Net kann vielen Entwicklern nützlich sein, ihre Software zu optimieren. Es zeigt ihnen unbedachte Fälle in ihrer Software, die später beim Benutzer fatale Folgen haben könnten. Dieses Testsystem liefert einen entscheidenden Beitrag, zukünftige Softwarelösungen, sicherer, effektiver und fehlerfreier zu machen. „There are no significant bugs in our released software that any significant number of users want fixed.“ Bill Gates 78 Literaturverzeichnis [Bal05] H. Balzert, Lehrbuch der Objektmodellierung - Analyse und Entwurf mit der UML 2.0. Heidelberg: Elsevier - Spektrum Akademischer Verlag, 2005. X, XI [Bal08] ——, “Software-Qualitätssicherung,” Ruhr-Universität Bochum, März 2003. Zugriff: 23 Juni 08. [Online]. Available: http://www.inf.fu-berlin.de/inst/ ag-se/teaching/V-SWT-2003/66_2LE_19tw.pdf 30, 32, 51 [Bas08] V. R. Basili, per Mail, Department of Computer Science and Institute for Advanced Computer Studies - University Maryland, 19. Juni 2008. 8 [CA05] K. Cwalina and B. Abrams, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Microsoft .NET Development Series). Addison-Wesley Professional, September 2005. 43 [Dai08] A. Dais, “Softwarequalität und Test von .NET-Anwendungen,” .Net Developers Group Stuttgart, Mai 2005. Zugriff: 28. Juni 08. [Online]. Available: http://www.devgroup-stuttgart.de/Download/2005-05-25/ DevgroupVortragDais.pdf 33 [Edd08] M. Eddington, “Peach 2 Tutorial,” peachfuzzer.com, Juni 2008. Zugriff: 24. Juni 08. [Online]. Available: http://peachfuzzer.com/docs/Peach%202% 20Tutorial.pdf 41 [Ger08] M. Gerke, “Vorlesung „Software-Engineering“,” FHDW Hannover, Januar 2007. Zugriff: 09 Juni 08. [Online]. Available: http://www.glenesoft.com/ fhdw/OOTest/PrintAll.shtml 9, 18, 19, 23, 25, 30, 32 [Gie08] I. Giese, “Softwarezuverlässigkeit gestern, heute und morgen,” GSI Darmstadt, Februar 2002. Zugriff: 12 Juni 08. [Online]. Available: http://www-aix.gsi.de/~giese/swr/allehtml.html 9, 10, 11, 12, 13, 14, VIII I [GK08] M. Gallaher and B. Kropp, “The Economic Impacts of Inadequate Infrastructure for Software Testing,” RTI International for National Institute of Standards & Technology, Mai 2002 ch. 1 - Introduction of Software Quality and Testing. Zugriff: 18 Juni 08. [Online]. Available: http://www.rti.org/abstract.cfm?pid=5272 22 [Gla08] C. Glass, “Berühmt berüchtigte Softwarefehler - Mars Climate Orbiter und Mars Polar Lander,” Universität Koblenz, September 2003. Zugriff: 15. Juni 08. [Online]. Available: http://www.uni-koblenz.de/~beckert/Lehre/ Seminar-Softwarefehler/Ausarbeitungen/glass.pdf 16 [HT04] A. Hunt and D. Thomas, Pragmatic Unit Testing in C# with Nunit. The Pragmatic Programmers; 1st edition, Mai 2004. 41 [Huc08] T. Huckle, “Kleine BUGs, große GAUs - Softwarefehler und ihre Folgen,” Technische Universität München, März 2003. Zugriff: 09. Juni 08. [Online]. Available: http://www5.in.tum.de/~huckle/bugsn.pdf 13 [IS08] A. S. Ina Schieferdecker, “Mythos „deutsche Qualität“ – schaffen wir ihn auch für unsere Software zu etablieren?” TU Berllin/Uni Bremen/Fraunhofer FOKUS, Oktober 2007. Zugriff: 12. Juni 08. [Online]. Available: http://www.cs.tu-berlin.de/frauenportal/data/VortragTestenv2.pdf 12, VIII [Kea08] D. Kean, per Mail, Microsoft FxCop-Entwickler, 14. März 2008, Microsoft Code Analysis Team. 42 [Kre08] J. Kresowaty, “FxCop: Writing Your Own Custom Rules (DRAFT),” Januar 2008. Zugriff: 05 Juni 08. [Online]. Available: http://www.binarycoder.net/ fxcop/pdf/fxcop.pdf 42, 43 [Lin85] H. Lin, “The development of software for ballistic-missile defense,” Sci. Am., vol. 253, no. 6, pp. 46–53, 1985. 17 [Mer08] A. Mertgen, “Debugging,” Technische Universität Berlin, Juni 2008. [Online]. Available: https://www.isis.tu-berlin.de/course/view.php?id=1016&topic=3 7 [MFS90] B. P. Miller, L. Fredriksen, and B. So, “An Empirical Study of the Reliability of UNIX Utilities,” Commun. ACM, vol. 33, no. 12, pp. 32–44, 1990. 34, 35 [Mil08] B. P. Miller, per Mail, The University of Wisconsin, 20. Januar 2008. 34 II [Möl08] R. Möller, “Vorlesung „Software-Engineering“,” Technische Universität Hamburg, Mai 2004, ch. 6 - Qualität-Metriken-Tests. Zugriff: 15. Juni 08. [Online]. Available: http://www.sts.tu-harburg.de/~r.f.moeller/lectures/ se-ss-04.html 19, 20, 21, 22, 23, 24, 26, 27 [NSS07] T. Northrup, S. J. Stein, and M. A. Stoecker, Anwendungsentwicklung für Windows-Clients mit .NET Framework 2.0. Redmond: Microsoft Press, 2007. XI [NWR06] T. Northrup, S. Wildermuth, and B. Ryan, Grundlagen der Anwendungsentwicklung mit .NET Framework 2.0. Redmond: Microsoft Press, 2006. 21, X [Pfe08] M. 25,” Pfeifer, “Berühmt Universität berüchtigte August Koblenz, Softwarefehler 2003. Zugriff: - Therac15. Ju- ni 08. [Online]. Available: http://www.uni-koblenz.de/~beckert/Lehre/ Seminar-Softwarefehler/Ausarbeitungen/pfeifer.pdf 16 [PGL08] A. K. Patrice Godefroid and M. Y. Levin, “Grammar-based Whitebox Fuzzing,” Microsoft Research, November 2007. Zugriff: 25. Juni 08. [Online]. Available: http://research.microsoft.com/research/pubs/view.aspx? tr_id=1397 42 [Pol08] A. Poller, “Approaches for Automated Software Security Evaluations,” Ph.D. dissertation, Chemnitz University of Technology, Oktober 2006. Zugriff: 17 Juni 08. [Online]. Available: http://archiv.tu-chemnitz.de/pub/2006/0187/ data/AutoSecEval.pdf 34 [See08] S. Seefeld, “Unit-Test - Theorie und Praxis,” INGTES AG, Nov 2006. Zugriff: 28. Juni 08. [Online]. Available: http://www.ingtes.ch/fileadmin/user_upload/ media/UnitTest_Theorie_und_Praxis.pdf 33 [SGA07] M. Sutton, A. Greene, and P. Amini, Fuzzing: Brute Force Vulnerability Discovery. Addison-Wesley Professional; 1 edition, Juli 2007, ch. 21 - Fuzzing Frameworks. 38 [Sil08] B. Silberhorn, “White-Box-Test,” Fachhochschule Augsburg, Mai 2006. Zugriff: 28. Juni 08. [Online]. Available: http://www2.fh-augsburg.de/ informatik/master/vorlesungen/swt/script/ss2006/testen/whitebox.pdf 33 III [SL02] A. Spillner and T. Linz, Basiswissen Softwaretest. Heidelberg: Dpunkt Verlag, 2002. 24 [Spi08] A. Spillner, “Das W-Modell,” Hochschule Bremen, Mai 2003. Zugriff: 21. Juni 08. [Online]. Available: http://www.gi-hb-ol.de/uta/gi-rg/ WModellSpillner.pdf 28, 29 [sR08] Åsmund Realfsen, “Analyse der Softwarefehler und die Erstellung von Software zur Qualitätsverbesserung des OpenACS-Assessmentpakets,” Wirtschaftsuniversität Wien, Juni 2006. Zugriff: 18 Juni 08. [Online]. Available: http://epub.wu-wien.ac.at/dyn/virlib/bakkWI/showentry?ID= epub-wu-01_aa2 7, 8 [TS04] D. A. K. Timo Schmitt, Ralf Heid, “Grundlagenwissen Modellbasierte Spezifikation und Testen,” Eine Publikation des CBTesten Konsortiums, September 2004. 23, 24, 25, 26, 27 [Vog08] U. Voges, “Definitionen von Begriffen im Kontext ‚Sicherheit (safety)’,” Forschungszentrum Karlsruhe, August 2002. Zugriff: 12. Juni 08. [Online]. Available: http://www.m-lehrstuhl.de/veranstaltung/GI_WS_07_ 02/Voges.doc 7, 19, 20 [vV08] E. van Veenendaal, “Standard glossary of terms used in Software Testing,” International Software Testing Qualifications Board, Dezember 2007. Zugriff: 23 Juni 08. [Online]. Available: http://www.istqb.org/downloads/ glossary-current.pdf 19, 20, 28, IX, XI [Wei08] D. Weinstein, “Fuzzing Fundmentals,” Redmond Developer News, Juni 2007. Zugriff: 22. Juni 08. [Online]. Available: http://reddevnews.com/techbriefs/ article.aspx?editorialsid=261 36 [Wel08] D. Wells, per Mail, TestComplete-Entwickler, 25. März 2008, AutomatedQA Entwicklerteam. 48 [Wil04] K. B. Williams, Grace Hopper: Admiral of the Cyber Sea. brary of Naval Biography, 2004. 6, 7 IV Annapolis: Li- Abbildungsverzeichnis 2.1 Erwartete Fehlerrate im Programmcode nach Basili . . . . . . . . . . . . 8 3.1 Allgemeines V-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.2 Allgemeines W-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4.1 NUnit - Erfolgreicher Gesamttest . . . . . . . . . . . . . . . . . . . . . . 39 4.2 NUnit 2008 - Generierung von Testklassen . . . . . . . . . . . . . . . . . 40 4.3 RanorexSpy liest Daten aus . . . . . . . . . . . . . . . . . . . . . . . . . 45 4.4 Properties des Buttons 2 des Windows-Taschenrechners, ausgelesen von TestComplete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 5.1 Anwendungsfalldiagramm für AutoTest.Net . . . . . . . . . . . . . . . . 50 5.2 Log-Bereich in AutoTest.Net 2008 . . . . . . . . . . . . . . . . . . . . . 56 5.3 Der Log-Bereich in AutoTest.Net 2008 . . . . . . . . . . . . . . . . . . . 56 5.4 Verwaltungsbereich für die Exceptions in AutoTest.Net 2008 . . . . . . . 57 5.5 Equivalenzklasse für AutoTest.Net . . . . . . . . . . . . . . . . . . . . . 58 5.6 Exceptionverwaltungsklasse für AutoTest.Net . . . . . . . . . . . . . . . 59 5.7 Klassendiagramm für AutoTest.Net . . . . . . . . . . . . . . . . . . . . . 61 6.1 Anwendungsschichten von AutoTest.Net . . . . . . . . . . . . . . . . . . 62 6.2 GUI von AutoTest.Net . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 6.3 Erstes Layout von AutoTest.Net 2008 . . . . . . . . . . . . . . . . . . . . 64 6.4 Unterstützende Snaplines beim Bau einer Maske . . . . . . . . . . . . . 64 6.5 Maske zur Analyse der Assembly . . . . . . . . . . . . . . . . . . . . . . 65 6.6 Darstellung von gruppierten Ergebnissen . . . . . . . . . . . . . . . . . . 67 6.7 Hinzufügen eines Wertes zu einer Äquivalenzklasse . . . . . . . . . . . . 69 6.8 Analyse des Backgroundworkers mit AutoTest.Net 2008 . . . . . . . . . . 70 7.1 Ergebnisse des Tests der Methode GetStringLength . . . . . . . . . . . . 73 7.2 Ergebnisse des Tests der Methoden GetStringLength, Round und ArrayTest 74 V 7.3 Ergebnisse des Tests der Methode DateToString . . . . . . . . . . . . . . 74 7.4 Ergebnisse der Tests des Objektes ByteConverter . . . . . . . . . . . . . 75 VI Tabellenverzeichnis 2.1 Programme mit der Anzahl der Quellcodezeilen und Fehler . . . . . . . . 3.1 Liste der Qualitätsmerkmale . . . . . . . . . . . . . . . . . . . . . . . . 22 3.2 Liste der beim Test 1990 abgestürzten Programme . . . . . . . . . . . . . 35 3.3 Liste der getesteten Systeme . . . . . . . . . . . . . . . . . . . . . . . . 35 5.1 Liste der optionalen Features . . . . . . . . . . . . . . . . . . . . . . . . 55 VII 9 Listings 1.1 Beispiel für Property . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2 Beispiele für ein Attribut . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Beispiele für Reflection . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.1 Ausschnitt aus dem FORTRAN-Steuerprogramm von Mariner 1 (Gie08) . 11 2.2 Beispiele für Tippfehler und deren Auswirkungen (IS08) . . . . . . . . . 12 2.3 Beispiele für Tippfehler in C-Schleifen . . . . . . . . . . . . . . . . . . . 12 2.4 Auszug aus dem Trägheitsnavigationssystem der „Ariane 5“ (Gie08) . . . 13 2.5 Beispiel für sichere Handhabung von Überläufen . . . . . . . . . . . . . 15 4.1 Beispiel eines TestComplete-Tests des Taschenrechners, ob 2 + 5 = 7 ist . 47 5.1 Singleton-Pattern der ExceptionManagement-Klasse . . . . . . . . . . . 59 6.1 Speichern aller Werte einer Enumeration . . . . . . . . . . . . . . . . . . 66 6.2 Gruppierung von Objekten mit LINQ . . . . . . . . . . . . . . . . . . . 67 6.3 ExceptionManagement. Auflistung aller Exceptions des Namespace System 68 6.4 Zugriff auf interne eingebettete Ressourcen . . . . . . . . . . . . . . . . 68 7.1 Test-Assembly (C#) - Methode GetStringLength . . . . . . . . . . . . . . 72 7.2 Test-Assembly (C#) - Methoden GetStringLength, Round und ArrayTest . 73 7.3 Test-Assembly (VB) - Methode DateToString . . . . . . . . . . . . . . . 74 1 NUnit-Klasse, welche die Testmethoden mit allen Werten der Äquivalenzklassen testet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XIII VIII Glossar A Äquivalenzklasse Eine Menge von Testdaten, bei der jeder Wert als Eingabe für ein Testobjekt gleichartiges Sollverhalten zeigt (Äquivalenzklasse von Eingabewerten) (vV08), S. 23. Assembly Eine mit .NET übersetzte Datei, welche typischerweise die Endung .exe oder .dll trägt, S. 2. Attribut Ein Attribut ist eine Metainformation und kann auf Klassen, Methoden oder Properties angewendet werden, S. 4. C Compiler Ein .Net-Sprachcompiler übersetzt den Quellcode in eine platformunabhängige Zwischensprache, vergleichbar mit Java-Bytecode. Diese Sprache wird als CIL (Common Intermediate Language), MSIL (Microsoft Intermediate Language) oder einfach IL (Intermediate Language) bezeichnet. Bei der Ausführung übersetzt ein JIT-Compiler (Just In Time-Compiler) den in der Zwischensprache vorliegenden Quellcodes der Assemblies in Maschinencode, S. 3. E Exception Eine Exception ist ein Objekt, welches bei unerwarteten Ereignissen geworfen wird und die normale Ausführung unterbricht. Dieses Objekt muss später behandelt werden, da sonst die gesamte Ausführung abgebrochen wird. Alle Exception-Objekte haben die Klasse Exception als Basistyp, S. 14. IX F Fuzzing Eine Technik, um mit zufällig generierten Werten, welche allerdings einer bestimmten Logik unterliegen, eine Software zum Fehlverhalten zu führen, S. ii. H Handle Ein Handle ist ein Objekt, welches genutzt wird, um andere Objekte zu steuern. Häufig wird ein Handle genutzt, um Ressourcen zu initialisieren oder freizugeben, S. 33. I Introspection Eine Technik, welche ähnlich arbeitet wie Reflection, allerdings mit wei- terführenden Möglichkeiten der Assemblyanalyse, S. 5. N Namespace Ein Namespace (dt. Namensraum) ist in .NET sowie in der UML ein gebräuchlicher Begriff. Es handelt sich um einen Bereich, in dem jedes Element einen eindeutigen Namen besitzen muss. In unterschiedlichen Namespaces kann der gleiche Name in unterschiedlicher Bedeutungen verwendet werden (Bal05, S. 538), S. 3. P Properties Eine Property ist ein Bestandteil eines Objektes in .NET. Properties sind interne Felder, welche gekapselt und nach außen geführt werden, S. 3. R Reflection Eine Fähigkeit des .NET Frameworks, um erstens Typeninformationen im Typensystem abzufragen und auszuwerten, und zweitens Code dynamisch zu erstellen (NWR06, S. 761), S. 4. X S Singleton Das Singleton-Muster (engl. singleton pattern) ist ein objektbasiertes Erzeugungsmuster, welches sicherstellt, dass genau eine Instanz einer Klasse existiert, auf die global zugegriffen werden kann. Diese Klassen haben nur private Konstruktoren (Bal05, S. 544), S. 59. Snapline Hilfslinien, die von Microsoft Visual Studio zur Unterstützung für die genauere Anordnung von Steuerelementen angezeigt werden (NSS07, S. 677), S. 64. T Testobjekt Ein zu testendes Softwareobjekt (Funktion/Methode, Modul/Klasse, Komponente/Teilsystem, System). Das Testobjekt soll für den Test nicht verändert werden und alle nötigen Umgebungskomponenten sollten bereits ausreichend getestet sein (vV08), S. 19. Testorakel Informationsquelle zur Ermittlung der jeweiligen Sollergebnisse eines Testfalls (kann beispielsweise die Anforderungsdefinition sein) (vV08), S. 18. Testtreiber Programm bzw. Werkzeug, das es ermöglicht, ein Testobjekt ablaufen zu lassen, mit Testdaten zu versorgen und Ausgaben/Reaktionen des Testobjekts entgegenzunehmen (vV08), S. 19. U UML UML (Unified Modeling Language) ist eine als Standard akzeptierte Modellierungssprache, die graphische Notationen für statische Strukturen und dynamische Abläufe in Software enthält (Bal05, S. 546), S. 4. XI Index Äquivalenzklasse, IX, XIII, 23, 24, 49, Testorakel, XI, 18, 19, 33 50, 52, 54, 56–58, 60, 63, 65– Testtreiber, XI, 19, 33, 36, 37 69 UML, X, XI, 4, 49 Assembly, IX, 2–4, 38, 42, 43, 49–52, 57, 65, 66, 70, 72, 75 Attribut, IX, 4, 39 Compiler, IX, 3, 6, 10, 11, 14 Exception, IX, 14, 50, 52–54, 56, 59, 65–68, 75, 76 Fuzzing, ii, X, 2, 3, 30, 34, 36, 41, 42, 49 Handle, X, 33 Introspection, X, 5, 42, 43 Namespace, X, 3, 5, 53, 54, 56, 62, 68, 75 Properties, IX, 3, 4, 39, 41, 43, 46, 60, 65 Property, X Reflection, X, 4, 5, 42, 43, 49, 66 Singleton, XI, 59, 60 Snapline, XI, 64 Testobjekt, XI, 19, 20, 33, 41, 67 XII Anhang Listings Listing 1: NUnit-Klasse, welche die Testmethoden mit allen Werten der Äquivalenzklassen testet 1 using TestLib ; 2 using Microsoft . VisualStudio . TestTools . UnitTesting ; 3 namespace TestProject1 4 { 5 [ TestClass () ] 6 public class TestClassTest 7 { 8 private TestContext testContextInstance ; 9 10 public TestContext TestContext 11 { 12 get 13 { 14 return testContextInstance ; 15 } 16 set 17 { 18 testContextInstance = value ; 19 20 } } 21 22 [ TestMethod () ] 23 public void GetStringLengthTest () 24 { 25 TestClass target = new TestClass () ; // TODO : Passenden Wert 26 string s = " muh und bla "; // TODO : Passenden Wert initialisieren 27 int expected = 11; // TODO : Passenden Wert initialisieren 28 int actual ; 29 actual = target . GetStringLength ( s ); 30 Assert . AreEqual ( expected , actual ); initialisieren 31 XIII 32 s = ""; // TODO : Passenden Wert initialisieren 33 expected = 0; // TODO : Passenden Wert initialisieren 34 actual = target . GetStringLength ( s ); 35 Assert . AreEqual ( expected , actual ); 36 37 s = "0"; // TODO : Passenden Wert initialisieren 38 expected = 1; // TODO : Passenden Wert initialisieren 39 actual = target . GetStringLength ( s ); 40 Assert . AreEqual ( expected , actual ); 41 42 s = " 54256246542 "; // TODO : Passenden Wert initialisieren 43 expected = 11; // TODO : Passenden Wert initialisieren 44 actual = target . GetStringLength ( s ); 45 Assert . AreEqual ( expected , actual ); 46 47 s = " string "; // TODO : Passenden Wert initialisieren 48 expected = 6; // TODO : Passenden Wert initialisieren 49 actual = target . GetStringLength ( s ); 50 Assert . AreEqual ( expected , actual ); 51 52 s = " STRING "; // TODO : Passenden Wert initialisieren 53 expected = 6; // TODO : Passenden Wert initialisieren 54 actual = target . GetStringLength ( s ); 55 Assert . AreEqual ( expected , actual ); 56 57 s = " String with spaces "; // TODO : Passenden Wert initialisieren 58 expected = 18; // TODO : Passenden Wert initialisieren 59 actual = target . GetStringLength ( s ); 60 Assert . AreEqual ( expected , actual ); 61 62 s = null ; // TODO : Passenden Wert initialisieren 63 expected = 0; // TODO : Passenden Wert initialisieren 64 actual = target . GetStringLength ( s ); 65 66 Assert . AreEqual ( expected , actual ); } 67 68 [ TestMethod () ] 69 public void RoundTest () 70 { 71 TestClass target = new TestClass () ; // TODO : Passenden Wert 72 double roundValue = 0F; // TODO : Passenden Wert initialisieren 73 int expected = 0; // TODO : Passenden Wert initialisieren 74 int actual ; 75 actual = target . Round ( roundValue ); 76 Assert . AreEqual ( expected , actual ); initialisieren 77 78 roundValue = 1.0; // TODO : Passenden Wert initialisieren 79 expected = 1; // TODO : Passenden Wert initialisieren 80 actual = target . Round ( roundValue ); 81 Assert . AreEqual ( expected , actual ); XIV 82 83 roundValue = -1.0; // TODO : Passenden Wert initialisieren 84 expected = -1; // TODO : Passenden Wert initialisieren 85 actual = target . Round ( roundValue ); 86 Assert . AreEqual ( expected , actual ); 87 88 roundValue = -1.7976 E +308; // TODO : Passenden Wert initialisieren 89 expected = 1000; // TODO : Passenden Wert initialisieren 90 actual = target . Round ( roundValue ); 91 Assert . AreEqual ( expected , actual ); 92 93 } } 94 } XV DVD Inhalt /DOT.NET 3.5 /dotNetFx35setup.exe /Quellen /bin /AutoTest.Net.exe /src /AutoTest.Net.sln /Diplomarbeit.pdf Zum Ausführen notwendiges .NET-Framework 3.5 Installationsdatei für Microsoft .NET 3.5 Framework Benutze, digital vorhandene Quellen vorkompilierte Anwendung Startdatei für AutoTest.Net Quellcode zu AutoTest.Net Projektdatei für das Projekt AutoTest.Net Diplomarbeit in digitaler Form Um die Anwendung ausführen zu können, ist eine Installation des Microsoft .NETFramework 3.5 erforderlich. Für das Öffnen des Projektes und das Kompilieren des Quellcodes ist zudem eine Installation von Microsoft Visual Studio 2008 nötig. Eine Bedienungsanleitung für die Software befindet sich in der Software unter dem Menupunkt „Hilfe“. XVI