View
3
Download
0
Category
Preview:
Citation preview
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: MarkusGrothoff@gmx.de
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 Hilfs-
mittel benutzt. Wörtliche und sinngemäße Zitate wurden als solche gekennzeichnet.
Hagen, 28. Februar 2012
Markus Grothoff
Inhalt
1 Einleitung ....................................................................................................................................... 1
2 Grundlagen .................................................................................................................................... 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
3 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 ent-
sprechend 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 Funktionali-
tä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 entgegen-
gewirkt 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 glei-
chen 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 Tes-
ten 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 Imple-
mentierung 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 an-
ders verhalten als das ursprüngliche Programm. Das veränderte Verhalten wird als fehlerhaft betrach-
tet. 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äti-
gen, dass in dem entsprechenden Fall die Testabdeckung hoch genug war, um das Fehlverhalten fest-
zustellen. Sollten Mutanten die Testbasis passieren, kann das ein Indiz dafür sein, dass die Testabde-
ckung nicht ausreichend ist, sollte der Mutant tatsächlich ein dem ursprünglichen Programm abwei-
chendes 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 sicht-
bare 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 Tes-
ting 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 Komponen-
ten von Refacola aufgesetzt werden. Mit dem Mutation Testing Framework sollen aus Java-
Programmen Mutanten generiert und auf Basis einer vom Entwickler ausgewählten Testbasis ausge-
wertet 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 die-
nen 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ögli-
che 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 Pro-
grammverhalten 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 wer-
den können.
In Abschnitt 2.5 werden einige für das Mutation Testing wichtige Bestandteile von Refacola vorge-
stellt 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 Entwick-
lung 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 entgegenzuwir-
ken wird Software verschiedenen Tests unterzogen. Einen ersten Schritt stellen dabei die Unit Tests
dar, da sie einzelne Softwaremodule – in objektorientierten Programmiersprachen sind dies die Klas-
sen – 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 kon-
figuriert. 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 Annah-
me 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 Schrei-
ben 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() aufge-
rufen 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 Annah-
me 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 Pro-
grammverhaltens 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 Feh-
ler 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 korrek-
ten Programm werden automatisiert Änderungen an der Codebasis des Programms vorgenommen. Die
manipulierten Programme werden anschließend gegen eine Testbasis ausgeführt, welche das fehlerhaf-
te 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 Pro-
gramme 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 Pro-
gramms 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 Programmier-
sprachen 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 Manipu-
lation des Bindungsverhaltens zu erwähnen, welches sowohl vom deklarierten Typ als auch vom Ob-
jekt, das referenziert wird, abhängt. Im Allgemeinen decken die durch das Mutation Testing vorge-
nommenen Ä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 Um-
binden 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 wich-
tig, da sich während der Entwicklung und Wartung unerwünscht Änderungen am Programm einschlei-
chen 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 Ver-
halten 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 Zusammen-
hang 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 komple-
xen 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 Mutan-
ten 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 redu-
zieren, indem lediglich wenige Änderungen am Programm vorgenommen werden.
Die grundsätzliche Vorgehensweise beim Mutation Testing zeigt Algorithmus 1. Die Testbasis beste-
hend 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 Pro-
gramm vor Ausführung des Mutation Testing alle Tests erfüllt. Ansonsten kann es zu falschen Ergeb-
nissen 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 constraint-
basierte Verfahren zur Generierung von Mutanten (s. Abschnitt 2.4).
2 Grundlagen 7
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 Testabde-
ckung 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, in-
dem er den Mutanten eingehend untersucht und diesen mit dem ursprünglichen Programm ver-
gleicht.
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 herauszufil-
tern. 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 Ent-
wicklers 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 Pro-
grammcodes 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> imple-
mentieren, 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 Prob-
lem entgegenzuwirken, muss der Code entwicklungsbegleitend gepflegt, überarbeitet und umstruktu-
riert 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 ange-
passt, 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 Testabde-
ckung 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 Methodenauf-
ruf 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.
2 Grundlagen 9
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 Entwick-
ler bei Refaktorisierungen. Dabei sollte sich der Entwickler darauf verlassen können, dass die von
ihnen durchgeführten Refaktorisierungen das Programmverhalten nicht beeinflussen. Fehlerhafte Re-
faktorisierungen 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) ent-
deckt 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 wer-
den können. Gibt es für ein Constraint mindestens eine Belegung der Constraint-Variablen mit Wer-
ten, sodass es erfüllt ist, wird das Constraint als lösbar bezeichnet. Im Allgemeinen werden viele un-
tereinander 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 zuei-
nander 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 systema-
tisch 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(x) > accessibility(y)
accessibility(y) ≥ accessibility(z)
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 Programmele-
menten, wie z. B. Methoden und Attribute, eines Programms auf Constraints, die von Constraint-
Regeln erzeugt werden, abgebildet. Grundsätzlich lassen sich zwei Arten von Constraints unterschie-
den.
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 Pro-
gramm 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 vor-
genommen. 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 Programmele-
mente 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 Um-
stä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 au-
tomatisiert durchgeführte Änderungen an der Code-Basis eines anderen Programms aus diesem her-
vorgegangen ist, mit dem Ziel zu prüfen, ob diese Änderungen am Programm von einer Testbasis er-
kannt 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 Zu-
stand oder das Verhalten einer oder mehrerer Klassen beziehen. Es werden mehrere Arten von Mutan-
ten unterschieden. Die Aufteilung orientiert sich dabei an [Bär10].
Gültige Mutanten
Mutanten, die den syntaktischen und semantischen Regeln der zugrundeliegenden Programmier-
sprache 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üngli-
chen Programm, weisen aber ein von außen beobachtbares Verhalten auf, das dem des ursprüngli-
chen Programms gleicht. Es wird davon ausgegangen, dass ein Vergleich des Programmverhaltens
nur zwischen kompilierbaren und zumindest von einer Testbasis ausführbaren Programmen mög-
lich 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 {
public void log(Object obj) {
System.out.println(obj);
}
public void log(String str) {
System.out.println(str);
}
}
class Foo {
void doSomething() {
Logger myLogger =
new Logger();
myLogger.log(new Object());
}
}
class Logger {
public void log(Object obj) {
System.out.println(obj);
}
private void log(String str) {
System.out.println(str);
}
}
class Foo {
void doSomething() {
Logger myLogger =
new Logger();
myLogger.log(new Object());
}
}
Listing 6 Ursprüngliches Programm (links) und äquivalenter Mutant (rechts)
2 Grundlagen 13
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 dy-
namische 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 ur-
sprünglichen Programm und damit auch ein äquivalenter Mutant sein. In jedem Fall ist er kompi-
lierbar 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 {
boolean add(Object value) {
return false;
}
}
class ReadonlyStack extends Stack {
boolean add(Object value) {
throw new
RuntimeException();
}
}
class Foo {
void doSomething() {
Stack myStack = new Stack();
myStack.add(new Object());
}
}
class Stack {
boolean add(Object value) {
return false;
}
}
class ReadonlyStack extends Stack {
boolean add(Object value) {
throw new
RuntimeException();
}
}
class Foo {
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 Ver-
halten sich von dem des ursprünglichen Programms unterscheidet. Ob sich ein relevanter Mutant tat-
sächlich anders verhält, muss vom Entwickler untersucht werden, denn ein Vergleich der von ver-
schiedenen Programmen berechneten Funktionen ist nicht möglich [Wei11]. Die manuelle Untersu-
chung 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 Pro-
gramm 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, sicher-
gestellt. 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 Re-
faktorisierung 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 bindungser-
haltenen Constraints systematisch ausgewählt und negiert werden, um möglichst viele relevante Mu-
tanten 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 Langua-
ge, 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 Refaktorisie-
rungen ermöglichen im Allgemeinen nur die Umstrukturierung von Code innerhalb einer Program-
miersprache. Eines der Ziele von Refacola ist die Unterstützung von Refaktorisierungen über die
Grenzen einer Programmiersprache und sogar über verschiedene Programmierparadigmen (objektori-
entiert, funktional, ...) hinaus. Durch die Verwendung des constraint-basierten Ansatzes werden feh-
lerhafte Refaktorisierungen, wie sie in bestimmten Situationen von den in Eclipse integrierten Werk-
zeugen 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 Constraint-
Solvers, 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 for-
muliert 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.
2 Grundlagen 15
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 objekt-
orientierten 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 Program-
miersprache 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 Pro-
grammelement 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 ob-
jektorientierten 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 Entity <: ENTITY
abstract AccessibleEntity <: Entity { accessibility }
abstract StaticMember <: Member
abstract 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, Mehrfach-
vererbung 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
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 Constraint-
Regeln. Sie basieren auf der ebenfalls in Refacola definierten Sprachdefinition der jeweiligen Pro-
grammiersprache 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 Pro-
grammiersprachen an die Variablen gebunden. Erfüllen die Programmelemente die Bedingung, wer-
den die im Aktionsteil definierten Constraints für die in den Variablen gebundenen Programmelemen-
te 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 Sicht-
barkeit public besitzt.
AccessibleEntity
+accessibility: AccessModifier
AccessModifier<<enumeration>>
+private+package+protected+public
Entity Member
StaticMember
TypedStaticMember
TypedMember
2 Grundlagen 17
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. Eben-
so 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ül-
lung 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 Refa-
cola als Änderung der Werte von Eigenschaften ausgewählter Programmelemente betrachtet werden.
Die Refaktorisierung "Methode umbenennen" würde der identifier Eigenschaft des Programm-
elements, das die ausgewählte Methode repräsentiert, den vom Entwickler gewünschten Namen zu-
weisen. 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. Spezifi-
zierungen 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 Kompo-
nente nachfolgend genannt wird, als Plugin für die Entwicklungsumgebung Eclipse realisiert. Im Vor-
feld 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 Frame-
works in mehrere Eclipse-Plugins eine bessere Wiederverwendung und eine saubere Trennung zwi-
schen 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 verschiede-
nen 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 Schrit-
te 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 ver-
wendet 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 wer-
den 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 Implemen-
tierung der Benutzungsoberfläche.
Das Mutation Testing Framework soll dem Refacola-Entwickler die Möglichkeit geben, die in Refaco-
la bereits definierten und zukünftig noch hinzukommenden Constraint-Regeln für das Mutation Tes-
ting 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 Constraint-
Regeln 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 jet-
zigen 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 Refaco-
la bereits definierten und zukünftig noch zu definierenden Constraint-Regeln deklarativ auszuzeich-
nen. Dadurch werden indirekt die von den ausgezeichneten Constraint-Regeln erzeugten Constraints
bestimmt, die im Laufe eines Mutation Testing Durchlaufs negiert werden. Constraints werden unab-
hä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 vorhan-
den. 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 Ände-
rungen 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 betrach-
tet.
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 Mu-
tantengenerierung 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 Sprachspezifika-
tion 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 Programmele-
mente 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üh-
rung ab. Unit-Tests werden umso häufiger ausgeführt, je einfacher sie gestartet werden können. Ei-
ne manuell zu konfigurierende Testumgebung hat immer das Potenzial zusätzliche Fehler zu erzeu-
gen 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 verwen-
dende 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 ledig-
lich 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 Mu-
tanten 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 so-
fort 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 Ent-
wickler 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 Unit-
Tests auszuarbeiten. Dabei sollte er die Möglichkeit haben, beliebig zwischen Mutanten und ur-
sprü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 Tren-
nung und besseren Wiederverwendung zugutekommt. Die Aufteilung von Anwendungskern und Be-
nutzungsoberfläche in zwei Plugins sowie deren Namensgebung orientieren sich an den Eclipse-
Plugins, in die Refacola aufgeteilt ist. Abbildung 2 stellt die Unterteilung des Mutation Testing Fra-
meworks 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, detail-
lierter 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 ge-
neriert für beliebige in Java geschriebene Programme Mutanten auf Basis der in Refacola ausge-
zeichneten 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 An-
wendungskerns 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 Mu-
tation 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 Java-
Programms 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 Ergeb-
nis 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 er-
füllt sind. Ein Mutation Testing Durchlauf kann für größere Programme eine längere Zeit in An-
spruch nehmen. Die Mutation Testing View teilt dem Entwickler daher den Fortschritt kontinuier-
lich 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. Ab-
schnitt 3.2.3 geht näher auf die Benutzungsoberfläche ein.
3 Implementierung 23
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 Mu-
tation 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, ausgela-
gert (s. Abschnitt 3.2.2). Zur Erfüllung seiner Aufgabe nutzt der Anwendungskern verschiedene Kom-
ponenten 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 Fort-
schritt 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. Mit-
tels 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äß Java-
Konvention eine OperationCanceledException geworfen. Diese Technik wird von der Benut-
zungsoberflä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 Vorbe-
dingungen (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 Sprach-
spezifikation 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ür-
de aus einem nicht kompilierbaren Programm nur nicht kompilierbare und damit ungültige Mutan-
ten 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 Änderun-
gen an der Testbasis nicht verlorengehen.
Die Testbasis muss kompilierbar sein
Entsprechend dem Java-Programm, aus dem Mutanten generiert werden sollen, muss das Java-
Programm, 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 unbe-
dingt 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 ha-
ben, darf kein Test fehlschlagen. Eine Testbasis, die keine JUnit Tests beinhaltet, erfüllt diese Vor-
bedingung 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 Refa-
cola ü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 er-
mittelten Änderungen Change-Objekte aufzubauen, mit dessen Hilfe Eclipse Änderungen am Java-
Programm vornehmen kann.
3.2.1.3 Initialisierung des Kontextes einer Refaktorisierung
Die Klasse RefactoringProblem stellt den Kontext einer Refaktorisierung dar. Sie verwaltet einer-
seits alle beteiligten Programmelemente und Fakten. Diese Informationen werden über einen
IProgramInfoProvider bereitgestellt, der schon im vorherigen Schritt für die Erzeugung der Fak-
tenbasis zuständig war. Sowohl die von ihm ermittelten Programmelemente als auch die Fakten wer-
den 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 standard-
mäßig zum Einsatz kommt. Dem RefactoringProblem wird eine ISolverFactory injiziert, da-
mit es Zugriff auf den zur Lösung eines Constraint-System zu verwendenden ISolver hat. Anderer-
seits muss spezifiziert werden, welche Eigenschaften von Programmelementen sich im Laufe der Su-
che 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 bindungserhal-
tenen Constraints realisiert wird. Nun müssen aber Änderungen von Eigenschaften der Programmele-
mente 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 se-
mantisch 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 Eigen-
schaften der Programmelemente nicht von Bedeutung. Ob bei einem Mutanten eine Methode umbe-
nannt 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 Programm-
elementen. Dass deren scheinbar willkürliche Belegung von Werten immer zu kompilierbaren Pro-
grammen 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 Programmelemen-
te 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 Tes-
ting verwendet wird und daher das RefactoringProblem nicht auf einige ausgewählte Programm-
elemente 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 Zu-
sammenhang 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 Erzeu-
gung 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 Negie-
rung 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
3 Implementierung 27
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 nach-
folgenden 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 entspre-
chenden Constraint-Regeln zusammengefasst (s. Abbildung 3). Die Constraint-Regeln werden zur
Darstellung innerhalb der Mutation Testing View verwendet. Jedes IConstraintChange wird wie-
derum in eine IMutation verpackt, die ihrerseits auf das zur Zeit einzige Constraint-System verweist.
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.
IConstraint<<interface>>
IConstraintChange<<interface>>
+originalConstraint: IConstraint {ReadOnly}+changedConstraint: IConstraint {ReadOnly}+appliedRule: IRule {ReadOnly}
*2
IMutation<<interface>>
+originalConstraints: Set<IConstraint> {ReadOnly}+constraintChange: IConstraintChange {ReadOnly}+refactoringProblem: IRefactoringProblem {ReadOnly}
+mutateConstraints(): Set<IConstraint>
1
1
Set<IConstraint><<interface>>
*1
Constraint-System
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 Constraint-
Systems 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 Mutati-
on Zugriff auf ein ConstraintChange, das ein ursprüngliches Constraint und seine negierte Form
verwaltet.
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 Constraint-
System 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 Re-
ferenz 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. Die-
se Situation kann ausgenutzt werden, um das Mutation Testing Framework für eine parallele Verarbei-
: IMutation
Original : Set<IConstraint>
: IMutation
Original1 : IConstraint Negated1 : IConstraint
Original2 : IConstraint
Negated2 : IConstraint
: IConstraint : IConstraint : IConstraint
3 Implementierung 29
tung anzupassen. In der jetzigen Implementierung findet ein Mutation Testing Durchlauf noch sequen-
ziell statt (s. Abschnitt 3.5).
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 Än-
derungen, 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 ver-
schiedener Programmelemente unterscheiden. Für das Mutation Testing sind diese Lösungen unterei-
nander 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-
: IMutation
Original : Set<IConstraint>
: IMutation
Original1 : IConstraint Negated1 : IConstraint
Original2 : IConstraint
Negated2 : IConstraint
: IConstraint : IConstraint : IConstraint
Mutated : Set<IConstraint>
Erzeugt
30 3 Implementierung
rend bei einer andere Lösung der Methodenname nicht verändert wird. Im Gegensatz zu Refaktorisie-
rung sind diese Unterschiede für das Erkennen von Mutanten durch eine Testbasis nicht von Bedeu-
tung. 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 verwen-
det. Der IProgramInfoProvider aus Abschnitt 3.2.1.2 bietet eine einfache Möglichkeit unter Nut-
zung des Manipulators aus IChangeSets Changes zu erstellen.
In dem Mutation Testing Framework kapselt ein ICodeRewriter die Komplexität der in den vorhe-
rigen 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 Änderun-
gen, die aus dem ursprünglichen Programm einen Mutanten entstehen lassen, wieder rückgängig ma-
chen. Wie in Abschnitt 3.1 erläutert, ist dies wichtig, damit der Entwickler beliebig zwischen Pro-
gramm 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 Programm-
elemente 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 aus-
fü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 Testba-
sis 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ön-
nen 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 Kor-
rektheit 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 be-
stimmt werden.
Die Testbasis selbst sollte nur über solche Tests verfügen, die nicht manuell konfiguriert werden müs-
sen, 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 kom-
plett ausgeführt wird, können lang laufende Tests die Laufzeit des Mutation Testing stark beeinflus-
sen. 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 Wiederverwen-
dung 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, auszu-
führen und liefert ein Ergebnis des Testdurchlaufs, das u. a. Informationen über die Anzahl der fehlge-
schlagenen 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 JUnit-
Framework verwechselt werden sollte. Mit dessen Hilfe kann aus einem IJavaElement, ein Java-
Programm 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.
Abbildung 6 Abstrakter Programmfluss zwischen Client und Server
Der jetzt folgende Ablauf orientiert sich an dem in Abbildung 6 dargestellten Sequenzdiagramm. Da-
bei wurde von der konkreten Implementierung abstrahiert, um sich auf das Wesentliche zu beschrän-
ken. 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
: IJUnitClient : JUnitServer : IVMRunner
1 : run()
2 : run() 3 : main()
<<create>>
4 : openSocket() 5 : runTests()
6 : connectSocket()
7 : testResult
3 Implementierung 33
eine Verbindung zwischen Client und Server aufgebaut wird. Der Server benötigt einerseits Verbin-
dungsdetails 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 Para-
meter 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 String-
Parameter 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 serialisier-
bare Klasse im JUnit Test Runner ersetzt. Der Inhalt des JUnit Testresultats wird einfach in das hinzu-
gefü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]. Standard-
mäß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 Entwicklungs-
umgebung 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 moda-
len 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 wer-
den. Es wird auf einzelne Funktionen und Besonderheiten eingegangen. Darunter fallen auch die Vor-
bedingungen, 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 Ent-
wurfsmuster 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 Bil-
dern 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 Mu-
tanten 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 Kontext-
menü 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 enthalte-
nen 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 Besonder-
heiten 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 nachfolgen-
den 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 ste-
henden Java Projekte basiert wie bei der Combobox "Java Project" auf einem
IResourceChangeListener. Das Enthalten sein von JUnit Tests wird in den zur Auswahl ste-
henden Java Projekten nicht überprüft. Ein Java Projekt ohne JUnit Tests wird beim Mutation Tes-
ting wie eine Testbasis betrachtet, die keine fehlschlagenden Tests enthält. So bekommt der Ent-
wickler 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. Verletzun-
gen 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 zu-
stä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 ur-
sprünglichen Programms, dann kann er von keinem JUnit Test entdeckt werden,
oder die Testabdeckung ist nicht ausreichend, um ihn zu entdecken. Was von bei-
den zutrifft, kann der Entwickler durch einen Vergleich des Mutanten mit dem ur-
sprü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 Muta-
tion Testing View zur Verfügung stehenden Schaltflächen in der Werkzeugleiste sind nach Ver-
wendungsart gruppiert und abhängig vom Zustand der View aktiviert oder deaktiviert.
Run
Startet einen Mutation Testing Durchlauf, in welchem aus dem unter "Java Project" ausge-
wä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 Ec-
lipse-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.
3 Implementierung 37
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 ausge-
fü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 Status-
leiste 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 Quasi-
Standard 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 Änderun-
gen 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 Mini-
mum. 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 kommuni-
zieren.
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 Entwurfs-
musters auf Ebene von Interfaces dar. Den in der Benutzungsoberfläche verwendeten Interfaces und
3 Implementierung 39
Klassen sind jeweils nach den Bestandteilen von MVP-VM Suffixe angehängt. Ein IMutationPre-
senter entspricht damit einem Presenter im MVP-VM. Das Model ist nicht Bestandteil der Benut-
zungsoberflä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 be-
schrieben.
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 Test-
basis, 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 Ta-
belle, 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. Da-
ten, 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. Kom-
plexe 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-
IMutationView<<interface>>
IMutationPresenter<<interface>>
IMutationViewModel<<interface>>
IJavaProjectViewModel<<interface>>
ISolutionViewModel<<interface>>
1
1
1*
* *
*
*
1
*
IMutationSolution<<interface>>
*
1
IJavaProject<<interface>>
*
1
IJavaProjectWatcher<<interface>>
*
1
IMutationSolutionCompilation<<interface>>
*
0..1
IMutationService<<interface>>
Data Binding
Data Binding
Data Binding
IView<T extends IViewModel><<interface>>
IViewModel<<interface>>
40 3 Implementierung
ten dargestellten Werte zur Verfügung stellt. Ähnliches gilt auch für die Daten in den Combobo-
xen.
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 Frame-
works 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 ei-
nes 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 Än-
derung des Wertes einer Eigenschaft des ViewModels. Die Änderung wird dem Data Binding Fra-
mework 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 Com-
boboxen 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 wur-
den in eine abstrakte Klasse ausgelagert, die von allen ViewModels erweitert wird. Die Verwen-
dung 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 ver-
wendet wird. Im Mutation Testing Framework wurden den Interfaces der ViewModels benannte
Konstante hinzugefügt, deren Werte den Namen der Eigenschaften entsprechen. Nach der Umbe-
nennung einer Eigenschaft muss dann nur noch der Wert der Konstanten an einer Stelle im Inter-
face des ViewModels angepasst werden.
Model
Alle sich am unteren Rand befindlichen Interfaces in Abbildung 11 stellen Abstraktionen von Mo-
dels dar und befinden sich zusammen mit ihren Standardimplementierungen in anderen Eclipse-
Plugins. 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 Hintergrund-
farbe der Zeilen von der der Tabelle abweicht (s. Abbildung 12). Die Benutzungsoberfläche des Muta-
tion Testing Frameworks verwendet je nach Ergebnis der Auswertung des Mutanten eine andere Hin-
tergrundfarbe für eine Zeile. Andernfalls könnte die Hintergrundfarbe der Tabelle mit denen der einge-
fä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 auszeich-
nen möchte. Bei der Ausführung des Mutation Testing wird für jedes Constraint, das von einer ausge-
zeichneten 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 gleich-
zeitig 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üng-
lichen Programm anders verhalten. Dies ist nicht garantiert und hängt von der konkreten Imple-
mentierung im Programm ab. Auf jeden Fall sind die generierten Mutanten gültig. Die Auszeich-
3 Implementierung 43
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 wer-
den 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 aufge-
teilt werden, von denen jede nur eine der zwei Arten von Constraints erzeugt. Dadurch hat der Refaco-
la-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 elementa-
ren 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 import-
Anweisung bekannt gemacht wurde, kann die Negatable-Annotation an Constraint-Regeln ange-
hä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
3 Implementierung 45
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 Constraint-
Regeln generiert werden, verfügen, falls in Refacola zum Zeitpunkt der Generierung die Constraint-
Regeln ausgezeichnet waren, über die Java Annotation Negatable, welche an die Klasse selbst ange-
hä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 ge-
sammelt. 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 wer-
den dann negierte Constraints abgeleitet. Ein ursprüngliches Constraint, seine negierte Form, die zu-
gehö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 verwen-
det werden. Für die Erstellung eines mutierten Constraint-Systems sind die in den Mutationen verwal-
teten Constraint-Regeln nicht weiter relevant. Sie werden aber zur Darstellung in der Benutzungsober-
flä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 Anwen-
dung 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). Methodenpara-
meter, 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 Situati-
onen 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 Metho-
de 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 Su-
perklasse 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 Me-
thode getClassName() der Klasse A wurde in Klasse B überschrieben. Abhängig vom Typ des Ob-
jekts wird in der Methode getName() zur Laufzeit entweder A.getClassName() oder
B.getClassName() aufgerufen. Die Negierung des von der Constraint-Regel aus Listing 19 erzeug-
ten Constraints verlangt, dass die Sichtbarkeit der Methode A.getClassName() auf private redu-
ziert 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 Pro-
gramm 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 {
public String getClassName() {
return "A";
}
public String getName() {
return getClassName();
}
}
class B extends A {
public String getClassName() {
return "B";
}
}
class A {
private String getClassName() {
return "A";
}
public String getName() {
return getClassName();
}
}
class B extends A {
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
3 Implementierung 49
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 mu-
tierten 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 Pro-
grammen führen, vorausgesetzt die Constraint-Regeln sind vollständig und korrekt, würde die Berech-
nung 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 {
public String getClassName()
{
return "A";
}
public String getName() {
return getClassName();
}
}
class B extends A {
public String getClassName()
{
return "B";
}
}
class newA {
private String newgetClassName()
{
return "A";
}
private String newgetName() {
return newgetClassName();
}
}
class B extends newA {
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 Proble-
me. 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 berech-
net 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 gekennzeich-
net 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 {
public String getClassName()
{
return "A";
}
public String getName() {
return getClassName();
}
}
class B extends A {
public String getClassName()
{
return "B";
}
}
class newA {
private String newgetClassName()
{
return "A";
}
String newgetName() {
return newgetClassName();
}
}
class newB extends newA {
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 Muta-
tion 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 Frame-
works entwickelten sich einige Ideen zur Verbesserung der Implementierung. Im Rahmen dieser Ar-
beit 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 Prob-
lem dar, solange der Mutation Testing Durchlauf ordnungsgemäß beendet oder durch den Entwick-
ler abgebrochen wird. In beiden Fällen wird der Quellcode des Programms wieder in seinen vorhe-
rigen Zustand überführt. Wird aber die Ausführung der Entwicklungsumgebung Eclipse abrupt ab-
gebrochen, beispielsweise durch einen Systemfehler, bei dem das Mutation Testing Framework
nicht mehr die Möglichkeit hat die Änderungen zurückzusetzen, verbleibt der Quellcode des Pro-
gramms in seinem geänderten Zustand. Der Entwickler ist dann dafür verantwortlich das Pro-
gramm wiederherzustellen, indem er z. B. die von den Änderungen betroffenen Stellen im Quell-
code 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 Änderun-
gen 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 Durch-
laufs 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 Bezeich-
ner 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 Pro-
grammelemente mehrerer Java-Projekte zu erzeugen. Mit ihrer Hilfe kann ein Constraint-System
aufgebaut werden, das aus den Constraints verschiedener Java-Projekte besteht. Das weitere Vor-
gehen zur Generierung von Mutanten erfolgt analog zu Abschnitt 3.2.1. Der MultiProgramInfo-
Provider 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 Be-
rechnung von Änderungen verschiedener Java-Projekte, weshalb der MultiProgramInfoProvi-
der 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), stel-
len 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 son-
dern ü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 ei-
ner 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 Anfor-
derungen eingehalten werden. Listing 25 zeigt wie eine Constraint-Regel aussehen könnte, die si-
cherstellt, dass JUnit-Test-Klassen für das JUnit 3 Framework die Sichtbarkeit public beibehal-
ten. 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 aufge-
baut. 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 Mehrkernpro-
zessoren 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 wer-
den. In der aktuellen Implementierung des Mutation Testing Frameworks teilen sich die Mutatio-
nen 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 Anferti-
gung von Kopien des Programms würde dieses Problem lösen. Jeder Thread, der zur Parallelverar-
beitung 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 Pro-
gramms gelten. Weiterhin ist zu klären, inwieweit der durch die Kopien zusätzlich benötigte Spei-
cherbedarf 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 Re-
faktorisierung auf. Refacola selbst ist aber nicht auf eine Programmiersprache festgelegt. Sie kann
nicht nur dazu verwendet werden, um Refaktorisierungen innerhalb einer konkreten Programmier-
sprache, sondern auch um Refaktorisierungen zwischen verschiedenen Programmiersprachen zu
definieren und durchzuführen. Während Refacola durch das Hinzufügen weiterer Sprachdefinitio-
nen, Constraint-Regeln und Faktenbasen weitere Programmiersprachen unterstützen kann, bleibt
das Mutation Testing Framework in seiner aktuellen Implementierung auf Java Programme be-
schränkt. Obwohl wo immer möglich Komponenten von Refacola verwendet werden, um weitest-
gehend von einer konkreten Programmiersprache zu abstrahieren, waren Abhängigkeiten zur Java
Programmiersprache nicht gänzlich zu vermeiden. Dies trifft insbesondere auf die Nutzung des JU-
nit 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 wer-
den, 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 Hin-
zufügen weiterer Funktionen können sie unbeabsichtigte Änderungen am Programmverhalten entde-
cken 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ön-
nen.
Beim Mutation Testing werden durch automatisierte Manipulationen an der Code-Basis eines Pro-
gramms 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 Re-
facola vorgestellt. Refacola verwendet die Technik der constraint-basierten Refaktorisierung, um si-
cherzustellen, 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 Eclipse-
View 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 Auswer-
tung von Mutanten. Während einige Funktionen von Refacola unverändert übernommen werden konn-
ten, wie die Generierung der Java-Faktenbasis, mussten andere im Mutation Testing Framework er-
neut 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 4 Zusammenfassung und Fazit
Constraint-Regeln 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ög-
lichst 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 naht-
los 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 Un-
vollständigkeit der Constraint-Regeln hat ebenfalls Auswirkungen auf die Lösungen des Constraint-
Systems. Es kann passieren, dass unterschiedliche Lösungen desselben Constraint-Systems zu unter-
schiedlichen 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 ent-
sprechenden Risiko, dass die durchgeführten Änderungen bei einem Systemfehler nicht mehr zurück-
gesetzt 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 Pro-
gramms 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 59
[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
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 verwende-
ten 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 mu-
tation.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 Vorein-
stellung macht keine Beschränkung bezüglich der Anzahl zu berechnender Lösungen pro Mutation.
C Benutzungsanleitung für den Refacola-Entwickler 65
C Benutzungsanleitung für den Refacola-Entwickler
Zur Auszeichnung der deklarativ definierten Constraint-Regeln verwendet das Mutation Testing Fra-
mework 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 Eclip-
se-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 ausge-
zeichnet wurden. Es wird deshalb empfohlen nur solche Constraint-Regeln mit der Negatable-
Annotation 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 resultie-
renden 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 Sub-
Menü "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 wer-
den 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 ver-
wendet, 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 fehlschla-
gen. 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 er-
kannt 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 Eclipse-
Editor 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 ge-
wechselt 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
Recommended