Fernuniversität in Hagen Fakultät für Mathematik und Informatik Lehrgebiet Programmiersysteme Prof. Dr. Friedrich Steimann Mutation Testing mit Refacola Abschlussarbeit im Studiengang Bachelor of Science in Informatik Betreuer: Dipl.-Inform. Andreas Thies Februar 2012 Markus Grothoff Matrikelnummer: 7593058 E-Mail: [email protected] Erklärung Hiermit erkläre ich, dass ich diese Abschlussarbeit selbstständig verfasst und noch nicht anderweitig für Prüfungszwecke vorgelegt habe. Ich habe keine anderen als die angegebenen Quellen und Hilfsmittel benutzt. Wörtliche und sinngemäße Zitate wurden als solche gekennzeichnet. Hagen, 28. Februar 2012 Markus Grothoff Inhalt 1 Einleitung ....................................................................................................................................... 1 2 Grundlagen .................................................................................................................................... 3 3 2.1 Unit Tests ................................................................................................................................ 3 2.2 Unit Tests und Mutation Testing ............................................................................................. 5 2.3 Constraint-basierte Refaktorisierung ....................................................................................... 8 2.4 Von der constraint-basierten Refaktorisierung zum Mutant ................................................. 11 2.5 Refacola ................................................................................................................................. 14 Implementierung ......................................................................................................................... 19 3.1 Anforderungen....................................................................................................................... 20 3.2 Umsetzung ............................................................................................................................. 21 3.2.1 Anwendungskern ........................................................................................................... 23 3.2.1.1 Vorbedingungen ........................................................................................................ 24 3.2.1.2 Erzeugung der Java-Faktenbasis ............................................................................... 25 3.2.1.3 Initialisierung des Kontextes einer Refaktorisierung ................................................ 25 3.2.1.4 Generierung von Mutationen ..................................................................................... 26 3.2.1.5 Suche nach Lösungen für Mutationen ....................................................................... 27 3.2.1.6 Änderung des ursprünglichen Programms zu einem Mutanten ................................. 29 3.2.1.7 Ausführung der Testbasis und Bestimmung des Ergebnisses ................................... 30 3.2.2 JUnit Test Runner .......................................................................................................... 31 3.2.3 Benutzungsoberfläche ................................................................................................... 33 3.2.3.1 Aufbau und Inhalt ...................................................................................................... 34 3.2.3.2 Verwendung des Model View Presenter – ViewModel Entwurfsmusters ................ 38 3.2.3.3 Darstellungs-Bug bei Verwendung einer SWT-Tabelle ............................................ 41 3.3 Auszeichnung von Constraint-Regeln ................................................................................... 42 3.4 Beispiel zur Anwendung des Mutation Testing Frameworks ............................................... 46 3.5 Mögliche Verbesserungen und Erweiterungen...................................................................... 51 4 Zusammenfassung und Fazit ...................................................................................................... 55 5 Literaturverzeichnis .................................................................................................................... 57 A Inhalt der beiliegenden DVD ...................................................................................................... 61 B Installation und Konfiguration .................................................................................................. 63 C Benutzungsanleitung für den Refacola-Entwickler.................................................................. 65 D Benutzungsanleitung für den Entwickler .................................................................................. 67 1 Einleitung 1 1 Einleitung Die Anforderungen, die heutige Programme erfüllen müssen, sind vielfältig und führen zu einer entsprechend hohen inneren Komplexität der Software. Die Entwicklung vollzieht sich evolutionär über einen längeren Zeitraum, in welchem neue Funktionen hinzugefügt und Fehler ausgemerzt werden. Dabei besteht immer das Risiko vorhandene Funktionalität zu brechen. Anpassungen der Code-Basis haben direkten Einfluss auf die Struktur und die Qualität des Codes. Das Hinzufügen von Funktionalität führt im Allgemeinen zu einer Reduzierung der Code-Qualität. Diesem kann durch regelmäßige, entwicklungsbegleitende Umstrukturierung der Code-Basis in Form von Refaktorisierungen entgegengewirkt werden. Auch dabei besteht die Gefahr, dass neue Fehler hinzukommen. Im Zuge der Qualitätssicherung wird Software auf unterschiedlichen Ebenen Tests unterzogen. Je später ein Fehler entdeckt wird, desto aufwändiger ist seine Behebung, da Abhängigkeiten zu weiteren Komponenten bestehen können, sodass sich die Eliminierung des Fehlers auch auf andere Bereiche der Software auswirken kann. Manuelle Tests zum Aufspüren von Fehlern können hilfreich sein, sind aber zeitlich aufwendig durchzuführen und lassen sich nur schwierig auf wenige Teilbereiche der Software begrenzen. Automatisierte Tests können, sofern korrekt verwendet, immer unter den gleichen kontrollierten Bedingungen wiederholt und häufiger als manuelle Tests durchgeführt werden. Sie sind im Allgemeinen verlässlicher als manuelle Tests. Nichtsdestotrotz gibt es Aspekte der Software, die nur manuell getestet werden können, wie z. B. ihre Benutzbarkeit. Unit Tests sind der erste Schritt einer Reihe von automatisierten Tests und werden zum isolierten Testen einzelner Software-Module eingesetzt. Dazu werden vom Entwickler Testfälle ausgearbeitet, die in Unit Tests umgesetzt werden. Jeder Testfall überprüft eine konkrete Annahme hinsichtlich der Implementierung einer Klasse oder einer Methode und ist recht feingranular, sodass sich von Unit Tests aufgedeckte Fehler auf einen kleinen Codeabschnitt eingrenzen lassen. Das Mutation Testing setzt genau bei den Unit Tests an. Aus dem zu testenden Programm werden durch automatisierte Manipulationen an der Code-Basis Programme generiert, die sich potenziell anders verhalten als das ursprüngliche Programm. Das veränderte Verhalten wird als fehlerhaft betrachtet. Diese sogenannten Mutanten werden einer Testbasis bestehend aus Unit Tests unterzogen, die nun das geänderte Verhalten entdecken sollen. Mutanten, die von der Testbasis entdeckt werden, bestätigen, dass in dem entsprechenden Fall die Testabdeckung hoch genug war, um das Fehlverhalten festzustellen. Sollten Mutanten die Testbasis passieren, kann das ein Indiz dafür sein, dass die Testabdeckung nicht ausreichend ist, sollte der Mutant tatsächlich ein dem ursprünglichen Programm abweichendes Verhalten aufzeigen. Aus den nicht entdeckten Mutanten gewonnenen Informationen können verwendet werden, um weitere Testfälle zu entwickeln. Die Schwierigkeit beim Mutation Testing besteht darin, einerseits nur syntaktisch und semantisch korrekte Mutanten zu generieren. Andererseits sollen auch nur Mutanten generiert werden, die nicht verhaltensäquivalent zum ursprünglichen Programm sind. Entscheidend ist dabei das von außen sichtbare Verhalten. Denn nur diese Mutanten können auch von einer Testbasis entdeckt werden. Die constraint-basierte Refaktorisierung bietet eine interessante Basis, die, entsprechend für das Mutation Testing angepasst, eine Lösung für diese Probleme darstellen kann. Die Generierung von verhaltensäquivalenten Mutanten kann sie zwar nicht verhindern, aber die Anzahl solcher für das Mutation Testing unerwünschten Mutanten wird reduziert. Ziel dieser Arbeit ist die Erweiterung der am Lehrgebiet Programmiersysteme der Fernuniversität Hagen entwickelten Sprache Refacola, die programmiersprachenunabhängig deklarative Spezifizie- 2 1 Einleitung rungen constraint-basierter Refaktorisierungen ermöglicht, um ein Framework zur Durchführung von Mutation Testing für in Java geschriebene Programme. Dazu kann auf bereits vorhandene Komponenten von Refacola aufgesetzt werden. Mit dem Mutation Testing Framework sollen aus JavaProgrammen Mutanten generiert und auf Basis einer vom Entwickler ausgewählten Testbasis ausgewertet werden. 2 Grundlagen 3 2 Grundlagen Dieses Kapitel stellt die für diese Arbeit nötigen Grundlagen über Unit Tests, Mutation Testing und Refacola zusammen und erläutert ihre Zusammenhänge. Unit Tests sind nicht nur ein wesentlicher Bestandteil der agilen Software-Entwicklung sondern dienen auch als Basis für das Mutation Testing. Abschnitt 2.1 geht kurz auf den Zweck von Unit Tests ein und erläutert einige ihrer Eigenschaften, die für das Mutation Testing von Bedeutung sind. Der Zusammenhang zwischen Unit Tests und Mutation Testing wird dann in Abschnitt 2.2 hergestellt. Es wird beschreiben, was Mutation Testing ist, und welches Ziel damit verfolgt wird. Losgelöst von einer konkreten Implementierung werden der grundsätzliche Ablauf des Mutation Testing und mögliche Probleme, die sich bei der Anwendung ergeben können, dargestellt. Abschnitt 2.3 geht auf die Technik der constraint-basierten Refaktorisierung ein und wie sie das Programmverhalten vor und nach einer Refaktorisierung mit Hilfe eines Constraint-Systems sicherstellen kann. Es wird erläutert welche Bedeutung die aus einem Programm erzeugten Constraints haben und welche Arten unterschieden werden können. Wie die constraint-basierte Refaktorisierung genutzt werden kann, um während des Mutation Testing Programme mit fehlerhaftem Verhalten zu generieren, und welche Anpassungen nötig sind, ist Teil von Abschnitt 2.4. Es wird ebenso beschrieben, welchen Vorteil die Verwendung von Constraints beim Mutation Testing mit sich bringt und welche Probleme des Mutation Testing damit gelöst werden können. In Abschnitt 2.5 werden einige für das Mutation Testing wichtige Bestandteile von Refacola vorgestellt und erläutert, warum sich Refacola als Basis für ein Mutation Testing Framework eignet. Danach werden die Bereiche für die Sprachdefinition und die Constraint-Regeln näher betrachtet. 2.1 Unit Tests Software unterliegt im Laufe ihres Lebens zahlreichen Änderungen, sowohl während ihrer Entwicklung als auch während der Wartung. Es werden Funktionalitäten hinzugefügt, Fehler beseitigt und Refaktorisierungen zur Erhöhung der Codequalität durchgeführt. Jeder Eingriff in den Code hat das Potenzial Fehler hinzuzufügen oder vorhandene Funktionalität zu zerstören. Um dem entgegenzuwirken wird Software verschiedenen Tests unterzogen. Einen ersten Schritt stellen dabei die Unit Tests dar, da sie einzelne Softwaremodule – in objektorientierten Programmiersprachen sind dies die Klassen – isoliert von anderen Softwaremodulen auf ihre Korrektheit testen. Unit Tests stellen die kleinste Testeinheit dar und werden von Entwicklern geschrieben, ausgeführt und gepflegt. Sie zählen zu der Klasse der automatisierten Tests. Unit Tests prüfen, ob das Verhalten von Methoden und Klassen ihren Spezifikationen entsprechen. Üblicherweise folgt ein Unit Test dem Schema: Arrange, Act, Assert. Zuerst wird die zu testende Klasse vorbereitet und für den Testfall konfiguriert. Abhängigkeiten zu anderen Klassen werden durch Stubs und Mocks aufgelöst. Ist dies nicht möglich, kann das ein Hinweis dafür sein, dass Klassen zu eng gekoppelt sind. Danach findet eine Interaktion mit der zu testenden Klasse statt, dessen Ergebnis mit der im Testfall formulierten Annahme verglichen werden soll. Das Ergebnis kann in einfachen Fällen der Rückgabewert einer Methode 4 2 Grundlagen oder der Zustand der Klasse sein. Im letzten Schritt findet ein Abgleich zwischen dem Ergebnis und der Annahme statt. Unit Testing Frameworks wie JUnit 4 unterstützen den Entwickler bei dem Schreiben und Ausführen von Unit Tests und teilen ihm die Ergebnisse der Testdurchläufe mit. Listing 1 zeigt einen einfachen mit JUnit 4 geschriebenen Unit Test, der prüft, ob toString() aufgerufen auf einem Exemplar von Object einen String zurückgibt, der die Zeichenfolge "Object" enthält. Die Variable cut (Class under Test) referenziert ein Objekt der zu testenden Klasse. Da die Standardimplementierung von toString() in Object die Konkatenation aus dem Klassennamen, einem @-Zeichen sowie dem Hashcode des Objekts zurückgibt, trifft die im Test formulierte Annahme zu. Der Unit Test ist damit erfolgreich. public class ObjectTest { @Test public void toString_newInstance_containsObjectString() { Object cut = new Object(); String returnedString = cut.toString(); boolean containsObjectString = returnedString.contains("Object"); assertTrue(containsObjectString); } } Listing 1 Einfacher Unit Test Gute Unit Tests weisen eine Reihe von Eigenschaften auf, von denen die folgende Auswahl für das Mutation Testing wesentlich von Bedeutung ist. Automatisiert Unit Tests können auf Knopfdruck gestartet werden. Manuelle Eingaben oder Konfigurationen sind nicht erforderlich. Dadurch kann jeder Entwickler eines Programms zu jedem Zeitpunkt die Unit Tests ausführen und prüfen, ob die in den Testfällen formulierten Annahmen bezüglich des Programmverhaltens weiterhin korrekt sind. Der Automatismus eliminiert Fehlerquellen, die durch fehlerhafte, manuelle Konfigurationen entstehen können. Je einfacher Unit Tests auszuführen sind, desto häufig werden sie in der Regel von Entwicklern durchgeführt. Fehler können so frühzeitig erkannt werden. Wiederholbar Unit Tests sollen das Ergebnis einer Methode oder das Verhalten einer Klasse überprüfen und Fehler aufdecken. Sollte ein Unit Test fehlschlagen, wird dieser nach Anpassung des Quellcodes vom Entwickler erneut ausgeführt, um zu prüfen, ob der Fehler tatsächlich beseitigt wurde. Im Laufe der Entwicklung wird ein Unit Test vielfach aufgerufen. Damit sich der Entwickler auf das Ergebnis verlassen kann, muss sich der Unit Test ausgehend von einer bestimmten Eingabe, die im Testfall hinterlegt ist, immer gleich verhalten. Insbesondere ist jeder Unit Test isoliert zu betrachten und sollte nicht von anderen abhängig sein. Sie dürfen sich nicht gegenseitig beeinflussen. 2 Grundlagen 5 Schnell Unit Tests müssen sehr häufig ausgeführt werden. Nach jeder Codeänderung soll nämlich geprüft werden, ob die Tests (weiterhin) erfolgreich sind. Je länger die Ausführung der Unit Tests dauert, desto weniger häufig wird der Entwickler sie auch laufen lassen. Fehler, die von ihnen entdeckt werden, fallen dann erst später auf, wenn bereits mehrere Änderungen am Code vorgenommen wurden. 2.2 Unit Tests und Mutation Testing Mutation Testing ist eine Testmethodik, bei der Programme so manipuliert werden, dass sie ein nach außen hin fehlerhaftes Verhalten aufzeigen. Ausgehend von einem syntaktisch und semantisch korrekten Programm werden automatisiert Änderungen an der Codebasis des Programms vorgenommen. Die manipulierten Programme werden anschließend gegen eine Testbasis ausgeführt, welche das fehlerhafte Verhalten erkennen soll. Auf diese Weise kann die Testabdeckung geprüft und, falls fehlerhafte Programme nicht entdeckt wurden, weitere Testdaten gewonnen werden, aus denen dann neue Tests entwickelt werden können. Die durch Manipulation des ursprünglichen Programms entstehenden Programme beim Mutation Testing werden als Mutanten bezeichnet. Ursprünglich wurde das Mutation Testing für imperative Programmiersprachen verwendet. Durch Austausch von Operatoren oder der Manipulation von Prädikaten konnte das Verhalten des Programms verändert werden. Die Änderung einer Bedingung, z. B. wenn ein und-Operator durch einen oder-Operator ersetzt wird, kann den Anwendungsfluss verändern. Objektorientierte Programmiersprachen bieten durch ihre Komplexität und Strukturierung dank Polymorphie und Vererbung darüber hinaus weitere Möglichkeiten das Programmverhalten zu verändern. Hier ist insbesondere die Manipulation des Bindungsverhaltens zu erwähnen, welches sowohl vom deklarierten Typ als auch vom Objekt, das referenziert wird, abhängt. Im Allgemeinen decken die durch das Mutation Testing vorgenommenen Änderungen typische Programmierfehler bei objektorientierten Programmen ab, die häufig in Verbindung mit falsch eingesetzter Vererbung, Überladung von Methoden, deren Spezifikationen voneinander abweichen, und Verdecken von Attributen stehen. Listing 2 zeigt ein Beispiel für die Manipulation von statischer Methodenbindung. Der Aufruf der Methode callMe wird zur Übersetzungszeit an callMe(Object) gebunden, da nur sie außerhalb des Pakets sichtbar ist. Wird die Sichtbarkeit von callMe(String) auf public erhöht, findet ein Umbinden von callMe(Object) auf callMe(String) statt, da der übergebene Wert vom Typ String genau dem Typ des Parameters entspricht. Die Erhöhung der Sichtbarkeit kann beim Mutation Testing ausgenutzt werden, um das Bindungsverhalten zu ändern. Ob sich dann tatsächlich das von außen sichtbare Programmverhalten ändert, welches von Unit Tests überprüft werden kann, hängt von der Implementierung ab. 6 2 Grundlagen package de; public class StaticBinding { public void callMe(Object obj) { } void callMe(String string) { } } package en; public class Foo { public static void main(String[] args) { new de.StaticBinding().callMe("Hello World"); } } Listing 2 Statische Bindung eines Methodenaufrufs Im Gegensatz zu Unit Tests wird beim Mutation Testing nicht die Korrektheit der Implementierung eines Programms getestet. Vielmehr wird geprüft, ob die Unit Tests die durch das Mutation Testing hervorgebrachten, fehlerhaften Änderungen am Verhalten eines Programms entdecken. Dies ist wichtig, da sich während der Entwicklung und Wartung unerwünscht Änderungen am Programm einschleichen können und von den Unit Tests entdeckt werden sollen. Eine grundlegende Basis an Unit Tests wird daher vorausgesetzt, wenn aus dem Mutation Testing Nutzen gezogen werden soll. Nun ist die Anzahl aller möglichen Änderungen eines Programms, welche zu einem fehlerhaften Verhalten führen können, potenziell unendlich groß, so dass sich auf eine Teilmenge von ihnen beschränkt werden muss. In [J+H] wurden Analysen und Ergebnisse verschiedener, englischsprachiger Arbeiten, die sich mit dem Thema Mutation Testing auseinandersetzen, zusammenfassend betrachtet. Darunter findet sich die "Mutation Coupling Effect Hypothesis" aus [Off92], die besagt, dass der Zusammenhang zwischen komplexen Mutanten und einfachen Mutanten darin besteht, dass eine aus Unit Tests bestehende Testbasis, die alle einfachen Mutanten erkennt, auch einen hohen Prozentsatz der komplexen Mutanten erkennen wird. Einfache Mutanten verursachen simple Fehler und zeichnen sich durch wenige syntaktische und semantische Änderungen am Programm aus. Die Erhöhung der Sichtbarkeit der Methode callMe(String) aus Listing 2 auf public resultiert in solch einen einfachen Mutanten dar. Dieser Zusammenhang zwischen einfachen und komplexen Mutanten wird beim Mutation Testing genutzt, um die Anzahl zu erzeugender Mutanten und somit auch die Laufzeitkosten zu reduzieren, indem lediglich wenige Änderungen am Programm vorgenommen werden. Die grundsätzliche Vorgehensweise beim Mutation Testing zeigt Algorithmus 1. Die Testbasis bestehend aus Unit Tests erkennt das fehlerhafte Verhalten der Mutanten dadurch, dass mindestens ein Test fehlschlägt, sobald die Testbasis auf den Mutanten ausgeführt wird. Dazu ist es nötig, dass das Programm vor Ausführung des Mutation Testing alle Tests erfüllt. Ansonsten kann es zu falschen Ergebnissen kommen. Welche Mutanten zu einem Programm gefunden werden und wie diese bestimmt werden ist abhängig von dem verwendeten Verfahren. Diese Arbeit stützt sich auf das constraintbasierte Verfahren zur Generierung von Mutanten (s. Abschnitt 2.4). 7 2 Grundlagen algorithm mutationTesting(programm, testbasis) precondition testbasis darf keine Tests enthalten, die fehlschlagen end precondition mutant := suche Mutant zum programm while mutant gefunden do if mutant nicht kompilierbar then ergebnis := mutant ungültig else führe testbasis auf mutant aus if testbasis hat fehlgeschlagene Tests then ergebnis := mutant getötet else ergebnis := mutant nicht getötet end if end if sammle ergebnis mutant := suche nächsten Mutant zum programm end while gib gesammelte Ergebnisse zurück end mutationTesting. Algorithmus 1 Mutation Testing mit Vorbedingungen, allgemein Die Auswertung eines Mutanten führt zu einem der folgenden Ergebnisse. Der Mutant wurde getötet Der Mutant wurde von der Testbasis durch einen fehlgeschlagenen Test entdeckt. Die Testabdeckung ist ausreichend, um das fehlerhafte Verhalten des Mutanten festzustellen. Der Mutant wurde nicht getötet Die Testbasis hat den Mutanten nicht entdeckt, da kein Test fehlgeschlagen ist. Es kann sein, dass das nach außen sichtbare Verhalten des Mutanten dem des ursprünglichen Programms gleicht. Der Mutant wäre dann äquivalent. Dies kann nur manuell durch den Entwickler festgestellt werden, indem er den Mutanten eingehend untersucht und diesen mit dem ursprünglichen Programm vergleicht. Der Mutant ist ungültig Der Mutant ist nicht kompilierbar, weil er die Sprachspezifikation der Programmiersprache nicht erfüllt. Mit dieser Art von Mutanten kann keine Aussage über die Testbasis getroffen werden. Ein wesentliches Problem beim Mutation Testing ist der zusätzliche Aufwand, den der Entwickler treiben muss, um aus den durch die Testbasis nicht entdeckten Mutanten die äquivalenten herauszufiltern. Dies ist mit ein Grund, warum sich das Mutation Testing bislang nicht in der Praxis durchsetzen konnte. Viele Verfahren, so auch das in Abschnitt 2.4 beschriebene constraint-basierte Verfahren, zielen darauf ab, die Anzahl der äquivalenten Mutanten und damit den manuellen Aufwand des Entwicklers beim Aufspüren solcher zu reduzieren. Da jeder gültige Mutant gegen die Testbasis ausge- 8 2 Grundlagen führt wird, reduzieren sich mit der Anzahl generierter, äquivalenter Mutanten auch die Laufzeitkosten. Diese können noch weiter reduziert werden, wenn ungültige Mutanten gar nicht erst erzeugt werden. Selbst äquivalente Mutanten, obwohl für das Mutation Testing selbst nicht relevant, sind in manchen Fällen für den Entwickler hilfreich wie das folgende Beispiel zeigt. Aus dem Programmcode in Listing 3 kann ein äquivalenter Mutant generiert werden, indem der Typ des Parameters value durch einen abstrakteren Typ (z. B. Iterable<Object>) ersetzt wird. Die Wiederverwendbarkeit des Programmcodes wird erhöht, wenn die durch den Mutanten hervorgebrachte Änderung für das Programm selbst übernommen wird, da dann Referenzen auf beliebige Objekte, die Iterable<Object> implementieren, als Werte an die Methode übergeben werden können. public class PrintStreamExtension { public static void println(PrintStream stream, List<Object> values) { for (Object o : values) { stream.println(o); } } } Listing 3 Programmcode als Basis für äquivalente Mutanten 2.3 Constraint-basierte Refaktorisierung Die Entwicklung von Software erfolgt über einen längeren Zeitraum. Während der Zeit ändern sich Anforderungen und neue kommen hinzu. Während die Code-Basis wächst, verschlechtert sich die Code-Qualität, da Methoden und Klassen durch Hinzufügen neuer Funktionen immer größer werden, was zu Lasten der Lesbarkeit und Wartbarkeit geht. Im Laufe der Zeit kann sich auch die Sicht auf die Anwendungsdomäne verändern, sodass die Struktur des Codes die Problemwelt stellenweise nicht mehr wiederspiegelt. Erweiterungen der Software werden dann umso aufwändiger. Um diesem Problem entgegenzuwirken, muss der Code entwicklungsbegleitend gepflegt, überarbeitet und umstrukturiert werden. Refaktorisierungen stellen Änderungen am Code dar, die unter Beibehaltung des Programmverhaltens zum Zwecke einer höheren Code-Qualität durchgeführt werden. Der Code wird dahingehend angepasst, dass er leichter zu lesen, zu warten und / oder zu erweitern ist. Die Durchführung von Refaktorisierungen ohne Unterstützung von Werkzeugen kann schnell dazu führen, dass das Programmverhalten unbeabsichtigt verändert wird. Im besten Fall fällt dies dadurch auf, dass das Programm nicht mehr kompilierbar ist. Ansonsten können, eine ausreichende Testabdeckung vorausgesetzt, fehlschlagende Unit Tests die Abweichung des Programmverhaltens aufdecken. Das selbst einfache Refaktorisierungen Fehler verursachen können zeigt Listing 4. Wird die Methode renameToPrint(String) umbenannt in print(String), bindet der Compiler den Methodenaufruf in main(String[]) nicht mehr an die Methode print(Object) sondern an die Methode print(String), da die übergebende Referenz auf ein Objekt vom Typ String zeigt, welcher mit dem des Methodenparameters übereinstimmt. 9 2 Grundlagen public class RenameMethod { private void print(Object obj) { System.out.println(obj); } private void renameToPrint(String str) { System.out.println("Boo"); } public static void main(String[] args) { new RenameMethod().print("Hallo Welt"); } } Listing 4 Umbenennung einer Methode Moderne Entwicklungsumgebungen wie Eclipse unterstützen mit Hilfe von Werkzeugen den Entwickler bei Refaktorisierungen. Dabei sollte sich der Entwickler darauf verlassen können, dass die von ihnen durchgeführten Refaktorisierungen das Programmverhalten nicht beeinflussen. Fehlerhafte Refaktorisierungen sollten verhindert werden. Leider können dies heutige Werkzeuge wie in [Ste10] gezeigt nicht in jeder Situation garantieren. Die Refaktorisierung führt dann entweder zu einem nicht kompilierbaren Programm oder verändert sein Verhalten, was im schlimmsten Fall nicht (sofort) entdeckt wird. Eine Lösung für dieses Problem verspricht die constraint-basierte Refaktorisierung. Ein Constraint stellt dabei eine logische Bedingung dar, die zu wahr oder falsch ausgewertet wird. Es enthält eine oder mehrere Constraint-Variablen, die mit beliebigen Werten aus ihren Wertebereichen belegt werden können. Gibt es für ein Constraint mindestens eine Belegung der Constraint-Variablen mit Werten, sodass es erfüllt ist, wird das Constraint als lösbar bezeichnet. Im Allgemeinen werden viele untereinander in Beziehung stehende Constraints gleichzeitig betrachtet und als ein Constraint-System aufgefasst. Dieses beschreibt dann ein logisches Problem, zu dem Lösungen gesucht werden. Ein Constraint-System ist genau dann lösbar, wenn die in ihm vorkommenden Constraint-Variablen so mit Werten belegt werden können, dass alle im Constraint-System enthaltenen Constraints lösbar sind. In Listing 5 ist ein einfaches Constraint-System dargestellt, das aus vier Constraints und drei Constraint-Variablen besteht. Die Menge accessibility sowie die Ordnung ihrer Elemente zueinander orientieren sich an den Sichtbarkeitsmodifikatoren in Java. Den Constraint-Variablen accessibility(x), accessibility(y) und accessibility(z) können beliebige Werte aus dem zugehörigen Wertebereich zugeordnet werden. Diesen Constraint-Variablen werden nun systematisch verschiedene Werte zugewiesen. Währenddessen wird geprüft, ob das Constraint-System mit der jeweiligen Wertebelegung erfüllt ist, also alle Constraints zu wahr ausgewertet werden. Trifft dies zu, wurde eine Lösung gefunden, die aus der jeweiligen Wertebelegung der Constraint-Variablen besteht. 10 2 Grundlagen public accessibility(x) accessibility(y) accessibility(z) = > ≥ > accessibility(x) accessibility(y) accessibility(z) private accessibility = { public, protected, package, private | public > protected > package > private } Listing 5 Constraint-System mit Wertebereich der Constraint-Variablen Bei der constraint-basierten Refaktorisierung werden die Beziehungen zwischen den Programmelementen, wie z. B. Methoden und Attribute, eines Programms auf Constraints, die von ConstraintRegeln erzeugt werden, abgebildet. Grundsätzlich lassen sich zwei Arten von Constraints unterschieden. Syntaktische und semantische Constraints Syntaktische und semantische Constraints stellen die Korrektheit eines Programms bezüglich der zugrundeliegenden Programmiersprache sicher, d. h. ihre Erfüllung gewährleistet, dass das Programm kompilierbar ist. Bindungserhaltene Constraints Die bindungserhaltenen Constraints sorgen dafür, dass das Programmverhalten beibehalten wird. Ein Umbinden an andere Programmelemente, wie z. B. an eine andere Methode, sei es statisch zur Übersetzungszeit oder dynamisch zur Laufzeit, wird unterbunden. Die Constraint-Regeln spiegeln die Regeln der Sprachspezifikation einer Programmiersprache wieder. Aus einem Programm kann unter Zuhilfenahme der Constraint-Regeln ein Constraint-System erzeugt werden. Jede Constraint-Variable stellt dabei eine Eigenschaft eines Programmelements dar. Eine gewünschte Änderung am Programm wird durch die Manipulation der Werte der Eigenschaften vorgenommen. Unter der Voraussetzung, dass die Constraint-Regeln vollständig und korrekt sind, ist ein Programm genau dann kompilierbar und verhaltensäquivalent zum ursprünglichen Programm, wenn die Wertebelegung der Constraint-Variablen eine Lösung für das aus ihm erzeugte Constraint-System darstellt. Dies wird durch die Constraints sichergestellt. Eine Refaktorisierung wird durchgeführt, indem Werte von Eigenschaften bestimmter Programmelemente verändert werden. Die Veränderung der Sichtbarkeit einer Klasse x erfordert, dass die Constraint-Variable accessibility(x) auf einen neuen Wert gesetzt wird. Das kann unter Umständen die Veränderung von Werten weiterer Eigenschaften anderer Programmelemente nach sich ziehen, falls die aktuelle Wertebelegung des Constraint-Systems keine Lösung darstellt. Dies zieht sich solange fort, bis entweder eine Lösung gefunden wurde, oder feststeht, dass keine Lösung erreicht werden kann. Im letzteren Fall wird die Refaktorisierung verweigert. 2 Grundlagen 11 2.4 Von der constraint-basierten Refaktorisierung zum Mutant Das im Abschnitt 2.3 betrachtete Verfahren der constraint-basierten Refaktorisierung kann als Basis für das Mutation Testing eingesetzt werden, um die Generierung von ungültigen und äquivalenten Mutanten möglichst zu vermeiden, was die Laufzeitkosten des Mutation Testing und den manuellen Aufwand zum Aussortieren nicht verwertbarer Mutanten erheblich reduziert. Ein Mutant im Sinne des Mutation Testing ist ein Programm, das durch absichtliche in der Regel automatisiert durchgeführte Änderungen an der Code-Basis eines anderen Programms aus diesem hervorgegangen ist, mit dem Ziel zu prüfen, ob diese Änderungen am Programm von einer Testbasis erkannt werden. Das Programm, aus dem Mutanten generiert werden, wird als ursprüngliches Programm bezeichnet. Von Interesse für das Mutation Testing sind solche Mutanten, die eine nach außen hin beobachtbare Verhaltensänderung aufweisen. Nur diese Art von Mutanten kann auch von einer Testbasis entdeckt werden, da automatisierte Tests auf der Überprüfung von Annahmen basieren, die sich auf den Zustand oder das Verhalten einer oder mehrerer Klassen beziehen. Es werden mehrere Arten von Mutanten unterschieden. Die Aufteilung orientiert sich dabei an [Bär10]. Gültige Mutanten Mutanten, die den syntaktischen und semantischen Regeln der zugrundeliegenden Programmiersprache entsprechen, sind korrekte Programme im Sinne der Sprachspezifikation und werden als gültig bezeichnet. Jeder gültige Mutant ist damit kompilierbar. 12 2 Grundlagen Äquivalente Mutanten Äquivalente Mutanten unterscheiden sich zwar syntaktisch und / oder semantisch vom ursprünglichen Programm, weisen aber ein von außen beobachtbares Verhalten auf, das dem des ursprünglichen Programms gleicht. Es wird davon ausgegangen, dass ein Vergleich des Programmverhaltens nur zwischen kompilierbaren und zumindest von einer Testbasis ausführbaren Programmen möglich ist. In diesem Sinne ist jeder äquivalente Mutant auch gültig. In Listing 6 wird ein Programm einem daraus generierten äquivalenten Mutanten gegenübergestellt. Die Sichtbarkeit der Methode log(String) wurde auf private reduziert. Dies hat keinerlei Auswirkungen auf die Bindung des Methodenaufrufs. Das Verhalten des Mutanten gleicht dem des ursprünglichen Programms. class Logger { class Logger { public void log(Object obj) { System.out.println(obj); } public void log(Object obj) { System.out.println(obj); } public void log(String str) { System.out.println(str); } private void log(String str) { System.out.println(str); } } } class Foo { class Foo { void doSomething() { Logger myLogger = new Logger(); myLogger.log(new Object()); } } void doSomething() { Logger myLogger = new Logger(); myLogger.log(new Object()); } } Listing 6 Ursprüngliches Programm (links) und äquivalenter Mutant (rechts) 13 2 Grundlagen Relevante Mutanten Relevante Mutanten weisen ein dem ursprünglichen Programm abweichendes Bindungsverhalten auf. Dies schließt sowohl die statische Bindung, die zur Übersetzungszeit erfolgt, als auch die dynamische Bindung während der Laufzeit ein. Das Umbinden eines Methodenaufrufs führt z. B. zu einem relevanten Mutanten. Eine Änderung der Bindung resultiert aber nicht zwangsläufig auch in einem anderen Verhalten. Ein relevanter Mutant kann ebenfalls verhaltensäquivalent zum ursprünglichen Programm und damit auch ein äquivalenter Mutant sein. In jedem Fall ist er kompilierbar und somit gültig. Listing 7 zeigt ein Programm mit einem daraus resultierenden relevanten Mutanten. In der Methode doSomething() wurde der ursprüngliche Typ Stack durch seinen Subtyp ReadonlyStack ersetzt. Dadurch wird der Aufruf von add(Object) zur Laufzeit an die im Subtyp überschriebene Methode gebunden, was zu einem anderen Programmverhalten führt. class Stack { class Stack { boolean add(Object value) { return false; } boolean add(Object value) { return false; } } } class ReadonlyStack extends Stack { class ReadonlyStack extends Stack { boolean add(Object value) { throw new RuntimeException(); } boolean add(Object value) { throw new RuntimeException(); } } } class Foo { class Foo { void doSomething() { Stack myStack = new Stack(); myStack.add(new Object()); } void doSomething() { Stack myStack = new ReadonlyStack(); myStack.add(new Object()); } } } Listing 7 Ursprüngliches Programm (links) und relevanter Mutant (rechts) Für das Mutation Testing ist nur die Teilmenge der relevanten Mutanten von Bedeutung, deren Verhalten sich von dem des ursprünglichen Programms unterscheidet. Ob sich ein relevanter Mutant tatsächlich anders verhält, muss vom Entwickler untersucht werden, denn ein Vergleich der von verschiedenen Programmen berechneten Funktionen ist nicht möglich [Wei11]. Die manuelle Untersuchung vieler potenziell äquivalenter Mutanten durch den Entwickler ist sehr zeitaufwendig. Die Anpassung der bei der constraint-basierten Refaktorisierung verwendeten Vorgehensweise für das Mutation Testing kann die Anzahl der generierten äquivalenten und ungültigen Mutanten erheblich reduzieren [S+T10]. Tatsächlich gibt es zwischen dem Problem nur gültige verhaltensbeibehaltene Refaktorisierungen zu erlauben und dem Problem möglichst nur relevante Mutation zu generieren 14 2 Grundlagen einige Gemeinsamkeiten. Beiden Problemen ist gemein, dass als Resultat ein kompilierbares Programm entstehen soll. Bei der constraint-basierten Refaktorisierung wird dies durch Erfüllung der im Constraint-System vorhandenen syntaktischen und semantischen Constraints, die aus den an der Sprachspezifikation der Programmiersprache angelehnten Constraint-Regeln erzeugt werden, sichergestellt. Die Übernahme der Constraint-Regeln für das Mutation Testing gewährleistet dann, dass nur noch gültige Mutation generiert werden. Die bindingserhaltenen Constraints sorgen dafür, dass das Programmverhalten durch Beibehalten statischer und dynamischer Bindung unverändert bleibt. Dies wird beim Mutation Testing ausgenutzt, indem ein bindungserhaltenes Constraint negiert wird, was in einem veränderten Bindungsverhalten des Programms resultiert. Das negierte Constraint ist genau dann erfüllt, wenn das ursprüngliche Constraint nicht erfüllt ist. Logisch gesehen wird ein Constraint durch Anwendung des Not-Operators negiert. Zur Generierung eines Mutanten wird ein bindungserhaltenes Constraint aus dem Constraint-System ausgewählt und negiert. Ein aus einem kompilierbaren Programm erzeugtes Constraint-System ist mit der initialen Wertebelegung der Constraint-Variablen lösbar. Die Negierung eines Constraints führt dazu, dass mindestens eine Constraint-Variable neu belegt werden muss, um eine Lösung zu erhalten. Dies führt zu einer kleinen syntaktischen und / oder semantischen Veränderung des Programms, einem einfachen Mutanten. Die Beibehaltung sämtlicher anderer Constraints gewährleistet, dass eine Lösung des Constraint-Systems zu einem gültigen Mutanten führt. Wie auch bei der constraint-basierten Refaktorisierung kann die Suche nach einer Lösung des Constraint-Systems Änderungen an den Werten weiterer Constraint-Variablen nach sich ziehen. Äquivalente Mutanten können mit diesem Verfahren zwar nicht vermieden werden, ein Umbinden von Aufrufen gewährleistet keine Änderung des nach außen sichtbaren Programmverhaltens, aber zumindest wird ihre Anzahl reduziert, da Mutanten mit unverändertem Bindungsverhalten nicht generiert werden. Es müssen nun nur noch alle bindungserhaltenen Constraints systematisch ausgewählt und negiert werden, um möglichst viele relevante Mutanten zu erhalten. 2.5 Refacola Die Refactoring Constraint Language (Refacola) wird an der Fernuniversität Hagen im Lehrgebiet Programmiersysteme entwickelt und ist eine domänenspezifische Sprache (Domain Specific Language, DSL), die es ermöglicht constraint-basierte Refaktorisierungen deklarativ zu spezifizieren. Dabei beschränkt sich Refacola nicht nur auf eine einzige Programmiersprache. Werkzeuge für Refaktorisierungen ermöglichen im Allgemeinen nur die Umstrukturierung von Code innerhalb einer Programmiersprache. Eines der Ziele von Refacola ist die Unterstützung von Refaktorisierungen über die Grenzen einer Programmiersprache und sogar über verschiedene Programmierparadigmen (objektorientiert, funktional, ...) hinaus. Durch die Verwendung des constraint-basierten Ansatzes werden fehlerhafte Refaktorisierungen, wie sie in bestimmten Situationen von den in Eclipse integrierten Werkzeugen durchgeführt werden, vermieden. Refacola stellt eine gute Basis für Mutation Testing dar, da viele Komponenten, die benötigt werden, bereits vorhanden sind und von den von ihr durchgeführten Refaktorisierungen verwendet werden. Der Aufbau des Constraint-Systems aus einem Programm und die Verwendung eines ConstraintSolvers, der mögliche Lösungen eines Constraint-Systems berechnet, zählen dazu. Wie in Abschnitt 2.4 erläutert können dieselben Constraint-Regeln, die für constraint-basierte Refaktorisierungen formuliert wurden, auch für das Mutation Testing herangezogen werden. Ebenso ist eine Komponente vorhanden, welche nach Auswahl einer Lösung die Änderungen am Programm zurückschreibt. 15 2 Grundlagen In Refacola gibt es zwei Bereiche, die für jede Programmiersprache definiert werden müssen. Dabei handelt es sich einerseits um die Sprachdefinition, welche die verschiedenen Programmelemente einer Programmiersprache beschreibt. Programmelemente sind Ausprägungen einzelner Typen. In objektorientierten Programmiersprachen stellen Klassen, Methoden oder auch Attribute Entitäten dar, die sich Typen zuordnen lassen. Jeder Typ verfügt über eine gewisse Anzahl von Eigenschaften, denen wiederum ein Wertebereich zugeordnet ist. Eine Klasse besitzt beispielsweise eine Eigenschaft, die ihren Namen repräsentiert und dessen Wertebereich die gültigen Bezeichner der jeweiligen Programmiersprache umfasst. Analog zur objektorientierten Denkweise stellt ein Typ einer Sprachdefinition genau eine Klasse innerhalb eines Programms dar, wobei die Typen im Gegensatz zu Klassen keinerlei Verhalten besitzen. Darüber hinaus können Abfragen als Prädikate definiert werden, aus denen Fakten, welche u. a. die Beziehungen zwischen Programmelementen darstellen können, abgeleitet werden. Beispielsweise existiert in der Sprachdefinition für Java eine Abfrage, die auswertet, ob ein Programmelement an ein anderes Programmelement gebunden ist. Listing 8 zeigt einen kleinen Ausschnitt der Refacola Sprachdefinition für Java. Wie ansatzweise zu sehen ist, werden die Programmelemente von Java in Refacola hierarchisch definiert. Dadurch können einerseits die Typen als Filter in Constraint-Regeln verwendet werden, was der Polymorphie in objektorientierten Programmiersprachen gleicht, andererseits soll möglichst die Terminologie aus der Java Sprachspezifikation übernommen werden. Ein Programmelement, das eine Klasse repräsentiert, ist damit (auch) vom Typ AccessibleEntity, da es ebenfalls über die Eigenschaft accessibility verfügt, welches die Sichtbarkeit der Klasse darstellt. language Java kinds abstract abstract abstract abstract Entity <: ENTITY AccessibleEntity <: Entity { accessibility } StaticMember <: Member TypedStaticMember <: StaticMember, TypedMember properties accessibility "\\alpha" : AccessModifier domains AccessModifier = {private, package, protected, public} queries ltPublic(A : AccessibleEntity) Listing 8 Ausschnitt aus der Refacola Sprachdefinition für Java Eine andere Sicht auf den Ausschnitt der Sprachdefinition in Listing 8 stellt Abbildung 1 dar. Hier ist gut die Analogie zwischen den Typen in der Sprachdefinition und den Klassen in objektorientierten Programmen zu sehen. Zu beachten ist, dass entgegen den Möglichkeiten, die Java bietet, Mehrfachvererbung bei der Definition von Typen möglich ist. Dieser Umstand wird bei der Generierung von Java-Quelltext aus der Refacola Sprachdefinition insofern berücksichtigt, als dass jeder Typ auf ein Interface abgebildet wird, das ggf. mehrere Interfaces erweitert (Schnittstellenvererbung). 16 2 Grundlagen Entity AccessibleEntity Member StaticMember TypedMember +accessibility: AccessModifier <<enumeration>> AccessModifier TypedStaticMember +private +package +protected +public Abbildung 1 Alternative Darstellung des Ausschnitts aus der Refacola Sprachdefinition für Java Der andere in Refacola zu definierende Bereich für eine Programmiersprache enthält die ConstraintRegeln. Sie basieren auf der ebenfalls in Refacola definierten Sprachdefinition der jeweiligen Programmiersprache und werden verwendet, um das Constraint-System eines Programms zu erzeugen. Jede Constraint-Regel besitzt einen eindeutigen Namen, der vom Refacola Entwickler frei vergeben werden kann, und teilt sich in zwei Abschnitte, dem Deklarationsteil und dem Regelrumpf. Im ersten Abschnitt werden Variablen deklariert, die innerhalb der Constraint-Regel verwendet werden. Diese sind typisiert, wobei ihr Typ aus der Refacola Sprachdefinition stammen muss, wie z. B. AccessibleEntity aus Listing 8. Der Regelrumpf unterteil sich wiederum in einen Bedingungsteil und einen Aktionsteil. Im Bedingungsteil können beliebige Abfragen aus der Sprachdefinition durch Kommata getrennt verwendet werden. Sie werden automatisch mittels logischem und-Operator zu einer Bedingung verknüpft. Vom Typ passende Programmelemente werden wie bei logischen Programmiersprachen an die Variablen gebunden. Erfüllen die Programmelemente die Bedingung, werden die im Aktionsteil definierten Constraints für die in den Variablen gebundenen Programmelemente erzeugt. In Listing 9 ist eine Constraint-Regel angegeben, welche die Forderung, dass in Interfaces deklarierte Programmelemente die Sichtbarkeit public besitzen müssen, deklarativ formuliert. Falls I an ein Programmelement vom Typ Interface und M an ein Programmelement vom Typ Member gebunden wird und M in I deklariert ist, dies entspricht der Abfrage Java.member(), dann wird für das an M gebundene Programmelement ein Constraint erzeugt, welches genau dann erfüllt ist, wenn M die Sichtbarkeit public besitzt. 17 2 Grundlagen OOPSLA_f0_interfaceMemberAccessibility for all I: Java.Interface M: Java.Member do if Java.member(I, M) then M.accessibility = # public end Listing 9 Constraint-Regel für die Sichtbarkeit von in Interfaces deklarierten Programmelementen Listing 10 zeigt ein Code-Beispiel mit Programmelementen, auf die die Constraint-Regel zutrifft. Das Interface MyInterface stellt ein Programmelement dar, das an die Variable I gebunden wird. Ebenso wird die Methode myMethod() an M gebunden. Da myMethod() in MyInterface deklariert und somit ein Mitglied von MyInterface ist, ist die Bedingung der Constraint-Regel erfüllt und das im Aktionsteil definierte Constraint wird erzeugt. myMethod() ist an M gebunden, was letztendlich in einem Constraint resultiert, das genau dann erfüllt ist, wenn myMethod() die Sichtbarkeit public besitzt. Die in Listing 9 gezeigte Constraint-Regel erzeugt semantische Constraints, deren Nichterfüllung ein nicht kompilierbares Programme zur Folge hätte. public interface MyInterface /* I */ { void myMethod(); // M } Listing 10 Beispiel zur Constraint-Regel Neben den beiden betrachteten Bereichen, der Sprachdefinition und den Constraint-Regeln, gibt es noch einen weiteren, in dem Refaktorisierungen definiert werden. Eine Refaktorisierung kann in Refacola als Änderung der Werte von Eigenschaften ausgewählter Programmelemente betrachtet werden. Die Refaktorisierung "Methode umbenennen" würde der identifier Eigenschaft des Programmelements, das die ausgewählte Methode repräsentiert, den vom Entwickler gewünschten Namen zuweisen. Ferner kann definiert werden, inwieweit Eigenschaften weiterer Programmelemente im Zuge der Suche nach einer Lösung des Constraint-Systems neue Werte zugewiesen werden dürfen. Spezifizierungen von Refaktorisierungen in Refacola werden in dieser Arbeit nicht weiter betrachtet, da sie für das Mutation Testing nicht verwendet werden. 18 2 Grundlagen 3 Implementierung 19 3 Implementierung Refacola soll im Rahmen dieser Arbeit um eine Komponente zur Durchführung von Mutation Testing erweitert werden. Wie auch Refacola selbst, wird das Mutation Testing Framework, wie die Komponente nachfolgend genannt wird, als Plugin für die Entwicklungsumgebung Eclipse realisiert. Im Vorfeld sind die Anforderungen zu spezifizieren, welche von dem Framework erfüllt werden sollen. Diese werden in Abschnitt 3.1 genauer betrachtet. Während der Entwicklung hat sich herausgestellt, dass eine Aufteilung des Mutation Testing Frameworks in mehrere Eclipse-Plugins eine bessere Wiederverwendung und eine saubere Trennung zwischen den verschiedenen Komponenten ermöglicht. Gleichzeitig orientiert sich die Aufteilung an die Strukturierung der Eclipse-Plugins, in die Refacola unterteilt ist. Eine Übersicht über die verschiedenen Eclipse-Plugins des Frameworks findet sich am Anfang von Abschnitt 3.2. Auf den Anwendungskern wird in Abschnitt 3.2.1 näher eingegangen. Es werden die einzelnen Schritte zur Vollziehung eines Mutation Testing Durchlaufs im Detail erklärt. Die Brücke zu Refacola wird hergestellt, indem aufgezeigt wird, welche bereits dort vorhandenen Klassen und Mechanismen verwendet werden. Die Ausführung einer Testbasis stellt einen wichtigen Schritt beim Mutation Testing dar. Andererseits ist diese Funktionalität allgemein genug, dass sie ebenfalls in einem anderen Kontext verwendet werden kann. Sie wurde daher in einem eigenen Eclipse-Plugin, dem JUnit Test Runner, ausgelagert. Die Interna des JUnit Test Runner sind in Abschnitt 3.2.2 beschrieben. Ein weiterer wichtiger Teil des Mutation Testing Frameworks stellt die Benutzungsoberfläche dar, die sich als Eclipse-View möglichst harmonisch in Eclipse integrieren soll. Als eine von vielen Views in Eclipse soll sie den Entwickler bei der Arbeit unterstützen. Es wurde auf modale Dialoge zugunsten einer angenehmen Benutzbarkeit verzichtet. Abschnitt 3.2.3 behandelt den Aufbau und die Implementierung der Benutzungsoberfläche. Das Mutation Testing Framework soll dem Refacola-Entwickler die Möglichkeit geben, die in Refacola bereits definierten und zukünftig noch hinzukommenden Constraint-Regeln für das Mutation Testing auszuzeichnen. Dabei wurde Wert darauf gelegt, dass Auszeichnungen so einfach wie möglich durchzuführen sind ohne auf Flexibilität verzichten zu müssen. Die Auszeichnung der ConstraintRegeln und was dabei zu beachten ist, wird in Abschnitt 3.3 näher untersucht. Nachdem ein Überblick über die Funktionalität des Mutation Testing Frameworks gegeben wurde, wird anhand eines kleinen Beispiels in Abschnitt 3.4 die Anwendung demonstriert. Das ursprüngliche Java-Programm, das verwendet wird, wird einem Mutanten gegenübergestellt. Die Implementierung des Mutation Testing Frameworks unterliegt einigen Einschränkungen. Obwohl Refacola nicht auf eine konkrete Programmiersprache festgelegt ist, kann das Framework in ihrer jetzigen Form nur auf in Java geschriebene Programme angewendet werden. Weiterhin zieht sie keinen Vorteil aus Mehrkernprozessoren, da das Mutation Testing sequenziell durchgeführt wird. Gründe dafür und Ansätze, um den Ablauf zu parallelisieren, werden in Abschnitt 3.5 betrachtet. 20 3 Implementierung 3.1 Anforderungen Wie bereits am Anfang von Kapitel 3 erläutert, soll für Refacola ein Framework entwickelt werden, mit dessen Hilfe Mutation Testing durchgeführt werden kann. Dazu soll sich das Framework soweit wie möglich auf bereits in Refacola vorhandene Funktionen stützen. Refacola selbst ist nicht an eine konkrete Programmiersprache gebunden. Ziel des Mutation Testing Frameworks im Rahmen dieser Arbeit ist die Unterstützung von Java-Programmen. Das Mutation Testing Framework soll dem Refacola-Entwickler die Möglichkeit geben, die in Refacola bereits definierten und zukünftig noch zu definierenden Constraint-Regeln deklarativ auszuzeichnen. Dadurch werden indirekt die von den ausgezeichneten Constraint-Regeln erzeugten Constraints bestimmt, die im Laufe eines Mutation Testing Durchlaufs negiert werden. Constraints werden unabhängig voneinander negiert, d. h. zu jedem Zeitpunkt während eines Durchlaufs ist innerhalb des aus einem Programm erzeugten Constraint-Systems nur ein Constraint in seiner negierten Form vorhanden. Dies bedeutet, dass aus dem ursprünglichen Constraint-System ein Constraint ausgewählt und durch seine negierte Form ersetzt wird. Bevor das nächste Constraint negiert wird, werden die Änderungen am Constraint-System wieder rückgängig gemacht. Dieses Verfahren wird in Abschnitt 3.2.1 noch ausführlicher betrachtet. Anforderungen, die an das Mutation Testing Framework zu stellen sind, werden nachfolgend betrachtet. Generischer Ansatz der Constraint-Negierung Da beliebige in Refacola definierte Constraint-Regeln als negierbar ausgezeichnet werden sollen, insbesondere auch solche, die zukünftig noch definiert werden, wird ein generischer Ansatz zur Negierung der Constraints benötigt. Basis des Ansatzes sind Untersuchungen, die im Zuge der Mutantengenerierung mit Type Constraints durchgeführt wurden (s. [Bär10]). Dabei können keine Annahmen bezüglich der Constraint-Regeln gemacht werden. Es muss davon ausgegangen werden, dass beliebige, sogar alle vorhandenen Constraint-Regeln ausgezeichnet werden. Auch sind die zu manipulierenden Eigenschaften von Programmelementen nicht weiter bekannt. Diese können von dem Namen eines Programmelements bis zu seiner Sichtbarkeit reichen. Einfache Auszeichnung zu negierender Constraint-Regeln Refacola befindet sich zum Zeitpunkt der Entstehung des Mutation Testing Frameworks in der Entwicklung. Die vorhandenen Constraint-Regeln decken noch nicht die gesamte Sprachspezifikation von Java ab. Es ist daher davon auszugehen, dass noch Constraint-Regeln hinzugefügt oder geändert werden. Auch ist nicht sicher, ob die vorhandenen Constraint-Regeln korrekt sind. Die Implementierung des Mutation Testing Frameworks sollte diese Umstände berücksichtigen, indem die Auszeichnung von Constraint-Regeln möglichst einfach und ohne Eingriff in den Quellcode möglich ist. Refacola bietet mit ihrer domänenspezifischen Sprache (domain specific language, DSL) in Verbindung mit einem Compiler bereits die Möglichkeit Java-Quellcode zu generieren. Auf diese Weise können Sprachdefinitionen, Constraint-Regeln und Refaktorisierungen deklarativ in der DSL formuliert werden. Die dort definierten Elemente können ähnlich wie Programmelemente in Java (Klassen, Methoden, ...) mit Annotationen ausgezeichnet werden. Dieser Ansatz scheint für die Auszeichnung von Constraint-Regeln sehr vielversprechend zu sein. 3 Implementierung 21 Automatisierte Durchführung des Mutation Testing Die Akzeptanz von Testmethoden hängt nicht zuletzt von ihrer einfachen und schnellen Durchführung ab. Unit-Tests werden umso häufiger ausgeführt, je einfacher sie gestartet werden können. Eine manuell zu konfigurierende Testumgebung hat immer das Potenzial zusätzliche Fehler zu erzeugen und erhöht den Aufwand, den ein Entwickler treiben muss, um einen Testdurchlauf anzustoßen. Aus diesen Gründen soll das Mutation Testing möglichst automatisiert durchgeführt werden. Eine Minimalkonfiguration ist allerdings nötig, da das zu testende Programm sowie die zu verwendende Testbasis als Informationen benötigt werden. Ein Mutation Testing Durchlauf kann danach vollautomatisiert unter der Voraussetzung erfolgen, dass die in der Testbasis enthaltenen Tests nicht manuell konfiguriert werden müssen. Es wird davon ausgegangen, dass die Testbasis lediglich aus Unit-Tests besteht, die per Definition keiner manuellen Konfiguration bedürfen. Übersichtliche Darstellung der Ergebnisse des Mutation Testing Die Ergebnisse des Mutation Testing sollen dem Entwickler Aufschluss über die generierten Mutanten und deren Erkennung durch die Testbasis geben. Da das Mutation Testing Framework als Plugin in Eclipse umgesetzt wird, bietet sich eine Eclipse-View zur Darstellung der Ergebnisse an. Eine gute Integration erfordert auch, dass sich eine Eclipse-View nicht in den Vordergrund drängt. Auf modale Dialog wird daher bei der Implementierung der Eclipse-View verzichtet. Es sollte sofort ersichtlich sein, welcher Mutant zu welchem Ergebnis geführt hat. Da das Interesse an den nicht getöteten Mutanten am größten ist, schließlich könnten sie Hinweise zur Verbesserung der Testabdeckung geben, sollte der Fokus auf diese Mutanten gelegt werden. Nicht getötete Mutanten sind besonders zu kennzeichnen. Vergleich von Änderungen an der Codebasis zwischen Mutant und ursprünglichem Programm Ob ein nicht getöteter Mutant verhaltensgleich zum ursprünglichen Programm ist, muss der Entwickler selbst untersuchen. Er kann dabei unterstützt werden, indem Änderungen im Quellcode auf Basis des ursprünglichen Programms hervorgehoben werden und die betroffenen Stellen in einem Eclipse-Editor angezeigt werden. Sollte sich herausstellen, dass der Mutant ein anderes Verhalten aufzeigt, wird der Entwickler den Wunsch haben, zur Erhöhung der Testabdeckung weitere UnitTests auszuarbeiten. Dabei sollte er die Möglichkeit haben, beliebig zwischen Mutanten und ursprünglichem Programm hin- und herzuwechseln, um die syntaktischen und / oder semantischen Unterschiede zu untersuchen. 3.2 Umsetzung Die Implementierung des Mutation Testing Frameworks für Refacola erfolgt in Form von Plugins für die Entwicklungsumgebung Eclipse. Dabei wird das Framework selbst in drei Plugins aufgeteilt, dem Anwendungskern, dem JUnit Test Runner und der Benutzungsoberfläche, was einer sauberen Trennung und besseren Wiederverwendung zugutekommt. Die Aufteilung von Anwendungskern und Benutzungsoberfläche in zwei Plugins sowie deren Namensgebung orientieren sich an den EclipsePlugins, in die Refacola aufgeteilt ist. Abbildung 2 stellt die Unterteilung des Mutation Testing Frameworks in die drei genannten Plugins dar. 22 3 Implementierung Abbildung 2 Aufteilung des Mutation Testing Frameworks auf Eclipse-Plugins Bevor in den nächsten Abschnitten, die sich an der Aufteilung der Eclipse-Plugins orientieren, detaillierter auf die einzelnen Bestandteile des Mutation Testing Frameworks eingegangen wird, folgt eine kurze Übersicht über die Aufgabe jedes Plugins. Anwendungskern (de.feu.ps.refacola.mutation) Der Anwendungskern enthält die gesamte innere Logik des Mutation Testing Frameworks und generiert für beliebige in Java geschriebene Programme Mutanten auf Basis der in Refacola ausgezeichneten Constraint-Regeln. Er arbeitet sowohl mit Refacola als auch mit dem JUnit Test Runner zusammen, wobei letzterer zur Ausführung der Testbasis verwendet wird. Die Aufgabe des Anwendungskerns ist die Generierung von Mutanten und die Auswertung der Testergebnisse, die er vom JUnit Test Runner für jeden Mutanten erhält. Als Ergebnis übergibt er die Resultate eines Mutation Testing Durchlaufs zusammen mit den Mutationen, mit dessen Hilfe ein Programm in einen durch die Mutation definierten Mutanten überführt werden kann, an den aufrufenden Code zurück. Der Anwendungskern wird in Abschnitt 3.2.1 detailliert betrachtet. JUnit Test Runner (de.feu.ps.refacola.mutation.junitrunner) Der JUnit Test Runner ist für die Ausführung von JUnit-Tests eines ihm übergebenen JavaProgramms verantwortlich. Er sucht die von ihm ausführbaren Testfälle im Java-Programm und lässt sie in einer eigenen Java Virtual Machine durch das JUnit-Framework ausführen. Das Ergebnis des Testdurchlaufs wird dann an den aufrufenden Code weitergeleitet. Abschnitt 3.2.2 widmet sich dem JUnit Test Runner. Benutzungsoberfläche (de.feu.ps.refacola.mutation.ui) Die Benutzungsoberfläche stellt den nach außen sichtbaren Teil des Mutation Testing Frameworks als Eclipse-View dar. Sie gibt dem Entwickler die Möglichkeit Programm und Testbasis für das Mutation Testing auszuwählen und stellt vor der Ausführung sicher, dass die Vorbedingungen erfüllt sind. Ein Mutation Testing Durchlauf kann für größere Programme eine längere Zeit in Anspruch nehmen. Die Mutation Testing View teilt dem Entwickler daher den Fortschritt kontinuierlich mit. Am Ende eines Mutation Testing Durchlaufs werden die Ergebnisse in der Eclipse-View dargestellt und Mutanten können vom Entwickler in einem Eclipse-Editor aufgerufen werden. Abschnitt 3.2.3 geht näher auf die Benutzungsoberfläche ein. 23 3 Implementierung 3.2.1 Anwendungskern Der Anwendungskern umfasst die gesamte Funktionalität zur Durchführung von Mutation Testing mit Refacola abgesehen von der Ausführung von JUnit Tests. Da deren Ausführung nicht nur auf das Mutation Testing begrenzt sein muss und ggf. für andere Refacola-Komponenten zukünftig relevant sein kann, wurde diese Funktionalität in einem separaten Eclipse-Plugin, dem JUnit Test Runner, ausgelagert (s. Abschnitt 3.2.2). Zur Erfüllung seiner Aufgabe nutzt der Anwendungskern verschiedene Komponenten von Refacola. algorithm mutationTesting(javaProgramm, testbasis) precondition javaProgramm darf keine ungespeicherten Änderungen beinhalten javaProgramm muss kompilierbar sein testbasis darf keine ungespeicherten Änderungen beinhalten testbasis muss kompilierbar sein testbasis darf keine Tests enthalten, die fehlschlagen end precondition kompiliere javaProgramm und testbasis erzeuge Java-Faktenbasis für javaProgramm initialisiere Kontext der Refaktorisierung generiere Mutationen foreach Mutation suche Lösungen zur Mutation if Lösung vorhanden then ändere Programm zu Mutant if Mutant kompilierbar then führe testbasis auf Mutant aus end if ändere Mutant zu Programm sammle Mutation und Ergebnis des Testdurchlaufs end if end foreach gib gesammelte Mutationen und Ergebnisse zurück end mutationTesting. Algorithmus 2 Mutation Testing mit Vorbedingungen Der Ablauf eines Mutation Testing Durchlaufs ist in Algorithmus 2 dargestellt. Auf einzelne Schritte wird in den folgenden Abschnitten näher eingegangen. Zentraler Punkt des Anwendungskerns ist die Klasse MutationService, welche den Algorithmus implementiert, und eine einfache Schnittstelle für Programme, die den Anwendungskern verwenden möchten, zur Verfügung stellt. Während der Ausführung eines Mutation Testing Durchlaufs werden Informationen über den Fortschritt an den aufrufenden Code weitergeleitet. Java stellt dazu das Interface IProgressMonitor zur Verfügung, über welches der Fortschritt in Form von Arbeitseinheiten angegeben werden kann. Mittels dieses Interfaces kann der aufrufende Code ebenfalls den Abbruch einer Operation anstoßen. Es liegt in der Verantwortung der aufgerufenen Methode das Abbruch-Flag des IProgressMonitors auszuwerten und entsprechend darauf zu reagieren. Im Falle des Anwendungskerns des Mutation Tes- 24 3 Implementierung ting Frameworks wird in kurzen, regelmäßigen Abständen das Abbruch-Flag geprüft, um möglichst schnell auf die Anforderung eines Abbruchs reagieren zu können. Es wird dann gemäß JavaKonvention eine OperationCanceledException geworfen. Diese Technik wird von der Benutzungsoberfläche des Mutation Testing Frameworks genutzt, um dem Entwickler eine Rückmeldung über den Fortschritt des Mutation Testing zu geben. Gleichzeitig erhält er damit die Möglichkeit einen Mutation Testing Durchlauf abzubrechen. Leider schleicht sich damit zusätzliche Funktionalität in den Anwendungskern ein, was die Lesbarkeit des Codes mindert. Trotz dieses Nachteils überwiegen die Vorteile, weshalb sich für eine Umsetzung entschieden wurde. 3.2.1.1 Vorbedingungen Die ordnungsgemäße Ausführung des Mutation Testing erfordert die Einhaltung verschiedener Vorbedingungen (s. Algorithmus 2). Sie werden nicht vom MutationService geprüft, die Schnittstelle stellt aber Methoden zur Verfügung, damit der aufrufende Code diese vor Ausführung des Mutation Testing selbst prüfen kann. Die Einhaltung der Vorbedingungen, welche kurz erläutert werden, werden durch die Benutzungsoberfläche des Mutation Testing Frameworks sichergestellt. Das Java-Programm darf keine ungespeicherten Änderungen beinhalten Im Laufe des Mutation Testing wird das Java-Programm mehrfach geändert und kompiliert. Damit transiente Änderungen am Programm während des Mutation Testing nicht verlorengehen, sollten diese vorher entweder gespeichert oder verworfen werden. Das Java-Programm muss kompilierbar sein Das Java-Programm darf keine syntaktischen und semantischen Fehler im Sinne der Java Sprachspezifikation aufweisen. Da während des Mutation Testing nur die bindungserhaltenen Constraints manipuliert werden, welche nicht dafür Sorge tragen, dass das Programm kompilierbar bliebt, würde aus einem nicht kompilierbaren Programm nur nicht kompilierbare und damit ungültige Mutanten generiert werden. Damit würde das Ziel des Mutation Testing, nämlich das Finden von nicht getöteten, aber gültigen Mutanten verfehlt. Eine Ausführung unter diesen Bedingungen wäre somit überflüssig. Die Testbasis darf keine ungespeicherten Änderungen beinhalten Wie beim Java-Programm soll diese Vorbedingung sicherstellen, dass nicht gespeicherte Änderungen an der Testbasis nicht verlorengehen. Die Testbasis muss kompilierbar sein Entsprechend dem Java-Programm, aus dem Mutanten generiert werden sollen, muss das JavaProgramm, welches als Testbasis fungiert, kompilierbar sein. Andernfalls kann die Testbasis nicht verwendet werden, um festzustellen, welche Mutanten getötet und welche nicht getötet werden. Die Testbasis darf keine Tests enthalten, die fehlschlagen Testbasen, die fehlschlagende Tests enthalten, würden beim Mutation Testing dazu führen, dass Mutanten als getötet erkannt werden, obwohl der Grund für die fehlgeschlagenen Tests nicht unbedingt auf die Änderungen am ursprünglichen Programm zurückzuführen ist. Zusätzlich könnte es durchaus passieren, wenn auch nur in seltenen Fällen, dass ein Mutant im Gegensatz zum ursprüng- 3 Implementierung 25 lichen Programm alle Tests erfolgreich durchläuft. Um eine verlässliche Ausgangssituation zu haben, darf kein Test fehlschlagen. Eine Testbasis, die keine JUnit Tests beinhaltet, erfüllt diese Vorbedingung automatisch. Damit verhält sich das Mutation Testing Framework analog zu JUnit. 3.2.1.2 Erzeugung der Java-Faktenbasis Zu Beginn eines Mutation Testing Durchlaufs wird die Faktenbasis des Java-Programms durch Refacola über einen IProgramInfoProvider erzeugt. Die Faktenbasis leitet sich aus dem abstrakten Syntaxbaum (Abstract Syntax Tree, AST) ab, der von Eclipse aus einem Java-Programm erstellt wird. Sie hält Informationen über die Programmelemente sowie deren Beziehungen untereinander und wird benötigt, um das RefactoringProblem zu initialisieren. Im späteren Verlauf des Mutation Testing Durchlaufs wird das Objekt, welches die Fakten erzeugt, noch benötigt, um aus den von Refacola ermittelten Änderungen Change-Objekte aufzubauen, mit dessen Hilfe Eclipse Änderungen am JavaProgramm vornehmen kann. 3.2.1.3 Initialisierung des Kontextes einer Refaktorisierung Die Klasse RefactoringProblem stellt den Kontext einer Refaktorisierung dar. Sie verwaltet einerseits alle beteiligten Programmelemente und Fakten. Diese Informationen werden über einen IProgramInfoProvider bereitgestellt, der schon im vorherigen Schritt für die Erzeugung der Faktenbasis zuständig war. Sowohl die von ihm ermittelten Programmelemente als auch die Fakten werden unverändert zur Initialisierung des RefactoringProblems verwendet. Für die Suche nach einer Lösung eines Constraint-Systems ist ein Constraint-Solver zuständig. Zur Zeit wird in Refacola nur der Choco genannte Constraint-Solver unterstützt, weshalb er auch beim Mutation Testing standardmäßig zum Einsatz kommt. Dem RefactoringProblem wird eine ISolverFactory injiziert, damit es Zugriff auf den zur Lösung eines Constraint-System zu verwendenden ISolver hat. Andererseits muss spezifiziert werden, welche Eigenschaften von Programmelementen sich im Laufe der Suche nach einer Lösung ändern dürfen oder sogar müssen. Soll beispielsweise ein Programmelement umbenannt werden, muss seiner Identifier-Eigenschaft ein neuer Wert zugeweisen werden, welcher dem neuen Namen des Programmelements entspricht. Bis zu diesem Punkt wird das RefactoringProblem im Mutation Testing Framework genauso verwendet wie es auch von den in Refacola spezifizierten Refaktorisierungen benutzt wird. Im Gegensatz zu den Refaktorisierungen werden beim Mutation Testing keine bestimmten Werte für ausgewählte Eigenschaften einiger Programmelementen gefordert. Die einzige Bedingung ist, dass sich das Verhalten des Programms ändern soll, was implizit durch die Negierung eines bindungserhaltenen Constraints realisiert wird. Nun müssen aber Änderungen von Eigenschaften der Programmelemente erlaubt werden, sonst ist eine Lösung des Constraint-Systems nicht möglich. Der Grund besteht darin, dass das ursprüngliche Constraint-System lösbar ist. Es wurde aus einem syntaktisch und semantisch korrekten Programm erzeugt. Die Vorbedingungen des Mutation Testing stellen das sicher. Wird jetzt ein Constraint negiert, kann dieses mit der ursprünglichen Variablenbelegung nicht mehr erfüllt werden. Im Gegensatz zu Refaktorisierungen ist die konkrete Belegung der Werte von Eigenschaften der Programmelemente nicht von Bedeutung. Ob bei einem Mutanten eine Methode umbenannt wurde, um seine syntaktische und semantische Korrektheit sicherzustellen oder nicht, ist nicht relevant. Es geht schließlich nur darum ein Programm mit abweichenden Verhalten zu erhalten. Des- 26 3 Implementierung halb wird auch die Änderung beliebiger Eigenschaften von Programmelementen akzeptiert. Dies wird dem über das RefactoringProblem erreichbaren ICodomainProvider mitgeteilt. Der ICodomainProvider verwaltet alle Informationen über zu ändernde Eigenschaften von Programmelementen. Dass deren scheinbar willkürliche Belegung von Werten immer zu kompilierbaren Programmen führt, wird durch die syntaktischen und semantischen Constraints sichergestellt. 3.2.1.4 Generierung von Mutationen Das Constraint-System wird nun einmalig für den Durchlauf des Mutation Testing aufgebaut. Dazu werden erst einmal die Eigenschaften aller im RefactoringProblem verwalteten Programmelemente gesammelt. Danach werden auf den gesammelten Eigenschaften der Programmelemente die in Refacola definierten Constraint-Regeln angewendet, welche unter Verwendung des RefactoringProblems Constraints erzeugen. Die Menge der erzeugten Constraints ergeben das Constraint-System des Programms. Zum Zeitpunkt der Entwicklung des Mutation Testing Frameworks standen zwei Implementierungen zur Erstellung von Constraint-Systemen in Refacola zur Auswahl. Während der CompleteConstraintSetGenerator alle Constraints erzeugt, generiert der MinimizedConstraintSetGenerator ausgehend von einem RefactoringProblem nur so viele Constraints, wie zur Lösung benötigt werden. Außerdem kann der MinimizedConstraintSetGenerator die Constraint-Generierung vorzeitig abbrechen, wenn er feststellt, dass das Constraint-System nicht lösbar ist. Da ein generischer Ansatz für das Mutation Testing verwendet wird und daher das RefactoringProblem nicht auf einige ausgewählte Programmelemente eingegrenzt wird, kommt nur der CompleteConstraintSetGenerator in Frage. Leider konnte zur Generierung des Constraint-Systems keine der bereits in Refacola vorhandenen Implementierungen verwendet werden, da nur während des Aufbaus des Constraint-Systems ein Zusammenhang zwischen Constraint-Regeln und den daraus generierten Constraints hergestellt werden kann. Ein Eingriff in den in Refacola implementierten Algorithmus ist von außen nicht möglich. Aus diesem Grund fand eine Reimplementierung des Algorithmus zur Erzeugung eines Constraint-Systems innerhalb des Mutation Testing Frameworks statt. Durch die Reimplementierung wurde es erst möglich einen IProgressMonitor während der Erzeugung des Constraint-Systems zu verwenden. Wie bereits am Anfang von Abschnitt 3.2.1 beschrieben, wird er zur Fortschrittsanzeige innerhalb der Benutzungsoberfläche des Mutation Testing Frameworks verwendet. Informationen über die aktuellen Arbeitsschritte der Constraint-System-Erzeugung können so dem Entwickler präsentiert werden. Er hat darüber hinaus die Möglichkeit das Mutation Testing abzubrechen. Der Zusammenhang zwischen Constraint-Regeln und Constraints ist notwendig, da die Identifizierung der zu negierenden Constraints über ihre Constraint-Regeln erfolgt. Diese sind, falls sie in Refacola entsprechend ausgezeichnet wurden (s. Abschnitt 3.3), mit einer Negatable-Annotation versehen. Trifft dies für eine Constraint-Regel zu, wird jedes von ihr erzeugte Constraint für die spätere Negierung sowie die Constraint-Regel, aus der es hervorging, gesammelt. Nachdem alle Constraints erzeugt wurden, werden für alle gesammelten Constraints ihre negierten Gegenstücke erstellt. Dazu wird eine von Refacola bereitgestellte statische Fabrikmethode verwendet, welche aus einem übergebenen 27 3 Implementierung Constraint ein ComposedConstraint erstellt, das genau dann erfüllt ist, wenn das ursprüngliche Constraint nicht erfüllt ist und umgekehrt. Zum jetzigen Zeitpunkt existiert einerseits das Constraint-System. Andererseits wurden die Constraints gesammelt, die negiert werden sollen. Zu diesen Constraints wurden die negierten Constraints bereits erstellt. Die vorhandenen Objekte müssen nun so verknüpft werden, dass im nachfolgenden Schritt eine Menge von Constraint-Systemen zur Verfügung steht, aus denen jeweils ein Mutant erzeugt werden kann, falls der Constraint-Solver mindestens eine Lösung findet. Um einen Bezug zwischen einzelnen Constraint-Paaren herzustellen, diese enthalten das ursprüngliche Constraint sowie dessen Negation, werden diese jeweils in IConstraintChanges mit den entsprechenden Constraint-Regeln zusammengefasst (s. Abbildung 3). Die Constraint-Regeln werden zur Darstellung innerhalb der Mutation Testing View verwendet. Jedes IConstraintChange wird wiederum in eine IMutation verpackt, die ihrerseits auf das zur Zeit einzige Constraint-System verweist. <<interface>> IConstraintChange <<interface>> IConstraint 2 * +originalConstraint: IConstraint {ReadOnly} +changedConstraint: IConstraint {ReadOnly} +appliedRule: IRule {ReadOnly} 1 1 <<interface>> IMutation +originalConstraints: Set<IConstraint> {ReadOnly} +constraintChange: IConstraintChange {ReadOnly} +refactoringProblem: IRefactoringProblem {ReadOnly} +mutateConstraints(): Set<IConstraint> * 1 <<interface>> Set<IConstraint> Constraint-System Abbildung 3 Mutationen, Ausschnitt des Klassendiagramms 3.2.1.5 Suche nach Lösungen für Mutationen Nachdem alle möglichen Mutationen (IMutation) aufgrund der durch die Annotation Negatable ausgezeichneten Constraint-Regeln gesammelt wurden und das Constraint-System aufgebaut wurde, werden nun Lösungen für die Mutationen gesucht. Dabei sind zwei Arten von Constraint-Systemen zu unterscheiden. 28 3 Implementierung Ursprüngliches Constraint-System Das ursprüngliche Constraint-System wird aus dem Java-Programm erzeugt, das für das Mutation Testing verwendet wird. Das Constraint-System ist identisch mit dem Constraint-System, welches von den in Refacola spezifizierten Refaktorisierungen aus demselben Java-Programm aufgebaut wird. Mutiertes Constraint-System Ein mutiertes Constraint-System entsteht aus einem ursprünglichen Constraint-System, in welchem ein Constraint durch seine Negation ersetzt wurde. Aus einer Lösung eines mutierten ConstraintSystems kann ein Mutant erstellt werden. Wie aus Abbildung 3 aus dem vorherigen Abschnitt ersichtlich ist, hält jede Mutation (IMutation) eine Referenz auf ein Constraint-System. Es handelt sich dabei um das einzige ursprüngliche Constraint-System, das während eines Mutation Testing Durchlaufs existiert. Zudem hat eine Mutation Zugriff auf ein ConstraintChange, das ein ursprüngliches Constraint und seine negierte Form verwaltet. Original1 : IConstraint : IMutation Negated1 : IConstraint : IMutation Negated2 : IConstraint Original : Set<IConstraint> Original2 : IConstraint : IConstraint : IConstraint : IConstraint Abbildung 4 Objektdiagramm zum Zeitpunkt vor der Erzeugung eines mutierten Constraint-Systems In Abbildung 4 ist ein Beispiel für das Objektgeflecht von Mutationen und Constraints abgebildet, welches den Zustand nach der Generierung von Mutationen und vor der Erzeugung von mutierten Constraint-Systemen verdeutlicht. Grundlage ist das Klassendiagramm aus Abbildung 3. Anstelle konkreter Klassen werden zum Zwecke der besseren Übersicht Interfaces in der Abbildung verwendet. Damit der Constraint-Solver Lösungen für eine Mutation suchen kann, muss ihm ein mutiertes Constraint-System zur Verfügung stehen. Jede Mutation kann aus dem ursprünglichen ConstraintSystem und einem ConstraintChange ein mutiertes Constraint-System erstellen. Dazu werden die Referenzen auf alle ursprünglichen Constraints dupliziert und in einem eigenen Set verwaltet. Dieses Set stellt eine Kopie des ursprünglichen Constraint-Systems dar. Aus dem Set wird die Referenz auf das ursprüngliche Constraint, welches durch eine Mutation referenziert ist, entfernt und durch die Referenz des negierten Constraints ersetzt. Die Mutation hat damit ein mutiertes Constraint-Set erstellt, zu dem der Constraint-Solver Lösungen suchen kann (s. Abbildung 5). Andere Mutationen werden davon nicht beeinflusst, da nur lesend auf die gemeinsam genutzten Constraints zugegriffen wird. Diese Situation kann ausgenutzt werden, um das Mutation Testing Framework für eine parallele Verarbei- 29 3 Implementierung tung anzupassen. In der jetzigen Implementierung findet ein Mutation Testing Durchlauf noch sequenziell statt (s. Abschnitt 3.5). Original1 : IConstraint : IMutation Negated1 : IConstraint : IMutation Negated2 : IConstraint Original : Set<IConstraint> Original2 : IConstraint : IConstraint : IConstraint Erzeugt : IConstraint Mutated : Set<IConstraint> Abbildung 5 Objektdiagramm zum Zeitpunkt nach der Erzeugung eines mutierten Constraint-Systems Unter Verwendung der im RefactoringProblem gehaltenen ISolverFactory wird ein Constraint-Solver erstellt, der nach Lösungen für das mutierte Constraint-System sucht. Aus einer gefundenen Lösung werden die an den Eigenschaften der Programmelemente durchzuführenden Änderungen, die für die jeweilige Lösung nötig sind, als IChanges zu IChangeSets zusammengefasst, die wiederum einem IRefactoringResult hinzugefügt werden. Dabei orientiert sich die Suche einer Lösung mit Hilfe des Constraint-Solvers an dem in Refacola verwendeten Algorithmus. Konnte keine Lösung gefunden werden, wird die Mutation verworfen. Die nachfolgenden Schritte werden für diese dann nicht mehr ausgeführt. Die Berechnung vieler Lösungen ist sehr aufwändig und erhöht sowohl die Laufzeit des Mutation Testing als auch den Speicherbedarf während der Lösungssuche enorm. Leider konnte auch hier die in Refacola bereits vorhandene Implementierung nicht verwendet werden, da ihre Nutzung nur möglich ist, wenn von der AbstractRefactoring-Klasse geerbt wird. Wie auch bei der Erzeugung des Constraint-Systems, brachte die Reimplementierung den Vorteil mit sich, dass nun ein IProgressMonitor für die Fortschrittsanzeige eingesetzt werden kann. Außerdem hat der aufrufende Code die Möglichkeit die Operation über den IProgressMonitor abzubrechen. 3.2.1.6 Änderung des ursprünglichen Programms zu einem Mutanten Im Allgemeinen kann der Constraint-Solver mehrere Lösungen zu einem mutierten Constraint-System finden, die sich durch die Werte der Eigenschaften von Programmelementen oder Manipulation verschiedener Programmelemente unterscheiden. Für das Mutation Testing sind diese Lösungen untereinander insoweit äquivalent, als dass sie Mutanten erzeugen, die paarweise ein identisches Verhalten aufweisen. Beispielsweise kann eine Lösung dazu führen, dass eine Methode umbenannt wird, wäh- 30 3 Implementierung rend bei einer andere Lösung der Methodenname nicht verändert wird. Im Gegensatz zu Refaktorisierung sind diese Unterschiede für das Erkennen von Mutanten durch eine Testbasis nicht von Bedeutung. Es könnte daher eine beliebige Lösung für ein mutiertes Constraint-System ausgewählt und die Änderungen am Programm durchgeführt werden. Als Auswahlkriterium für die vom Constraint-Solver gefundenen Lösungen wurde die Anzahl der vorzunehmenden Änderungen mit dem Hintergrund gewählt, dass jede Änderung am Programm die gleichen Laufzeitkosten verursacht. Es wird daher immer eine Lösung mit minimalen Änderungen aus der Menge aller vom Constraint-Solver berechneten Lösungen eines mutierten Constraint-Systems ausgewählt, um die Kosten für Änderungen am Programm zu minimieren. Eine Lösung wird in Refacola durch ein IChangeSet repräsentiert, welches seinerseits eine Menge von IChanges hält, die jeweils Änderungen an Eigenschaften von Programmelementen darstellen. In Eclipse werden Änderungen allerdings durch Changes definiert. Da die Rückschreibekomponente von Eclipse verwendet werden soll, diese stellt auch die Möglichkeit zur Verfügung Änderungen wieder rückgängig zu machen, müssen die in den IChanges gehaltenen Informationen in Changes überführt werden. Diese Aufgabe wird durch eine weitere Komponente von Refacola, dem Manipulator, übernommen. Für in Refacola spezifizierte Refaktorisierungen wird dieselbe Vorgehensweise verwendet. Der IProgramInfoProvider aus Abschnitt 3.2.1.2 bietet eine einfache Möglichkeit unter Nutzung des Manipulators aus IChangeSets Changes zu erstellen. In dem Mutation Testing Framework kapselt ein ICodeRewriter die Komplexität der in den vorherigen Abschnitten dargestellten Vorgehensweise. Er ist damit in der Lage Änderungen am Programm in Form von Changes von Eclipse durchführen zu lassen. Dabei profitiert er von der Möglichkeit, dass Eclipse zu in Changes gekapselten Änderungen Undo-Changes berechnen kann, welche die Änderungen, die aus dem ursprünglichen Programm einen Mutanten entstehen lassen, wieder rückgängig machen. Wie in Abschnitt 3.1 erläutert, ist dies wichtig, damit der Entwickler beliebig zwischen Programm und Mutanten hin- und herwechseln kann. Ferner ist für den Algorithmus 2 aus Abschnitt 3.2.1 das Zurücksetzen der Änderungen nötig, damit nach der Ausführung der Testbasis der nächste Mutant aus dem ursprünglichen Programm erzeugt werden kann. Welche Änderungen durch eine Mutation erfolgt sind und worin sich ein Mutant vom ursprünglichen Programm unterscheidet, wird im Eclipse-Editor durch Hervorhebung der entsprechenden Programmelemente deutlich. Das erleitert den Vergleich zwischen Programm und Mutant. Da der ICodeRewriter bereits über Informationen bezüglich der Änderungen verfügt, kann er aus Changes die Codestellen herausfiltern, die davon betroffen sind. Über die genaue Position der Änderungen in einzelnen CompilationUnits, diese entsprechen in Eclipse den Java-Dateien eines Programms, werden die Programmelemente herausgefiltert. 3.2.1.7 Ausführung der Testbasis und Bestimmung des Ergebnisses Nachdem aus dem ursprünglichen Programm ein Mutant generiert wurde, wird dieser jetzt kompiliert. Schlägt dies fehl, so ist der Mutant im Sinne der Sprachspezifikation fehlerhaft und damit nicht ausführbar. Unter der Voraussetzung, dass die in Refacola vorhandene Sprachdefinition vollständig und korrekt ist, bedeutet dies, dass ein Constraint negiert wurde, welches die syntaktische und semantische Korrektheit des Programms sicherstellt. Der Mutant wird als ungültig gekennzeichnet. Zur Verbesse- 3 Implementierung 31 rung der Ergebnisse eines Mutation Testing Durchlaufs sollten möglichst keine ungültigen Mutanten generiert werden (s. Abschnitt 3.3). War die Kompilierung des Mutanten erfolgreich, dann wird die Ausführung der Testbasis mittels des JUnit Test Runners (s. Abschnitt 3.2.2) angestoßen. Dieser führt alle im übergebenen Java-Programm vorhandenen JUnit-Tests aus, die auf JUnit 3.8.x oder JUnit 4 basieren. Nach Abschluss der Tests wird das Result entgegengenommen und ausgewertet. Der Mutant wurde genau dann von der Testbasis erkannt und gilt als getötet, wenn mindestens ein Test fehlschlägt. Folgende Ergebnisse sind für einen Mutanten möglich. Der Mutant wurde getötet Der Mutant weist ein dem ursprünglichen Programm abweichendes Verhalten auf, welches von außen sichtbar ist und durch die Testbasis entdeckt wurde. Mindestens ein Test ist fehlgeschlagen. Der Mutant wurde nicht getötet Der Mutant wurde von der Testbasis nicht entdeckt. Keiner der Tests ist fehlgeschlagen. Es kann nicht bestimmt werden, ob sich der Mutant anders verhält als das ursprüngliche Programm. Es wird empfohlen, dass der Entwickler den Mutanten dahingehend genauer zu untersuchen. Falls es sich nicht um einen äquivalenten Mutanten handelt, kann er durch Einführung weiterer Tests erkannt werden. Dadurch wird auch die Testabdeckung erhöht. Testbasen, die keine Tests enthalten, können keine Mutanten entdecken, weil nie ein Test fehlschlagen kann. Der Mutant ist ungültig Der Mutant konnte nicht kompiliert werden, da er fehlerhaft im Sinne der Sprachspezifikation ist. Es kann sein, dass ein Constraint negiert wurde, welches die syntaktische und semantische Korrektheit eines Programms sicherstellt. Es wird empfohlen zu prüfen, ob die Auszeichnung der Constraint-Regel, welche das Constraint erzeugt hat, überhaupt zu gültigen Mutanten führen kann (s. auch Abschnitt 3.3). Die betreffende Constraint-Regel kann über die Mutation Testing View bestimmt werden. Die Testbasis selbst sollte nur über solche Tests verfügen, die nicht manuell konfiguriert werden müssen, da ein Eingreifen seitens des Entwicklers während des Mutation Testing nicht möglich ist (vom Abbruch der Operation einmal abgesehen). Da die Testbasis für jeden gültigen Mutanten einmal komplett ausgeführt wird, können lang laufende Tests die Laufzeit des Mutation Testing stark beeinflussen. Testbasen, die für das Mutation Testing verwendet werden, sollten ausschließlich Unit-Tests und schnell ausführbare, sich selbst konfigurierbare Integration-Tests beinhalten. 3.2.2 JUnit Test Runner Der JUnit Test Runner ist verantwortlich für die Ausführung von JUnit Tests eines Java-Programms. Er wird während des Mutation Testing eingesetzt, um zu bestimmen, ob der generierte Mutant von der Testbasis erkannt wird oder nicht. Der JUnit Test Runner ist allgemein genug, um ihn auch in einem anderen Kontext abseits des Mutation Testing zu verwenden, wo Resultate eines Testdurchlaufs benötigt werden. Die Auswertung der Resultate obliegt dem aufrufenden Code. Damit eine Wiederverwendung des JUnit Test Runners möglich ist und Details der Testausführung nicht im Anwendungskern 32 3 Implementierung des Mutation Testing untergehen, wurde er von diesem separiert und in ein eigenes Eclipse-Plugin untergebracht. Zur Ausführung einer Testbasis wird dem JUnit Test Runner ein Java-Programm übergeben, welches die auszuführenden JUnit Tests beinhaltet. JUnit bietet mit JUnitCore eine einfache Schnittstelle an, um Tests, welche in den zu übergebenen Klassen (Objekte vom Typ Class) vorhanden sind, auszuführen und liefert ein Ergebnis des Testdurchlaufs, das u. a. Informationen über die Anzahl der fehlgeschlagenen Tests bereitstellt. Genau diese Information ist für das Mutation Testing von entscheidender Bedeutung, um festzustellen, ob ein Mutant von der verwendeten Testbasis erkannt wurde. Eclipse stellt ebenfalls eine Klasse JUnitCore zur Verfügung, die nicht mit der o. g. Klasse aus dem JUnitFramework verwechselt werden sollte. Mit dessen Hilfe kann aus einem IJavaElement, ein JavaProgramm stellt in Eclipse ebenfalls ein IJavaElement dar, alle Klassen ermittelt werden, die JUnit Tests beinhalten. Diese Klassen können dann als Eingabe für JUnitCore aus dem JUnit-Framework verwendet werden. Die in ihnen enthaltenen JUnit Tests werden dann ausgeführt, sofern sie auf JUnit 3.8.x oder JUnit 4 basieren. Für jeden Testdurchlauf wird lokal auf dem Rechner eine Java Virtual Machine gestartet, in denen die JUnit Tests mittels JUnit ausgeführt werden. Über Eclipse kann die für ein Java-Programm benötigte Version einer Java Virtual Machine bestimmt werden. Zusätzlich benötigt die neue Instanz der Java Virtual Machine Informationen über die zu verwendenden Klassenpfade, damit die Klassen, welche die JUnit Tests beinhalten, von ihr gefunden werden können. Gleiches gilt für die Bibliotheken des JUnit Frameworks. Beides übergibt der JUnit Test Runner an die Java Virtual Machine. : IJUnitClient : IVMRunner 1 : run() 2 : run() : JUnitServer <<create>> 3 : main() 4 : openSocket() 5 : runTests() 6 : connectSocket() 7 : testResult Abbildung 6 Abstrakter Programmfluss zwischen Client und Server Der jetzt folgende Ablauf orientiert sich an dem in Abbildung 6 dargestellten Sequenzdiagramm. Dabei wurde von der konkreten Implementierung abstrahiert, um sich auf das Wesentliche zu beschränken. Der IJUnitClient, im folgenden Client genannt, konfiguriert und startet eine Instanz der Java Virtual Machine (IVMRunner). Er wird intern vom IJUnitRunner verwendet, der wiederum die öffentliche Schnittstelle des JUnit Test Runners darstellt. Der JUnitServer, nachfolgend Server genannt, ist die Klasse, welche von der neuen Instanz der Java Virtual Machine ausgeführt wird. Als Kommunikationskanal zwischen dem Client und dem Server wird ein TCP-Socket verwendet. Der Client wählt vor dem Starten der Java Virtual Machine Instanz einen freien Port aus, über den dann 3 Implementierung 33 eine Verbindung zwischen Client und Server aufgebaut wird. Der Server benötigt einerseits Verbindungsdetails zur Herstellung einer Verbindung (den Hostname und den Port), sowie andererseits die Klassen, welche JUnit Tests beinhalten, die vom JUnit-Framework ausgeführt werden sollen. Alle Informationen übergibt der Client an die gestartete Instanz der Java Virtual Machine, die sie als Parameter an das Server-Programm bei der Ausführung der bei Java-Programmen üblichen main-Methode weiterleitet. Die Klassen (Class-Objekte) können nicht direkt übergeben werden, da nur StringParameter erlaubt sind. Stattdessen werden die vollqualifizierten Namen der Klassen an den Server übergeben, aus denen die Java Laufzeitumgebung über den Klassenpfad Class-Objekte erstellen kann. Während der Server die JUnit Tests ausführt, öffnet der Client einen TCP-Socket und wartet auf das Resultat des Testdurchlaufs, welches vom JUnit-Framework als Result geliefert wird. Da das von JUnit gelieferte Testergebnis-Objekt nicht serialisierbar ist, wurde es durch eine eigene serialisierbare Klasse im JUnit Test Runner ersetzt. Der Inhalt des JUnit Testresultats wird einfach in das hinzugefügte Datentransferobjekt (Data Transfer Object, DTO) kopiert. Dies vereinfacht die Übertragung des Testresultats vom Server zum Client, da auf ein eigenes Protokoll zum Datenaustausch verzichtet werden kann. Mit Beendigung der main-Methode des Servers beendet sich auch die vom JUnit Test Runner gestartete Java Virtual Machine. Das Testresultat wird anschließend an den aufrufenden Code als Ergebnis zurückgegeben. 3.2.3 Benutzungsoberfläche Die Benutzungsoberfläche des Mutation Testing Frameworks (s. Abbildung 7) ist als View in Eclipse integriert. Der Aufbau orientiert sich stellenweise an den Type Constraints Tester [Bär10]. Standardmäßig wird sie nicht angezeigt, kann aber über die View-Auswahl von Eclipse aufgerufen werden. Sie ist dort in der Kategorie General als Mutation Testing gekennzeichnet. Es wurde Wert darauf gelegt, dass die Mutation Testing View harmonisch ins Bild der Entwicklungsumgebung Eclipse passt und einfach zu bedienen ist, sowie über genügend Funktionalität verfügt, um dem Entwickler bei der Arbeit zu unterstützen. Dabei wurde bewusst auf die Verwendung von modalen Dialogen verzichtet, die den Entwickler bei seiner Arbeit unterbrechen könnten. Abschnitt 3.2.3.1 befasst sich mit den Möglichkeiten, die von der View zur Verfügung gestellt werden. Es wird auf einzelne Funktionen und Besonderheiten eingegangen. Darunter fallen auch die Vorbedingungen, die erfüllt sein müssen, damit Mutation Testing für ein Programm ausgeführt werden kann. Abseits der View aus Abbildung 7 nutzt das Mutation Testing Framework die Statusleiste und die Fortschrittsanzeige von Eclipse, deren Verwendung im Rahmen der View kurz beschrieben wird. Die Implementierung der View unter Verwendung des Model View Presenter – ViewModel Entwurfsmuster ist Thema von Abschnitt 3.2.3.2. Ihre Verwendung entkoppelt die Präsentationslogik und den Zustand einer Oberfläche von ihrer Darstellung. Dazu nutzt sie das in Eclipse enthaltene Databinding-Framework. Während der Entwicklung der View traten Darstellungsfehler bei der Nutzung von transparenten Bildern in Verbindung mit dem Tabellen-Widget aus dem SWT-Framework auf. Es stellte sich heraus, dass es sich um einen Bug im SWT-Framework handelt. In Abschnitt 3.2.3.3 werden das Problem und der verwendete Workaround näher erläutert. 34 3 Implementierung 3.2.3.1 Aufbau und Inhalt Abbildung 7 Mutation Testing View Die Mutation Testing View teilt sich im Wesentlichen in einen Eingabe- und einen Ausgabeteil auf. Der Eingabeteil umfasst zwei Comboboxen, in denen sowohl das Java Projekt ausgewählt werden kann, aus dem Mutanten generiert werden, als auch die Testbasis, welche zur Erkennung dieser Mutanten eingesetzt wird. Kann ein Mutation Testing Durchlauf nicht gestartet werden, wird dies durch ein Symbol zwischen Beschriftung und Combobox angezeigt. Die Tabelle mit den Resultaten eines Mutation Testing Durchlaufs stellt den Ausgabeteil dar. Operationen können über die Schaltflächen in der Werkzeugleiste rechts neben den Reitern der View ausgeführt werden. Die Werkzeugleiste enthält immer die Schaltflächen der gerade aufgerufenen View. Einige Operationen stehen auch im Kontextmenü der Tabelle zur Verfügung. Informationen über den letzten Mutation Testing Durchlauf werden in der Statusleiste von Eclipse angezeigt, wenn die Mutation Testing View den Eingabefokus hält. Darüber hinaus wird während der Ausführung von Mutation Testing Gebrauch von der Fortschrittsanzeige und der in Eclipse enthaltenen Progress View gemacht, um den Entwickler über laufende Aktivitäten in Kenntnis zu setzen. Die einzelnen Elemente der Mutation Testing View und ihre Bedeutungen sowie eventuelle Besonderheiten werden im folgenden erläutert. Weiterführende Informationen zu den Vorbedingungen, die vor Ausführung des Mutation Testing erfüllt sein müssen, finden sich in Abschnitt 3.2.1.1. Combobox "Java Project" In der Combobox "Java Project" stehen alle nicht geschlossenen Projekte des aktuell verwendeten Workspace, die Java Quellcode beinhalten, aufsteigend nach Namen sortiert zur Auswahl. Aus dem ausgewählten Java Projekt werden während des Mutation Testing Mutanten generiert. Ein IResourceChangeListener überwacht Änderungen an den Projekten des Workspace, um die Liste der zur Auswahl stehenden Java Projekte aktuell zu halten. Nach Auswahl eines Java Projekts wird dieses auch automatisch als Testbasis vorausgewählt, sodass in Situationen, in denen das Java Projekt ebenfalls die für das Mutation Testing zu verwendenden JUnit Tests zur Verfügung stellt, eine nochmalige Auswahl desselben Projekts in der Combobox "Test Base" entfällt. Natürlich kann auch ein anderes Java Projekt als Testbasis eingestellt werden. Erfüllt das ausgewählte Java Projek- 3 Implementierung 35 te zum Zeitpunkt des Startens eines Mutation Testing Durchlaufs nicht alle Vorbedingungen, weist ein dann eingeblendetes Symbol zwischen Beschriftung und Combobox mit einer der nachfolgenden Meldungen über die verletzte Vorbedingung hin. "[Java Project]" has unsaved changes "[Java Project]" contains errors Combobox "Test Base" Die Combobox "Test Base" listet ebenfalls alle nicht geschlossenen Projekte der Workspace auf, sofern diese Java Quellcode enthalten. Der Synchronisierungsmechanismus der zur Auswahl stehenden Java Projekte basiert wie bei der Combobox "Java Project" auf einem IResourceChangeListener. Das Enthalten sein von JUnit Tests wird in den zur Auswahl stehenden Java Projekten nicht überprüft. Ein Java Projekt ohne JUnit Tests wird beim Mutation Testing wie eine Testbasis betrachtet, die keine fehlschlagenden Tests enthält. So bekommt der Entwickler auf einfache Art und Weise die Möglichkeit auch ohne vorhandene JUnit Tests einige Kenntnisse aus den generierten Mutanten zu gewinnen. Die JUnit Tests in dem auswählten Java Projekt werden während des Mutation Testing zur Erkennung von Mutanten verwendet. Verletzungen der Vorbedingungen aus Abschnitt 3.2.1.1 resultieren in eine der folgenden Fehlermeldungen. "[Test Base]" has unsaved changes "[Test Base]" contains errors "[Test Base]" contains failing JUnit Tests Tabelle mit Mutation Testing Ergebnissen In der Tabelle werden nach einem Mutation Testing Durchlauf die Ergebnisse zu allen generierten Mutanten aufgelistet. Dazu gehört ebenfalls das ursprüngliche Constraint, dessen Negierung den Mutanten hervorgebracht hat, sowie die Constraint-Regel, die für das Erzeugen des Constraints zuständig war. Per Doppelklick auf eine Zeile wird der Mutant in einem Eclipse-Editor geöffnet und das manipulierte Programmelement hervorgehoben. Das Kontextmenü der Tabelle enthält die gleichnamigen in der Werkzeugleiste befindlichen Operationen zum Öffnen des ursprünglichen Programms (Open Original in Editor) und des Mutanten (Open Mutant in Editor). Mutant Result Das Ergebnis der Auswertung eines Mutanten entspricht einem der folgenden Werte. Mutant Survived Der Mutant wurde nicht durch die Testbasis entdeckt. Entweder ist sein von außen beobachtbares Verhalten äquivalent zu dem des ursprünglichen Programms, dann kann er von keinem JUnit Test entdeckt werden, oder die Testabdeckung ist nicht ausreichend, um ihn zu entdecken. Was von beiden zutrifft, kann der Entwickler durch einen Vergleich des Mutanten mit dem ursprünglichen Programm herausfinden. 36 3 Implementierung Mutant Invalid Der Mutant erfüllt nicht die Sprachspezifikation der Programmiersprache und ist deshalb nicht kompilierbar. Mutant Killed Der Mutant weist ein dem ursprünglichen Programm abweichendes Verhalten auf, das durch mindestens einen fehlgeschlagenen Test in der Testbasis entdeckt wurde. Original Constraint Das ursprüngliche Constraint, welches negiert wurde, um den Mutant zu generieren, wird in dieser Spalte aufgelistet. Constraint Rule Die Spalte enthält die Constraint-Regel, welche das ursprüngliche Constraint erzeugt hat. Sie ist in Refacola mit der Negatable-Annotation für das Mutation Testing ausgezeichnet (s. Abschnitt 3.3). Werkzeugleiste der Mutation Testing View Der Inhalt der Werkzeugleiste wird durch die zur Zeit ausgewählte View bestimmt. Die der Mutation Testing View zur Verfügung stehenden Schaltflächen in der Werkzeugleiste sind nach Verwendungsart gruppiert und abhängig vom Zustand der View aktiviert oder deaktiviert. Run Startet einen Mutation Testing Durchlauf, in welchem aus dem unter "Java Project" ausgewählten Java Projekt Mutanten generiert werden. Das unter "Test Base" ausgewählte Java Projekt dient als Testbasis. Sind die Vorbedingungen nicht erfüllt (s. Abschnitt 3.2.1.1), wird eine Fehlermeldung ausgegeben. Zum Starten muss sowohl unter "Java Project" als auch unter "Test Base" ein Java Projekt ausgewählt sein. Cancel Bricht einen laufenden Mutation Testing Durchlauf ab. Open Original in Editor Öffnet das ursprüngliche Programm zum in der Tabelle markierten Resultat in einem Eclipse-Editor. Das Programmelement, welches durch die Mutation manipuliert wird, wird im Editor hervorgehoben. Diese Operation steht auch im Kontextmenü der Tabelle zur Verfügung. Open Mutant in Editor Öffnet den Mutant zum in der Tabelle markierten Resultat in einem Eclipse-Editor. Das manipulierte Programmelement wird im Editor hervorgehoben. Diese Operation steht auch im Kontextmenü der Tabelle zur Verfügung. Show Killed Mutants Zeigt die getöteten Mutanten in der Tabelle an, falls ausgewählt, andernfalls werden sie herausgefiltert. 37 3 Implementierung Show Survived Mutants Zeigt die nicht getöteten Mutanten in der Tabelle an, falls ausgewählt, andernfalls werden sie herausgefiltert. Show Invalid Mutants Zeigt die ungültigen Mutanten in der Tabelle an, falls ausgewählt, andernfalls werden sie herausgefiltert. Benachrichtigungen über den Fortschritt während des Mutation Testing Das Mutation Testing Framework verwendet an mehreren Stellen einen IProgressMonitor, um dem aufrufenden Code Informationen über die laufenden Operationen eines Mutation Testing Durchlaufs mitzuteilen. Ein solches Objekt wird von Eclipse während der Ausführung eines Jobs zur Verfügung gestellt. Ein Job führt den in seiner run-Methode enthaltenen Code außerhalb des GUI-Threads aus, damit die Benutzungsoberfläche weiterhin auf Aktionen des Benutzers reagieren und sich bei Bedarf aktualisieren kann. Das Mutation Testing wird innerhalb eines Jobs ausgeführt. Durch die Nutzungen eines Jobs und dem von ihm bereitgestellten IProgressMonitor ist es möglich, dass Eclipse dem Entwickler den Fortschritt eines Mutation Testing Durchlaufs über ein separates Fenster (s. Abbildung 8), der Progress View und der Fortschrittsanzeige in der Statusleiste mitteilen kann. Abbildung 8 Fortschrittsanzeige während des Mutation Testing Informationen über den letzten Mutation Testing Durchlauf In der Statusleiste von Eclipse werden Informationen (s. Abbildung 9) über den letzten Mutation Testing Durchlauf angezeigt, wenn die View den Eingabefokus hält. U. a. sind der Name des Java Projekts, aus dem Mutanten generiert wurden, der Name der Testbasis (in eckigen Klammern) und der Zeitpunkt, an dem ein Durchlauf abgeschlossen wurde, enthalten. Abbildung 9 Informationen über den letzten Mutation Testing Durchlauf 38 3 Implementierung 3.2.3.2 Verwendung des Model View Presenter – ViewModel Entwurfsmusters Das Model View Presenter – ViewModel (MVP-VM) Entwurfsmuster (s. Abbildung 10) ist aus dem Model View ViewModel (MVVM) Entwurfsmuster entstanden, das sich bei der Entwicklung von Windows Presentation Foundation (WPF) und Silverlight Anwendungen unter .NET zum QuasiStandard etabliert hat. Das wesentliche Merkmal beider Entwurfsmuster ist die Verwendung eines Data Binding Frameworks zur Synchronisierung von Änderungen zwischen View und ViewModel, das auch als PresenterModel bekannt ist. Steuerelemente innerhalb der View werden über das Data Binding Framework an Eigenschaften eines ViewModels gebunden. Modifizierungen an den in der View dargestellten Daten werden direkt an das zugeordnete ViewModel weitergereicht. Bei Änderungen von Werten einer Eigenschaft des ViewModels wird das Data Binding Framework indirekt über das manuelle Auslösen von Events davon unterrichtet, damit es den neuen Wert auslesen und an die View weiterleiten kann. Das Model wird vor der View durch das ViewModel verborgen, welches als Vermittler zwischen beiden dient. Auf Code zur Synchronisierung kann dann verzichtet werden. Die lose Kopplung und die Verlagerung der Zustandsverwaltung von der View ins ViewModel erleichtern das automatisierte Testen der Präsentationslogik. Im Gegensatz zum MVVM wird beim MVP-VM die Präsentationslogik aus dem ViewModel in einen Presenter verschoben. Die View delegiert Aufgaben durch den Aufruf entsprechender Methoden an den Presenter und reduziert jegliche Logik aufs Minimum. Der Presenter hat zusätzlich die Möglichkeit wie der Presenter beim Model View Presenter (MVP) Entwurfsmuster über ein von der View implementiertes Interface direkt mit ihr zu kommunizieren. Abbildung 10 MVP-VM Entwurfsmuster [Ezr09] Da Eclipse ein Data Binding Framework zur Verfügung stellt, hat sich die Verwendung von MVP-VM für das Mutation Testing Framework angeboten. Abbildung 11 stellt die Umsetzung von MVP-VM im Mutation Testing Framework sowie die Abhängigkeiten zwischen den Bestandteilen des Entwurfsmusters auf Ebene von Interfaces dar. Den in der Benutzungsoberfläche verwendeten Interfaces und 39 3 Implementierung Klassen sind jeweils nach den Bestandteilen von MVP-VM Suffixe angehängt. Ein IMutationPresenter entspricht damit einem Presenter im MVP-VM. Das Model ist nicht Bestandteil der Benutzungsoberfläche und umfasst im Falle des Mutation Testing Frameworks den Anwendungskern und den JUnit Test Runner. Nachfolgend wird das Zusammenspiel zwischen den Bestandteilen sowie ihre Zuständigkeiten beschrieben. <<interface>> IView<T extends IViewModel> <<interface>> IViewModel Data Binding Data Binding <<interface>> IMutationView 1 Data Binding * <<interface>> IMutationViewModel 1 1 * * * <<interface>> IJavaProjectViewModel * * * * 1 <<interface>> IMutationPresenter * <<interface>> ISolutionViewModel * 0..1 <<interface>> IMutationService <<interface>> IMutationSolutionCompilation 1 1 <<interface>> IJavaProjectWatcher <<interface>> IMutationSolution 1 <<interface>> IJavaProject Abbildung 11 MVP-VM im Mutation Testing Framework View Die Benutzungsoberfläche besteht nur aus einer einzigen View (IMutationView), die genau der Eclipse-View des Mutation Testing Frameworks entspricht. Sie nimmt Eingaben des Entwicklers entgegen und leitet sie an ihren Presenter weiter, wenn es sich um Operationen handelt, die den Anwendungskern betreffen. Werte von gebundene Steuerelemente, wie z. B. die ausgewählte Testbasis, werden bei Änderungen automatisch durch das Data Binding mit dem im DataContext enthaltenen ViewModel synchronisiert. Die Bindung zwischen Steuerelementen und Eigenschaften des ViewModels wird durch die View aufgebaut, nachdem sie während der Initialisierung ihr ViewModel durch den von ihr erzeugten Presenter zugeteilt bekommt. Andere Funktionalitäten, welche nur die Darstellung der Steuerelemente beeinflussen, wie das Einfärben von Zeilen der Tabelle, oder Abhängigkeiten zu einem GUI Framework wie SWT oder JFace besitzen, sind weder Bestand des Presenters noch des ViewModels und werden nur innerhalb der View verwendet. Daten, die die View über das ViewModel erhält, wurden bereits für die Darstellung aufbereitet. Die Konvertierung von Daten zwischen View und Model wird vom ViewModel übernommen. Komplexe Daten, wie die in der Tabelle dargestellten Resultate, werden durch Aggregationen zwischen ViewModels aufgelöst. Die Datenquelle der Tabelle ist eine Liste von ISolutionViewModels, von denen jedes Element eine Zeile repräsentiert und Eigenschaften zum Abrufen der in den Spal- 40 3 Implementierung ten dargestellten Werte zur Verfügung stellt. Ähnliches gilt auch für die Daten in den Comboboxen. Presenter In der Regel existiert zu jeder View genau ein Presenter, der ihre Präsentationslogik implementiert und Aufrufe an den Anwendungskern durchführt. Die einzige View des Mutation Testing Frameworks erzeugt ihren eigenen Presenter (IMutationPresenter) und stellt ihm eine Referenz auf sich selbst zur Verfügung, über die der Presenter der View ihr ViewModel übergeben kann. Der Presenter ruft Methoden der View über ihr Interface auf, wenn der Entwickler Mutation Testing ausführen möchte, Vorbedingungen aber nicht erfüllt sind. Zur Anzeige einer Fehlermeldung wird eine ControlDecoration eingesetzt. Sie stellt keine Eigenschaft zur Verfügung, über die sie wahlweise ein- und ausgeblendet werden kann. Dafür ist ein Aufruf der Methoden show() und hide() nötig. Data Binding ist aber nur für Eigenschaften möglich. Deshalb erfolgt die Interaktion mit den beiden verwendeten ControlDecorations über den Aufruf ihrer Methoden aus der View heraus. Die Validierung der Eingabe bezüglich Einhaltung der Vorbedingungen übernimmt der Presenter. Im negativen Fall teilt er der View über ihr Interface die darzustellende Fehlermeldung mit. Die Logik zur Validierung und die Auswahl der Fehlermeldung konnte durch den Einsatz eines Presenter in Verbindung mit einem von der View abstrahierenden Interface trotz des Verzichts auf Data Binding aus der View herausgehalten werden. Der Presenter kommuniziert ebenfalls mit dem ViewModel und übergibt ihr nach dem Mutation Testing die Resultate. Damit während der Ausführung des Mutation Testing ein erneuter Start nicht möglich ist, wird die Schaltfläche zum Starten zeitweise deaktiviert. Der Presenter veranlasst dies zu Beginn der Ausführung über die Änderung des Wertes einer Eigenschaft des ViewModels. Die Änderung wird dem Data Binding Framework indirekt über Events mitgeteilt, sodass die Schaltfläche in der View deaktiviert wird. ViewModel Das ViewModel fungiert als Vermittler zwischen View und Model. Es stellt Eigenschaften zur Verfügung, die an Steuerelemente über ein Data Binding Framework gebunden werden können. Falls nötig werden Daten vom ViewModel für die Darstellung aufbereitet, wie es beim Mutation Testing Framework für die Ausgabe von Nachrichten auf der Statusleiste von Eclipse der Fall ist. Da die View auch komplexe (nicht elementare) Daten darstellt, werden verschiedene Typen von ViewModels verwendet zwischen denen Aggregationsbeziehungen bestehen. Das IMutationViewModel wird der einzigen View zugeordnet. Jedes Java Projekt, das in den Comboboxen gelistet ist, wird durch ein IJavaProjectViewModel repräsentiert. Wie beim Presenter bereits angesprochen, erhält das ViewModel der View die Resultate des letzten Mutation Testing. Es erstellt für jedes Resultat (IMutationSolution) ein ISolutionViewModel, in das jeweils ein Resultat-Objekt injiziert wird, und stellt eine Liste dieser ViewModels fürs Data Binding über eine Eigenschaft zur Verfügung. Damit das Data Binding Framework bei Änderungen von Werten eines ViewModel die neuen Werte an gebundene Steuerelemente weiterleiten kann, muss ihm dies über das Auslösen eines PropertyChange-Events mitgeteilt werden. Dazu muss das ViewModel die Möglichkeit anbieten, dass sich bei ihm Komponenten des Data Binding Frameworks als PropertyChangeListener anmelden können. Anmeldung und Verwaltung von PropertyChangeListener sowie Methoden zum Auslösen von PropertyChange-Events wurden in eine abstrakte Klasse ausgelagert, die von allen ViewModels erweitert wird. Die Verwendung der PropertyChange-Events und die Erstellung von Bindungen zwischen Steuerelementen und Eigenschaften von ViewModels für das Data Binding haben den Nachteil, dass der Name der Eigenschaft als String angeben werden muss. Das erschwert Refaktorisierungen, da z. B. die 3 Implementierung 41 Umbenennung von Eigenschaften der ViewModels Strings nicht berücksichtigt. Es ist zusätzlich eine manuelle Anpassung an allen Stellen nötig, wo der Name der umbenannten Eigenschaft verwendet wird. Im Mutation Testing Framework wurden den Interfaces der ViewModels benannte Konstante hinzugefügt, deren Werte den Namen der Eigenschaften entsprechen. Nach der Umbenennung einer Eigenschaft muss dann nur noch der Wert der Konstanten an einer Stelle im Interface des ViewModels angepasst werden. Model Alle sich am unteren Rand befindlichen Interfaces in Abbildung 11 stellen Abstraktionen von Models dar und befinden sich zusammen mit ihren Standardimplementierungen in anderen EclipsePlugins. Sie haben keinerlei Abhängigkeiten zur Benutzungsoberfläche und verwenden, falls nötig, das Observer Entwurfsmuster, um Listener über Zustandsänderungen in Kenntnis zu setzen. 3.2.3.3 Darstellungs-Bug bei Verwendung einer SWT-Tabelle In der für die Entwicklung verwendeten Version von Eclipse 3.6 (Helios Service Release 2) kommt es unter bestimmten Bedingungen zu Darstellungsfehlern, wenn in Tabellen aus dem SWT Framework Bilder mit transparentem Hintergrund eingebettet werden [Ecl11]. Der transparente Bereich des Bildes erhält die Hintergrundfarbe der Tabelle. Das führt zu einem unschönen Effekt, wenn die Hintergrundfarbe der Zeilen von der der Tabelle abweicht (s. Abbildung 12). Die Benutzungsoberfläche des Mutation Testing Frameworks verwendet je nach Ergebnis der Auswertung des Mutanten eine andere Hintergrundfarbe für eine Zeile. Andernfalls könnte die Hintergrundfarbe der Tabelle mit denen der eingefärbten Zeilen synchronisiert werden. Um dieses Problem zu umgehen registriert sich die Mutation Testing View als Listener an der Tabelle und färbt die einzelnen Zellen selbst ein (s. Listing 11). Abbildung 12 Darstellungs-Bug – Vor und nach dem Workaround 42 3 Implementierung private void registerAsEraseItemListenerOnSolutionTable( Table solutionsTable, final ITableColorProvider tableColorProvider) { solutionsTable.addListener(SWT.EraseItem, new Listener() { @Override public void handleEvent(Event event) { Color background = tableColorProvider.getBackground( event.item.getData(), event.index); event.gc.setBackground(background); event.gc.fillRectangle(event.getBounds()); } }); } Listing 11 Workaround zum Darstellungs – Bug SWT-Tabelle 3.3 Auszeichnung von Constraint-Regeln Das Mutation Testing Framework macht keine Einschränkungen bezüglich der zu negierenden Constraints. Dem Refacola-Entwickler ist es völlig freigestellt, welche Constraint-Regeln er auszeichnen möchte. Bei der Ausführung des Mutation Testing wird für jedes Constraint, das von einer ausgezeichneten Constraint-Regel erzeugt wird, eine Mutation generiert, die zu dem ursprünglichen Constraint-System ein mutiertes Constraint-System aufbaut, in dem das negierte Constraint verwendet wird. Eine Lösung des mutierten Constraint-Systems entspricht den an den Programmelementen durchzuführenden Änderungen, um das ursprüngliche Programm in einen Mutanten zu überführen (s. Abschnitt 3.2.1.4). Die Laufzeit des Mutation Testing hängt wesentlich von der Anzahl der mutierten Constraint-Systeme ab, zu denen der Constraint-Solver Lösungen soll. Deren Anzahl hängt direkt von der Summe der Constraints, die von ausgezeichneten Constraint-System erzeugt werden, ab. Dadurch können gleichzeitig mehr Mutanten generiert werden. Die Menge der generierten ungültigen Mutanten, die keinen Beitrag zur Erhöhung der Testabdeckung liefern, sollte deshalb minimiert werden. Von den erzeugten Constraints können zwei Arten unterschieden werden. Syntaktische und semantische Constraints Die syntaktischen und semantischen Constraints stellen die Korrektheit des Programms im Sinne der Sprachspezifikation der zugrundeliegenden Programmiersprache sicher und gewährleisten, dass seine Kompilierung möglich ist. Unter bestimmten Bedingungen kann die Negierung von Constraints dieser Art zu ausführbaren Programmen mit verändertem Verhalten führen [Bär10]. In den meisten Fällen führt ihre Negierung aber zu ungültigen Mutanten. Die Auszeichnung von Constraint-Regeln, die syntaktische und semantische Constraints erzeugen, stellt daher eher die Ausnahme dar. Bindungserhaltene Constraints Die bindungserhaltenen Constraints sorgen dafür, dass das Programmverhalten erhalten bleibt. Die Negierung von Constraints dieser Art kann zu Mutanten führen, die sich gegenüber dem ursprünglichen Programm anders verhalten. Dies ist nicht garantiert und hängt von der konkreten Implementierung im Programm ab. Auf jeden Fall sind die generierten Mutanten gültig. Die Auszeich- 43 3 Implementierung nung von Constraint-Regeln, aus denen diese Art von Constraints erzeugt werden, sollte deshalb angestrebt werden. Refacola erlaubt die Zusammenlegung mehrerer Constraint-Regeln zu einer einzigen. Voraussetzung ist, dass sie sich in den Bedingungen zur Erzeugung von Constraints gleichen. Semantisch hat dies keine Auswirkungen auf die Refaktorisierungen von Refacola, da weiterhin dieselben Constraints erzeugt werden. Auch das Mutation Testing Framework wird weder direkt noch indirekt von einer Zusammenlegung mehrerer Constraint-Regeln beeinflusst. Die Datei, in der sie definiert sind, wird dadurch kompakter und besser lesbar. Nun kann es vorkommen, dass eine Constraint-Regel Constraints verschiedener Arten erzeugt. Wird sie ausgezeichnet, werden auch die von ihr erzeugten syntaktischen und semantischen Constraints negiert, wodurch mehr ungültige Mutanten generiert werden können. Diese Constraint-Regeln können, ohne dass die von ihr erzeugten Constraints und damit die Refaktorisierungen von Refacola beeinflusst werden, wieder in mehrere Constraint-Regeln aufgeteilt werden, von denen jede nur eine der zwei Arten von Constraints erzeugt. Dadurch hat der Refacola-Entwickler die Möglichkeit, nur die Constraint-Regel auszuzeichnen, die bindungserhaltene Constraints erzeugt. Listing 12 stellt eine komplexe Constraint-Regel dar, die sich im Gegensatz zu den beiden elementaren Constraint-Regeln in Listing 13 auf zwei Constraint-Regeln aufteilen lässt, weil sie mehr als ein Constraint erzeugt. Die komplexe Constraint-Regel und die beiden elementaren Constraint-Regeln sind semantisch äquivalent und lassen sich gegenseitig ineinander überführen. Mehrere von einer Constraint-Regel erzeugte Constraints werden im Aktionsteil (then-Rumpf) durch Kommata getrennt. Die elementaren Constraint-Regeln in Listing 13 lassen sich zu der in Listing 12 zusammenfassen, da in beiden dieselben Bedingungen im Regelrumpf (if-Rumpf) für dieselben Programmelemente (for all-Rumpf) vorkommen. OOPSLA_f0_memberAccess for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then rec.owner = ref.owner, Java.sub*(rec.inferredDeclaredType, M.owner) end Listing 12 Komplexe Constraint-Regel 44 3 Implementierung OOPSLA_f0_memberAccess_1 for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then rec.owner = ref.owner end OOPSLA_f0_memberAccess_2 for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then Java.sub*(rec.inferredDeclaredType, M.owner) end Listing 13 Elementare Constraint-Regeln Zur Auszeichnung von Constraint-Regeln in Refacola für das Mutation Testing wurde die in Listing 14 dargestellten Negatable-Annotation eingeführt. Sie dient als Marker für das Mutation Testing Framework, um die zu negierenden Constraints zu bestimmen. Nachdem der Refacola-Datei, in der Constraint-Regeln definiert werden, einmalig die Annotation durch Einfügen einer importAnweisung bekannt gemacht wurde, kann die Negatable-Annotation an Constraint-Regeln angehängt werden (s. Listing 15). Wurden Änderungen an Constraint-Regeln durchgeführt, muss das zur Generierung von Java Quellcode Dateien aus der Refacola DSL vorhandene Script ausgeführt werden, damit die Änderungen für das Mutation Testing Framework sichtbar werden. annotation Negatable() Listing 14 Refacola-Typ zur Auszeichnung von Constraint-Regeln 45 3 Implementierung import "mutation.annotation.refacola" @Negatable OOPSLA_f0_nameBasedAccess for all r: Java.NamedReference E: Java.NamedEntity do if Java.binds(r, E) then r.identifier = E.identifier end Listing 15 Auszeichnung einer Constraint-Regel Aus der Refacola DSL wird für jede definierte Constraint-Regel eine Java Klasse generiert. Für die Negatable-Annotation wird eine Java Annotation mit demselben Namen erzeugt (s. Listing 16), auf die während der Laufzeit per Reflection zugegriffen werden kann. Java Klassen, die aus ConstraintRegeln generiert werden, verfügen, falls in Refacola zum Zeitpunkt der Generierung die ConstraintRegeln ausgezeichnet waren, über die Java Annotation Negatable, welche an die Klasse selbst angehängt wird (s. Listing 17). @Retention(RetentionPolicy.RUNTIME) public @interface Negatable { } Listing 16 Generierte Java Annotation zu in Refacola definierten Annotation @Negatable() public class OOPSLA_f0_nameBasedAccess extends AbstractRule Listing 17 Generierte Java Klasse aus ausgezeichneter Constraint-Regel Während der Erzeugung des Constraint-Systems aus einem Programm beim Mutation Testing, wird mittels Reflection geprüft, ob die Java Klasse der Constraint-Regel, aus der das jeweilige Constraint erstellt wurde, über die Java Annotation Negatable verfügt. Alle von solch ausgezeichneten Constraint-Regeln erzeugten Constraints werden während des Aufbaus des Constraint-Systems gesammelt. Zusätzlich werden für die einzelnen gesammelten Constraints noch Referenzen auf die Constraint-Regeln, von denen sie erzeugt wurden, verwaltet. Aus den gesammelten Constraints werden dann negierte Constraints abgeleitet. Ein ursprüngliches Constraint, seine negierte Form, die zugehörige Constraint-Regel sowie das komplette Constraint-System ergeben zusammen eine Mutation (s. Abschnitt 3.2.1.4). Jede Mutation kann aus diesen Informationen ein mutiertes Constraint-System erstellen, dessen Lösungen zur Erzeugung von Mutanten aus dem ursprünglichen Programm verwendet werden. Für die Erstellung eines mutierten Constraint-Systems sind die in den Mutationen verwalteten Constraint-Regeln nicht weiter relevant. Sie werden aber zur Darstellung in der Benutzungsoberfläche verwendet, um dem Entwickler Informationen über die Herkunft der Constraints zu geben. 46 3 Implementierung 3.4 Beispiel zur Anwendung des Mutation Testing Frameworks In diesem Abschnitt wird ausgehend von den in Refacola definierten Constraint-Regeln die Anwendung des Mutation Testing Frameworks an einem kleinen Java-Programm demonstriert. Zuerst wird eine Constraint-Regel formuliert, dessen Constraints die syntaktische und semantische Korrektheit des Java-Programms im Kontext überschriebener Methoden sicherstellen (s. Listing 18). Methodenparameter, Rückgabetypen sowie die Deklarationen von Checked Exceptions werden ausgeblendet und nicht weiter betrachtet. Die Constraint-Regel aus Listing 18 stellt jede Methode einer Subklasse jeder Methode einer Superklasse gegenüber und prüft, ob die Methode der Subklasse die der Superklasse überschreibt. Dies ist nicht der Fall, wenn beide Methoden unterschiedliche Bezeichner besitzen oder die Methode in der Superklasse über den Sichtbarkeitsmodifikator private verfügt. In diesen Situationen ist das erzeugte Constraint stets erfüllt. In allen anderen Fällen überschreibt die Methode in der Subklasse die der Superklasse. Gemäß der Java Sprachspezifikation muss die überschreibende Methode der Subklasse zumindest die gleiche Sichtbarkeit besitzen wie die überschriebene Methode der Superklasse. Dies wird durch die Teilbedingung SubMethod.accessibility >= SuperMethod.accessibility des Constraints ausgedrückt. Eine Verletzung führt zu einem nicht kompilierbaren Programm. Die Constraint-Regel erzeugt somit ausschließlich syntaktische und semantische Constraints und wird für das Mutation Testing nicht ausgezeichnet. MGR_methodOverriding for all SuperClass: Java.Class SubClass: Java.Class SuperMethod: Java.InstanceMethod SubMethod: Java.InstanceMethod do if Java.sub(SubClass, SuperClass), Java.member(SuperClass, SuperMethod), Java.member(SuperClass, SubMethod) then SuperMethod.identifier != SubMethod.identifier or SuperMethod.accessibility = #private or SubMethod.accessibility >= SuperMethod.accessibility end Listing 18 Constraint-Regel zur Sicherstellung syntaktischer und semantischer Korrektheit bei überschriebenen Methoden Die nachfolgende Constraint-Regel (Listing 19) wird nun genutzt, um die dynamische Bindung des Methodenaufrufs im Java-Programm, das als Beispiel verwendet wird, zu entfernen. Falls in der Superklasse ein Aufruf einer Methode vorhanden ist und die Methode denselben Bezeichner besitzt wie eine Methode aus einer der Subklassen, dann muss die Methode der Superklasse über eine höhere Sichtbarkeit verfügen als private. Damit ist sichergestellt, dass die Methode der Subklasse die der Superklasse überschreibt. Die Auszeichnung dieser Constraint-Regel mit der Negatable-Annotation führt dann dazu, dass das erzeugte Constraint negiert wird und die Sichtbarkeit private für die in der 3 Implementierung 47 Superklasse vorhandene Methode gesetzt werden muss, um eine Lösung des Constraint-Systems zu erhalten. @Negatable MGR_methodOverridingAccess for all reference: Java.MemberReference SuperClass: Java.Class SubClass: Java.Class SuperMethod: Java.InstanceMethod SubMethod: Java.InstanceMethod do if Java.binds(reference, SuperMethod), Java.sub(SubClass, SuperClass), Java.member(SuperClass, SuperMethod), Java.member(SubClass, SubMethod) then SuperMethod.identifier = SubMethod.identifier -> SuperMethod.accessibility > #private end Listing 19 Constraint-Regel zum Erhalt der dynamischen Bindung bei überschriebenen Methoden In Listing 20 ist links das Java-Programm dargestellt, aus dem Mutanten generiert werden. Die Methode getClassName() der Klasse A wurde in Klasse B überschrieben. Abhängig vom Typ des Objekts wird in der Methode getName() zur Laufzeit entweder A.getClassName() oder B.getClassName() aufgerufen. Die Negierung des von der Constraint-Regel aus Listing 19 erzeugten Constraints verlangt, dass die Sichtbarkeit der Methode A.getClassName() auf private reduziert wird. Als Resultat entsteht der Mutant, der in Listing 20 rechts dargestellt ist. Die Methode getClassName() wird von Klasse B nicht mehr überschrieben und getName() ruft jetzt unabhängig vom Typ des Objekts stets die Methode A.getClassName() auf. Während im ursprünglichen Programm ein Aufruf von getName() auf einem Objekt vom Typ B den Wert "B" liefert, gibt der Mutant beim Aufruf derselben Methode auf einem Objekt desselben Typs den Wert "A" zurück. In Listing 21 ist der verwendete JUnit-Test dargestellt, der den Mutanten erkennt. Die Ergebnisse des Mutation Testing Durchlaufs werden in Abbildung 13 aufgelistet. 48 3 Implementierung class A { class A { public String getClassName() { return "A"; } private String getClassName() { return "A"; } public String getName() { return getClassName(); } public String getName() { return getClassName(); } } } class B extends A { class B extends A { public String getClassName() { return "B"; } } public String getClassName() { return "B"; } } Listing 20 Ursprüngliches Programm (links) und generierter Mutant mit minimalen Änderungen (rechts) public class BTests { @Test public void toString_newInstance_returnsB() { B cut = new B(); String result = cut.getName(); assertTrue(result.equals("B")); } } Listing 21 JUnit-Test zum Anwendungsbeispiel Abbildung 13 Ergebnisse des Anwendungsbeispiels 49 3 Implementierung Der Mutant in Listing 20 weicht syntaktisch kaum vom ursprünglichen Programm ab. Das Mutation Testing Framework hat aus der Menge der vom Constraint-Solver berechneten Lösungen zu dem mutierten Constraint-System diejenige ausgewählt, die am wenigsten Programmelemente verändert. Die Anzahl zu berechnender Lösungen wurde nicht beschränkt. Selbst für dieses kleine Java-Programm konnte der verwendete Constraint-Solver Choco 192 Lösungen finden. Da verschiedene Lösungen desselben Constraint-Systems zu paarweise verhaltensäquivalenten Programmen führen, vorausgesetzt die Constraint-Regeln sind vollständig und korrekt, würde die Berechnung von höchstens einer Lösung für jedes mutierte Constraint-System ausreichen und zudem die Laufzeit des Mutation Testing reduzieren. Diese Lösungen müssen nicht minimal in Bezug auf die syntaktischen Änderungen am Programm sein. Listing 22 rechts zeigt den Mutanten, der entsteht, wenn maximal eine Lösung vom Constraint-Solver berechnet wird. Das ursprüngliche Ziel, dass die Methode B.newgetClassName() die Methode A.newgetClassName() nicht mehr überschreibt, wird ebenfalls erreicht. class A { class newA { public String getClassName() { return "A"; } private String newgetClassName() { return "A"; } public String getName() { return getClassName(); } private String newgetName() { return newgetClassName(); } } } class B extends A { class B extends newA { public String getClassName() { return "B"; } } public String newgetClassName() { return "B"; } } Listing 22 Ursprüngliches Programm (links) und generierter Mutant (rechts), JUnit-Test liegt in einem anderem Java-Projekt Allerdings ergeben sich durch die zusätzlichen Änderungen an den Programmelementen neue Probleme. Wie in Abbildung 13 zu sehen ist, wurde als Testbasis ein anderes Java-Projekt verwendet, aus dem die Methode B.getName() referenziert wird. Die Klasse B des Mutanten verfügt aber über keine Methode mit diesem Namen. Die Testbasis kann daher für diesen Mutanten nicht ausgeführt werden, da sie in diesem Fall nicht kompilierbar ist. Dieses Problem wird in Abschnitt 3.5 genauer betrachtet. 50 3 Implementierung Wird der JUnit-Test aus Listing 21 in das Java-Projekt MutationExample verschoben, generiert das Mutation Testing Framework den Mutanten in Listing 23 rechts, wenn maximal eine Lösung berechnet wird. Da sich die Klasse, welche den JUnit-Test beinhaltet, nun im selben Java-Projekt befindet, werden auch für ihre Programmelemente Constraints erzeugt und bei der Suche nach einer Lösung berücksichtigt. Dadurch ist sichergestellt, dass die Methode newgetName() weiterhin von der Klasse newBTests aufgerufen werden kann. Das Problem besteht darin, dass die Sichtbarkeit der Klasse newBTests reduziert wird (s. Listing 24). Obwohl dies kein syntaktischer oder semantischer Fehler ist, kann der enthaltene JUnit-Test nicht mehr vom JUnit 4 Framework ausgeführt werden. JUnit 4 verlangt für Klassen, die JUnit-Tests beinhalten, dass diese mit der Sichtbarkeit public gekennzeichnet sind. In der Folge meldet JUnit 4 fehlgeschlagene Tests, woraus das Mutation Testing Framework die Erkennung des Mutanten ableitet. Auch dieses Problem wird in Abschnitt 3.5 aufgegriffen. class A { class newA { public String getClassName() { return "A"; } private String newgetClassName() { return "A"; } public String getName() { return getClassName(); } String newgetName() { return newgetClassName(); } } } class B extends A { class newB extends newA { public String getClassName() { return "B"; } } public String newgetClassName() { return "B"; } } Listing 23 Ursprüngliches Programm (links) und generierter Mutant (rechts) JUnit-Test liegt im selben Java-Projekt class newBTests { @Test public void toString_newInstance_returnsB() { newB cut = new newB(); String result = cut.newgetName(); assertTrue(result.equals("B")); } } Listing 24 JUnit-Test zum Mutanten in Listing 23 rechts 3 Implementierung 51 3.5 Mögliche Verbesserungen und Erweiterungen Die Implementierung des Mutation Testing Frameworks bietet dem Refacola-Entwickler durch die Auszeichnung von Constraint-Regeln eine einfache und flexible Möglichkeit an, ohne Änderungen oder Erweiterungen des Quellcodes Einfluss auf die Generierung von Mutanten zu nehmen. Die Mutation Testing View unterstützt den Entwickler, der Mutation Testing für seine Programme benutzen möchte, bei der Untersuchung von Mutanten. Während der Entwicklung des Mutation Testing Frameworks entwickelten sich einige Ideen zur Verbesserung der Implementierung. Im Rahmen dieser Arbeit war es zeitlich nicht möglich diese umzusetzen. Auf Kopien von Programmen arbeiten Während eines Mutation Testing Durchlaufs werden Mutanten aus einem vorher ausgewählten Programm generiert. Dabei wird das Programm selbst geändert und anschließend neu kompiliert. Die durchgeführten Änderungen werden danach wieder rückgängig gemacht. Dies stellt kein Problem dar, solange der Mutation Testing Durchlauf ordnungsgemäß beendet oder durch den Entwickler abgebrochen wird. In beiden Fällen wird der Quellcode des Programms wieder in seinen vorherigen Zustand überführt. Wird aber die Ausführung der Entwicklungsumgebung Eclipse abrupt abgebrochen, beispielsweise durch einen Systemfehler, bei dem das Mutation Testing Framework nicht mehr die Möglichkeit hat die Änderungen zurückzusetzen, verbleibt der Quellcode des Programms in seinem geänderten Zustand. Der Entwickler ist dann dafür verantwortlich das Programm wiederherzustellen, indem er z. B. die von den Änderungen betroffenen Stellen im Quellcode manuell sucht und umschreibt. Die Wahrscheinlichkeit eines abrupten Abbruchs wird umso größer, je mehr Zeit zwischen den Änderungen am Programm und dem Zurücksetzen der Änderungen liegt. Mit dem Anzeigen eines Mutanten werden Änderungen am Programm durchgeführt, die erst mit dem Anzeigen des ursprünglichen Quellcodes oder während der Freigabe von Ressourcen des Mutation Testing Frameworks beim Beenden von Eclipse wieder zurückgesetzt werden. Um diese Probleme zu lösen, kann von dem Programm eine temporäre Kopie erzeugt werden, die für das Mutation Testing verwendet wird. Die Kopie könnte zu Beginn eines Mutation Testing Durchlaufs automatisch erstellt werden. Manipulation von Programmelementen für mehrere Java-Projekte Damit Lösungen für ein mutiertes Constraint-System, dieses ist durch das ursprüngliche Constraint-System entstanden, in dem ein ausgewähltes Constraint negiert wurde, gefunden werden können, müssen Werte von Constraint-Variablen verändert werden. Diese Veränderungen wirken sich entsprechend auf den Quellcode des Mutanten aus und können beispielsweise die Sichtbarkeit einer Methode reduzieren oder den Bezeichner einer Klasse manipulieren. Die syntaktischen und semantischen Constraints gewährleisten, dass der aus einer Lösung erstellte Mutant kompilierbar ist. Nun kann es durchaus sein, dass aus einem anderen Java-Projekt heraus auf eine Klasse verwiesen wird, die Teil desjenigen Projekts ist, aus dem Mutanten generiert werden Das erstere Java-Projekt kann die JUnit-Tests beinhalten, die als Testbasis für das Mutation Testing verwendet werden. In einem solchen Fall wäre die Testbasis nicht mehr kompilierbar, da sie die Klasse, dessen Bezeichner manipuliert wurde, nicht mehr referenzieren kann. Um diese Referenzen anzupassen, müssen nicht nur für das Java-Projekt, aus dem Mutanten generiert werden, sondern auch für die Testbasis, falls es sich nicht um dasselbe Java-Projekt handelt, Constraints erzeugt und zu einem Constraint- 52 3 Implementierung System zusammengefasst werden. Daraus kann dann der Constraint-Solver Lösungen berechnen, die sicherstellen, dass auch die Testbasis kompilierbar bleibt. Refacola bietet mit dem MultiProgramInfoProvider die Möglichkeit Faktenbasen und Programmelemente mehrerer Java-Projekte zu erzeugen. Mit ihrer Hilfe kann ein Constraint-System aufgebaut werden, das aus den Constraints verschiedener Java-Projekte besteht. Das weitere Vorgehen zur Generierung von Mutanten erfolgt analog zu Abschnitt 3.2.1. Der MultiProgramInfoProvider könnte unter Verwendung der in Refacola enthaltenen Klasse Manipulator aus den Informationen eines IChangeSets die von Eclipse verwendeten Changes zur Durchführung von Änderungen am Quellcode berechnen. Allerdings unterstützt der Manipulator zur Zeit keine Berechnung von Änderungen verschiedener Java-Projekte, weshalb der MultiProgramInfoProvider nicht vom Mutation Testing Framework verwendet wird. Constraints für JUnit-Test-Klassen An Klassen, in denen JUnit-Tests enthalten sind (im folgenden JUnit-Test-Klassen genannt), stellen die JUnit Frameworks besondere Anforderungen. JUnit 3 verlangt, dass alle JUnit-Test-Klassen stets über die Sichtbarkeit public verfügen und von der Klasse TestCase erben. Zur Identifikation der durch das JUnit 3 Framework auszuführenden Methoden müssen diese im Bezeichner das Präfix test verwenden. Auch JUnit 4 verlangt, dass die JUnit-Test-Klassen die Sichtbarkeit public haben. Methoden, die vom JUnit 4 Framework ausgeführt werden sollen, werden nicht mehr über ihren Bezeichner sondern über eine Annotation Test identifiziert, die an entsprechende Methoden angeheftet wird. Für das Mutation Testing Framework sind diese besonderen Anforderungen, die an bestimmte Klassen und Methoden zu stellen sind, nicht ersichtlich. Es kann also durchaus sein, dass eine vom Constraint-Solver berechnete Lösung ausgewählt wird, die zu einer Änderung der Sichtbarkeit einer JUnit-Test-Klasse führt. Das JUnit Framework würde in einem solchen Fall das Scheitern eines oder mehrerer Tests melden und der Mutant würde dann fälschlicherweise als erkannt angesehen. Dieses Problem kann über Constraints gelöst werden, die sicherstellen, dass die besonderen Anforderungen eingehalten werden. Listing 25 zeigt wie eine Constraint-Regel aussehen könnte, die sicherstellt, dass JUnit-Test-Klassen für das JUnit 3 Framework die Sichtbarkeit public beibehalten. Refacola müsste entsprechend erweitert werden, um auch Abfragen bezüglich Annotationen durchführen zu können. MGR_junit3_testclass_public for all TestCaseClass: Java.TopLevelClass TestClass: Java.Class do if Java.sub(TestClass, TestCaseClass) then TestCaseClass.identifier = 'TestCase' -> TestClass.accessibility = #public end Listing 25 Constraint-Regel für Sichtbarkeit von JUnit-3-Test-Klassen 3 Implementierung 53 Parallele Verarbeitung Das Mutation Testing Framework arbeitet strikt sequenziell. Nach Prüfung der Vorbedingungen für das Programm und die Testbasis erfolgt der Aufbau der Java-Faktenbasis. Mit dessen Hilfe sowie der in Refacola vorhandenen Constraint-Regeln wird das Constraint-System des Programms aufgebaut. Sowohl die Erstellung der Java-Faktenbasis als auch die Erzeugung des Constraint-Systems können bei größeren Programmen durchaus einige Zeit in Anspruch nehmen. Es wäre zu prüfen, welche Operationen parallelisierbar sind, um die Leistung heutiger und zukünftiger Mehrkernprozessoren auszunutzen, um die Laufzeit zu reduzieren. Davon würden auch die Refaktorisierungen in Refacola profitieren, da sie ebenfalls auf die Erstellung von Java Faktenbasen angewiesen sind. Die Abarbeitung der Mutationen erfolgt in einer Schleife. Für jede Mutation wird eine Reihe von Schritten ausgeführt, von der Suche nach einer Lösung für das mutierte Constraint-System bis zur Ausführung der Testbasis unter Verwendung des generierten Mutanten (s. Algorithmus 2). Wenn zwischen den Mutationen keine Datenabhängigkeit besteht, könnte die Schleife parallelisiert werden. In der aktuellen Implementierung des Mutation Testing Frameworks teilen sich die Mutationen das Programm, welches zum Mutation Testing ausgewählt wurde. Jede Mutation manipuliert das Programm, um aus diesem einen Mutanten zu erzeugen, und setzt die Änderungen anschließend zurück, bevor die nächste Mutation das Programm verändert. Hierbei handelt es sich im Sinne der Parallelverarbeitung um einen kritischen Abschnitt, in dem sich zu jedem Zeitpunkt maximal ein Prozess oder ein Thread befinden darf. Wird die Datenabhängigkeit zwischen den Mutationen aufgelöst, entfällt der kritische Abschnitt und die Schleife kann parallelisiert werden. Die Anfertigung von Kopien des Programms würde dieses Problem lösen. Jeder Thread, der zur Parallelverarbeitung eingesetzt wird, könnte eine Menge von Mutationen erhalten, die er sequenziell abarbeitet. Dabei wird für jeden dieser Threads eine Kopie des Programms erzeugt. Die Mutationen, die Thread n bearbeitet, würden dann Kopie n verwenden. Zu klären wäre, ob die Java-Faktenbasis sowie das Constraint-System nur einmal erstellt werden können und dann für alle Kopien des Programms gelten. Weiterhin ist zu klären, inwieweit der durch die Kopien zusätzlich benötigte Speicherbedarf eine Einschränkung darstellt. Unterstützung weiterer Programmiersprachen Ziel des Mutation Testing Frameworks ist die Generierung von Mutanten aus Java Programmen. Es stellt eine Erweiterung von Refacola dar und setzt auf ihrem Konzept der Constraint-basierten Refaktorisierung auf. Refacola selbst ist aber nicht auf eine Programmiersprache festgelegt. Sie kann nicht nur dazu verwendet werden, um Refaktorisierungen innerhalb einer konkreten Programmiersprache, sondern auch um Refaktorisierungen zwischen verschiedenen Programmiersprachen zu definieren und durchzuführen. Während Refacola durch das Hinzufügen weiterer Sprachdefinitionen, Constraint-Regeln und Faktenbasen weitere Programmiersprachen unterstützen kann, bleibt das Mutation Testing Framework in seiner aktuellen Implementierung auf Java Programme beschränkt. Obwohl wo immer möglich Komponenten von Refacola verwendet werden, um weitestgehend von einer konkreten Programmiersprache zu abstrahieren, waren Abhängigkeiten zur Java Programmiersprache nicht gänzlich zu vermeiden. Dies trifft insbesondere auf die Nutzung des JUnit Frameworks zu. 54 3 Implementierung 4 Zusammenfassung und Fazit 55 4 Zusammenfassung und Fazit Automatisierte Tests können während der Entwicklung und der Wartung von Software eingesetzt werden, um Fehler frühzeitig zu entdecken. Bei Änderungen an der Struktur der Software im Zuge von teils mit Werkzeugen durchgeführten, teils manuell durchgeführten Refaktorisierungen und dem Hinzufügen weiterer Funktionen können sie unbeabsichtigte Änderungen am Programmverhalten entdecken und sicherstellen, dass vorhandene Funktionalität nicht zerstört wird. Dazu ist es erforderlich, dass die Testabdeckung ausreichend ist, um Fehlverhalten seitens das Programms entdecken zu können. Beim Mutation Testing werden durch automatisierte Manipulationen an der Code-Basis eines Programms Mutanten generiert. Sie sollen kompilierbar sein und ein dem ursprünglichen Programm verändertes, aber nicht erwünschtes Verhalten zeigen. Eine Testbasis soll dann diese Mutanten erkennen. Nicht erkannte Mutanten lassen auf eine nicht ausreichende Testabdeckung schließen. Der Entwickler kann durch Untersuchung der Mutanten Testdaten gewinnen, aus denen weitere Testfälle zur Erhöhung der Testabdeckung entwickelt werden können. Es wurde aufgezeigt, dass die Schwierigkeit beim Mutation Testing in der Generierung von Mutanten hoher Qualität liegt. Können äquivalente Mutanten vermieden werden, erhöht sich die Effizienz des Mutation Testing, da der Entwickler diese Mutanten nicht zeitaufwändig untersuchen und manuell herausfiltern muss. Der Verzicht auf die Generierung nicht kompilierbarer Mutanten reduziert die Laufzeitkosten und damit die Dauer eines Mutation Testing Durchlaufs. Es wurde diskutiert, wie die constraint-basierte Refaktorisierung als Grundlage verwendet und angepasst werden kann, um die Generierung nicht kompilierbarer Mutanten zu vermeiden und die Anzahl generierter äquivalenter Mutanten zu reduzieren. In Anschluss daran wurden einige Ausschnitte aus der sich noch in der Entwicklung befindlichen Refacola vorgestellt. Refacola verwendet die Technik der constraint-basierten Refaktorisierung, um sicherzustellen, dass das resultierende Programm nach einer Refaktorisierung weiterhin kompilierbar ist und sein Verhalten beibehält. Durch die Vorzüge, die eine Anpassung der bei der constraint-basierten Refaktorisierung verwendeten Technik für das Mutation Testing bietet, wurde Refacola im Rahmen dieser Arbeit um ein Mutation Testing Framework erweitert. Ausgehend von der Sicht des Entwicklers wurden Anforderungen an das Mutation Testing Framework ausgearbeitet und diskutiert. Während sich andere Ansätze zur constraint-basierten Generierung von Mutanten, in [Bär10] werden Type Constraints und in [S+T10] Accessibility Constraints betrachtet, auf bestimmte Constraints beschränken, macht das Mutation Testing Framework diesbezüglich keine Einschränkungen. Der Refacola-Entwickler kann durch Auszeichnung von in Refacola definierten Constraint-Regeln die Constraints bestimmen, die im Zuge des Mutation Testing zur Generierung von Mutanten negiert werden sollen. Das Mutation Testing Framework stellt darüber hinaus eine EclipseView zur Verfügung, die alle generierten Mutanten auflistet und dem Entwickler die Möglichkeit gibt diese zu untersuchen. Kern des Mutation Testing Frameworks ist der verwendete Algorithmus zur Generierung und Auswertung von Mutanten. Während einige Funktionen von Refacola unverändert übernommen werden konnten, wie die Generierung der Java-Faktenbasis, mussten andere im Mutation Testing Framework erneut implementiert werden, was zu einer Dopplung von Code geführt hat. Dazu zählt der CompleteConstraintSetGenerator, der für die Erzeugung von Constraints aus Programmen verantwortlich ist. Nur während der Generierung von Constraints konnte eine Verbindung zwischen 56 Constraint-Regeln 4 Zusammenfassung und Fazit und Constraints hergestellt werden. Ein Eingriff in den vom CompleteConstraintSetGenerator implementierten Algorithmus ist leider nicht möglich. Dies ist für die Umsetzung aber notwendig gewesen, da nur über die Auszeichnung der Constraint-Regeln die für das Mutation Testing zu negierenden Constraints identifiziert werden können. Die Auszeichnung der Constraint-Regeln zur Bestimmung der zu negierenden Constraints wurde möglichst einfach gehalten. Es genügt entsprechende auszuwählen und mit einer Annotation zu versehen. Dies erfolgt ebenso deklarativ wie die Spezifizierung von Constraint-Regeln und fügt sich damit nahtlos in Refacola ein. Die Qualität der generierten Mutanten hängt nicht zuletzt von den spezifizierten Constraint-Regeln ab. Refacola befindet sich zum Zeitpunkt der Erstellung dieser Arbeit noch in der Entwicklung, weshalb die vorhandenen Constraint-Regeln noch unvollständig sind und nicht die gesamte Sprachspezifikation von Java abdecken. Dadurch werden häufig Mutanten generiert, die nicht kompilierbar sind. Die Unvollständigkeit der Constraint-Regeln hat ebenfalls Auswirkungen auf die Lösungen des ConstraintSystems. Es kann passieren, dass unterschiedliche Lösungen desselben Constraint-Systems zu unterschiedlichen Arten von Mutanten führen. Während eine Lösung zu einem relevanten Mutanten führt, kann eine andere Lösung wiederum in einen ungültigen Mutanten resultieren. Das Mutation Testing Framework kann darüber hinaus noch ausgebaut und verbessert werden. So wird bei der Generierung von Mutanten stets das ursprüngliche Programm manipuliert mit dem entsprechenden Risiko, dass die durchgeführten Änderungen bei einem Systemfehler nicht mehr zurückgesetzt werden können. In diesem Fall muss der Entwickler die Änderungen am Programm manuell rückgängig machen. Hier wäre es ratsam während des Mutation Testing auf einer Kopie des Programms zu arbeiten. Das hätte ebenfalls den Vorteil, dass große Teile des Algorithmus parallelisiert werden können, da jeder Thread dann eine eigene Kopie des Programms verwenden kann. Da sowohl die in Refacola spezifizierten Refaktorisierungen als auch das Mutation Testing Framework dieselben Constraint-Regeln verwenden, profitieren beide gleichermaßen von ihrer Vervollständigung, die im Laufe der Entwicklung von Refacola noch erfolgen wird. 5 Literaturverzeichnis 57 5 Literaturverzeichnis [Bär10] Mutantengenerierung durch Type Constraints BÄR, ROBERT Bachelorarbeit Fernuniversität in Hagen August 2010 http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeit-baer.pdf Zugriff: 18. Februar 2012 [Ecl11] Bug 50163 – Table doesn't respect transparency in column images when using a different row background color https://bugs.eclipse.org/bugs/show_bug.cgi?id=50163 Zugriff: 27. Dezember 2011 [Ezr09] MVVM for .NET Winforms – MVP-VM (Model View Presenter – View Model) Introduction EZRA, AVIAD August 2009 http://aviadezra.blogspot.com/2009/08/mvp-mvvm-winforms-data-binding.html Zugriff: 17. Dezember 2011 [J+H] An Analysis and Survey of the Development of Mutation Testing (JIA, YUE), (HARMAN, MARK) Journal IEEE Transactions on Software Engineering Volume 37 Issue 5, September 2011 IEEE Press Piscataway, NJ, USA September 2011 [Kre11] Systematisches Testen von Constraintregeln KREIS, MARIUS Masterarbeit Fernuniversität in Hagen Mai 2011 http://www.fernuni-hagen.de/imperia/md/content/ps/masterarbeit-kreis.pdf Zugriff: 18. Februar 2012 [Off92] Investigations of the Software Testing Coupling Effect OFFUTT, A. J. Journal ACM Transactions on Software Engineering and Methodology (TOSEM) Volume 1 Issue 1, Jan. 1992 ACM New York, NY, USA Januar 1992 [Osh10] The Art of Unit Testing Deutsche Ausgabe OSHEROVE, ROY November 2010 mitp ISBN 978-3-8266-9023-5 58 5 Literaturverzeichnis [S+T09] From Public to Private to Absent: Refactoring JAVA Programs under Constrained Accessibility (STEIMANN, FRIEDRICH), (THIES, ANDREAS) Proceeding Genoa Proceedings of the 23rd European Conference on ECOOP 2009 – Object Oriented Programming Springer-Verlag Berlin, Heidelberg ©2009 Juli 2009 http://www.fernuni-hagen.de/ps/pubs/ECOOP2009.pdf Zugriff: 18. Februar 2012 [S+T10] From Behaviour Preservation to Behaviour Modification: Constraint-Based Mutant Generation (STEIMANN, FRIEDRICH), (THIES, ANDREAS) Proceeding ICSE '10 Proceedings of the 32nd ACM/IEEE International Conference on Software Engineering – Volume 1 ACM New York, NY, USA ©2010 Mai 2010 [SKP11] A Refactoring Constraint Language and its Application to Eiffel (STEIMANN, FRIEDRICH), (KOLLEE, CHRISTIAN), (VON PILGRIM, JENS) Proceeding ECOOP'11 Proceedings of the 25th European conference on Object-oriented programming Springer-Verlag Berlin, Heidelberg ©2011 Juli 2011 http://www.feu.de/ps/pubs/ECOOP2011.pdf Zugriff: 18. Februar 2012 [Ste10] Korrekte Refaktorisierungen: Der Bau von Refaktorisierungswerkzeugen als eigenständige Disziplin STEIMANN, FRIEDRICH OBJEKTspektrum 4 Seite 24 - 29 April 2010 http://www.sigs-datacom.de/fileadmin/user_upload/zeitschriften/os/2010/04/ steimann_OS_04_10.pdf Zugriff: 25. Februar 2012 [Ste11] Constraint-Based Model Refactoring STEIMANN, FRIEDRICH Proceeding MODELS'11 Proceedings of the 14th international conference on Model driven engineering languages and systems Springer-Verlag Berlin, Heidelberg ©2011 Oktober 2011 http://www.feu.de/ps/pubs/MoDELS2011.pdf Zugriff: 25. Februar 2012 5 Literaturverzeichnis [Stu11] Automatisierte Analyse von C#-Programmen für das Pull-Up-Field-Refactoring in MonoDevelop STUMPF, KEVIN Bachelorarbeit Fernuniversität in Hagen September 2011 http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeit-stumpf.pdf Zugriff: 18. Februar 2012 [TKB03] Refactoring for Generalization using Type Constraints (TIP, FRANK), (KIEZUN, ADAM), (BÄUMER, DIRK) OOPSLA '03 Proceedings of the 18th annual ACM SIGPLAN conference on Object oriented programing, systems, languages, and applications ACM New York, NY, USA ©2003 November 2003 [Wei11] Grundlagen der Theoretischen Informatik A WEIHRAUCH, K. Studienbrief zum Kurs 1657 Fernuniversität in Hagen 2011 59 60 5 Literaturverzeichnis A Inhalt der beiliegenden DVD 61 A Inhalt der beiliegenden DVD .\Bachelorarbeit Enthält diese Bachelorarbeit im PDF-Format. .\MutationTesting\annotation Enthält die Datei mutation.annotation.refacola, welche die zur Auszeichnung von Constraint-Regeln benötigte Negatable-Annotation zur Verfügung stellt. .\MutationTesting\doc Enthält die Quellcode-Dokumentation des Mutation Testing Frameworks. .\MutationTesting\example Enthält das Java-Programm, das als Beispiel in Abschnitt 3.4 verwendet wird, sowie die JUnit Tests, welche die aus dem Java-Programm generierten Mutanten erkennen. .\MutationTesting\src Enthält den Quellcode des Mutation Testing Frameworks, das im Rahmen dieser Arbeit entwickelt wurde. .\Refacola\src Enthält den Quellcode von Refacola (Stand: 19. Februar 2012). .\Refacola_MutationTesting\release Enthält das Mutation Testing Framework sowie Refacola (Stand: 19. Februar 2012) in Form von Plugins zur Verwendung in Eclipse. Die in Refacola definierten Constraint-Regeln für Java wurden um die in Abschnitt 3.4 verwendeten Constraint-Regeln ergänzt. Die Constraint-Regel MWA_noNameCollisionTopLevelTypes wurde auskommentiert. .\Refacola_MutationTesting\src Enthält den Quellcode des Mutation Testing Frameworks und den Quellcode von Refacola (Stand: 19. Februar 2012). Dem Java-Projekt de.feu.ps.refacola.lang.java wurde im Paket refacola die Datei mutation.annotation.refacola beigelegt, welche die für das Mutation Testing nötige Negatable-Annotation umfasst. Die Datei Ruleset.refacola im selben Paket wurde um die Import-Anweisung zur Einbindung der Negatable-Annotation ergänzt und die Constraint-Regel MWA_noNameCollisionTopLevelTypes wurde auskommentiert. Desweiteren wurden die in Abschnitt verwendeten Constraint-Regeln hinzugefügt. 62 A Inhalt der beiliegenden DVD .\Eclipse Enthält Eclipse 3.6 (Helios Service Release 2, 32-bit) für Windows mit allen Plugins, die für die Entwicklung des Mutation Testing Frameworks und von Refacola (Stand: 19. Februar 2012) benötigt werden. Der von Eclipse maximal zu nutzende Arbeitsspeicher wurde auf 512 MB erhöht. Darüber hinaus sind sowohl das Mutation Testing Framework als auch Refacola in dieser Version von Eclipse integriert. Die in Refacola definierten Constraint-Regeln für Java wurden um die in Abschnitt 3.4 verwendeten Constraint-Regeln ergänzt. Die Constraint-Regel MWA_noNameCollisionTopLevelTypes wurde auskommentiert. .\Eclipse\jdk\doc Enthält die API-Dokumentation vom JDK 6. .\Eclipse\jdk\src Enthält den Quellcode vom JDK 6. .\JDK Enthält das JDK 6 Update 27 (32-bit) für Windows. B Installation und Konfiguration 63 B Installation und Konfiguration Integration des Mutation Testing Framework in ein bestehendes Refacola Projekt Kopieren Sie die Datei mutation.annotation.refacola aus dem Ordner .\MutationTesting\annotation in das Paket /refacola des Projekts de.feu.ps.refacola.lang.java.refacola. Importieren Sie alle Projekte aus dem Ordner .\MutationTesting\src in Ihren Eclipse Workspace, der die Refacola Projekte enthält. Führen Sie im Projekt de.feu.ps.refacola.lang.java das Script /GenerateJavaApiJava.mwe2 aus. Bestätigen Sie eine evtl. erscheinende Fehlermeldung. Erstellen Sie danach alle Projekte. Konfiguration des Mutation Testing Frameworks und von Refacola für die Entwicklung unter Eclipse Importieren Sie alle Projekte aus der Datei .\Refacola_MutationTesting\src\Refacola_MutationTesting_src.zip in Ihren Eclipse Workspace. Führen Sie im Projekt de.feu.ps.refacola.dsl das Script /de.feu.ps.refacola.dsl/GenerateRefacola.mwe2 aus. Bestätigen Sie eine evtl. erscheinende Fehlermeldung. Führen Sie anschließend im Projekt de.feu.ps.refacola.factbase das Script /de.feu.ps.refacola.factbase/GenerateFactBase.mwe2 aus. Bestätigen Sie eine evtl. erscheinende Fehlermeldung. Führen Sie dann noch im Projekt de.feu.ps.refacola.lang.java das Script /GenerateJavaApiJava.mwe2 aus. Bestätigen Sie eine evtl. erscheinende Fehlermeldung. Erstellen Sie nun alle Projekte. 64 B Installation und Konfiguration Mutation Testing Framework als Eclipse-Plugin installieren Kopieren Sie den Inhalt des Ordners .\Refacola_MutationTesting\release in den Unterordner \plugins Ihrer Eclipse Installation. Starten Sie anschließend Eclipse neu. Konfiguration von Eclipse Es wird empfohlen den von Eclipse maximal zu nutzenden Arbeitsspeicher auf 512 MB oder höher zu setzen, um OutOfMemoryErrors zu vermeiden. Starten Sie dazu Eclipse mit folgendem Befehl: eclipse -vmargs -Xms512m -Xmx512m -XX:PermSize=512m -XX:MaxPermSize=512m Festlegen der maximalen Anzahl zu berechnender Lösungen pro Mutation im Mutation Testing Framework Die maximale Anzahl zu berechnender Lösungen pro Mutation kann im Klassen-Konstruktor des Activators im Projekt de.feu.ps.refacola.mutation.ui eingestellt werden. Die Voreinstellung macht keine Beschränkung bezüglich der Anzahl zu berechnender Lösungen pro Mutation. 65 C Benutzungsanleitung für den Refacola-Entwickler C Benutzungsanleitung für den Refacola-Entwickler Zur Auszeichnung der deklarativ definierten Constraint-Regeln verwendet das Mutation Testing Framework die in der Datei mutation.annotation.refacola vorhandene Negatable-Annotation. Sollte die im Eclipse-Plugin de.feu.ps.refacola.mutation vorhandene Klasse MutationCatalyst Fehler-Marker enthalten, die darauf zurückzuführen sind, dass der Typ Negatable nicht aufgelöst werden kann, dann wurde die Java-Annotation Negatable noch nicht generiert. Bitte stellen Sie sicher, dass sich die Datei mutation.annotation.refacola im Eclipse-Plugin de.feu.ps.refacola.lang.java im Paket refacola befindet und führen Sie das MWE2-Script GenerateJavaApiJava.mwe2 im gleichnamigen Eclipse-Plugin aus. Bevor Constraint-Regeln ausgezeichnet werden können, muss die Negatable-Annotation über die in Listing 26 angegebene Import-Anweisung der Refacola-Datei (Ruleset.refacola), welche die Constraint-Regeln beinhaltet, bekannt gemacht werden. import "mutation.annotation.refacola" Listing 26 Importieren der Negatable-Annotation Nun können beliebige Constraint-Regeln durch anheften der Negatable-Annotation für das Mutation Testing ausgezeichnet werden (s. Listing 27). Da das Mutation Testing Framework auf die aus den Constraint-Regeln generierten Java-Klassen zugreift, werden Änderungen für das Framework erst sichtbar, wenn anschließend das MWE2-Script GenerateJavaApiJava.mwe2 ausgeführt wird. @Negatable OOPSLA_f0_nameBasedAccess for all r: Java.NamedReference E: Java.NamedEntity do if Java.binds(r, E) then r.identifier = E.identifier end Listing 27 Auszeichnung einer Constraint-Regel Während des Mutation Testing werden die von den ausgezeichneten Constraint-Regeln erzeugten Constraints negiert, um auf diese Weise Mutanten zu generieren. Die Qualität der Mutanten sowie die Laufzeit des Mutation Testing hängen maßgeblich von der Wahl der Constraint-Regeln ab, die ausgezeichnet wurden. Es wird deshalb empfohlen nur solche Constraint-Regeln mit der NegatableAnnotation zu markieren, dessen Constraints das Bindungsverhalten des Programms sicherstellen. Ihre Negierung kann dann dazu führen, dass die generierten Mutanten ein verändertes Programmverhalten aufweisen. 66 C Benutzungsanleitung für den Refacola-Entwickler Es ist zu beachten, dass Constraint-Regeln nur als Ganzes ausgezeichnet werden können. Eine direkte Selektion einzelner Constraints ist nicht möglich. Sollte dies jedoch gewünscht sein, können Constraint-Regeln wie in Listing 28 und Listing 29 dargestellt aufgeteilt werden, um dann die resultierenden Constraint-Regeln einzeln auszuzeichnen. Die gezeigte Aufteilung der Constraint-Regeln hat keinerlei Einfluss auf die generierten Constraints und berührt vorhandene Refaktorisierungen nicht. OOPSLA_f0_memberAccess for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then rec.owner = ref.owner, Java.sub*(rec.inferredDeclaredType, M.owner) end Listing 28 Komplexe Constraint-Regel OOPSLA_f0_memberAccess_1 for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then rec.owner = ref.owner end OOPSLA_f0_memberAccess_2 for all rec: Java.DeclaredTypedEntityReference ref: Java.MemberReference M: Java.Member do if Java.binds(ref, M), Java.receiver(ref, rec) then Java.sub*(rec.inferredDeclaredType, M.owner) end Listing 29 Elementare Constraint-Regeln D Benutzungsanleitung für den Entwickler 67 D Benutzungsanleitung für den Entwickler Die Mutation Testing View (s. Abbildung 14) kann nach erfolgreicher Installation über den "Show View" Dialog von Eclipse aufgerufen werden. Öffnen Sie dazu das Menü "Window" in der Menüleiste von Eclipse und wählen Sie im SubMenü "Show View" den Eintrag "Other..." aus. Öffnen Sie dann die Kategorie "General", in der die View als "Mutation Testing" gelistet und auswählbar ist. Abbildung 14 Mutation Testing View, vor Ausführung Unter "Java Project" kann das Java-Programm ausgewählt werden, aus dem Mutanten generiert werden sollen. Bitte stellen Sie sicher, dass alle Änderungen am Programm gespeichert wurden, dieses keine Fehler enthält und somit kompilierbar ist. Die in der Testbasis ("Test Base") enthaltenen JUnit Tests werden während des Mutation Testing verwendet, um festzustellen, ob sie das geänderte Programmverhalten der Mutanten erkennen. Unterstützt werden JUnit Tests, die auf JUnit 3.8.x und JUnit 4 basieren. Andere JUnit Tests werden ignoriert. Für jeden generierten Mutanten wird die gesamte Testbasis, d. h. alle in ihr enthaltenen unterstützten JUnit Tests, ausgeführt. Es wird daher empfohlen nur eine Testbasis zu verwenden, die kurz laufende Tests enthält. Bitte stellen Sie auch bei der Testbasis sicher, dass alle Änderungen gespeichert wurden und sie keine Fehler enthält. Darüber hinaus darf keiner der in der Testbasis enthaltenen Tests fehlschlagen. Dies würde das Ergebnis des Mutation Testing verfälschen. Bitte beachten Sie, dass auch eine Testbasis verwendet werden kann, die keine Tests enthält. In diesem Fall können keine Mutanten erkannt werden. Während des Mutation Testing werden viele Änderungen in kurzer Zeit am Programm durchgeführt. Das kann zu Problemen führen, wenn Dateien des zu verwendenden Programms in einem EclipseEditor geöffnet sind. Stellen Sie daher bitte sicher, dass während des Mutation Testing keine Datei des ausgewählten Java-Programms im Editor geöffnet ist. 68 D Benutzungsanleitung für den Entwickler Über die gerade ausgeführten Operationen werden Sie während des Mutation Testing mittels der in Eclipse vorhandenen Fortschrittsanzeigen (Fenster, s. Abbildung 15) informiert. Darüber hinaus ist es Ihnen möglich, den Mutation Testing Durchlauf abzubrechen. Abbildung 15 Fortschrittsanzeige während des Mutation Testing Um die Fortschrittsanzeige in Abbildung 15 nach dem Ausblenden ("Always run in background") beim nächsten Mutation Testing Durchlauf wieder anzeigen zu lassen, öffnen Sie im Menü "Window" der Menüleiste von Eclipse den Eintrag "Preferences". Unter dem Punkt "General" kann die getroffene Wahl ("Always run in background") rückgängig gemacht werden. Nach einem Mutation Testing Durchlauf werden die Ergebnisse in der Mutation View dargestellt. Dabei steht jeder Eintrag für einen Mutanten. Sie können über einen Doppelklick auf ein Ergebnis den Mutanten in einem Eclipse-Editor aufrufen. Das manipulierte Programmelement wird entsprechend hervorgehoben. Zum Vergleich kann zwischen ursprünglichen Programm und Mutanten beliebig gewechselt werden. Über das Kontextmenü eines Ergebnisses kann wieder das ursprüngliche Programm in einem Eclipse-Editor angezeigt werden. Abbildung 16 Mutation Testing View, nach Ausführung