Mutation Testing mit Refacola

Werbung
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
Herunterladen