Mutationstests (auf Französisch: tests de mutation) habe ich kürzlich diesen Begriff entdeckt, der einen Prozess beschreibt, der in der Lage ist, Lücken in Komponententests zu erkennen, die über die Codeabdeckung hinausgehen. Heute stelle ich Ihnen diesen Ansatz vor, der darin besteht, diese Tests durch Manipulation des Codes durchzuführen.

Unit-Tests
In Anbetracht dessen, dass die Nützlichkeit von Unit-Tests gut etabliert ist, wird dieses Thema interessant, wenn Sie ein getestetes Projekt entwickeln, egal wie wichtig die Codeabdeckung ist.
Die Unit-Tests ermöglichen das Aufzeigen möglicher Regressionen, die durch eine Änderung des Codes verursacht werden. Wenn die Tests das Programm validieren, bedeutet dies theoretisch, dass in der Anwendung alles korrekt funktioniert. Als erstes und oft einziges Vertrauensmaß verwenden wir Code Coverage. Je näher diese Metrik an 100 % herankommt, desto mehr versichert sie uns, dass keine Regression übersehen wird. Leider bleibt diese Behauptung theoretisch.
Obwohl die Tests für die Validierung einer qualitativen Anwendung unerlässlich sind, ist es schwierig, ihre Relevanz zu demonstrieren oder auch nur einzuschätzen.
Codeabdeckung und Fallabdeckung
100 % Code Coverage bedeutet nicht 100 % validierter Code, sondern nur 100 % dieses Codes bei Bestehen der Tests ausgeführt, mehr nicht.
Die Codeabdeckung (Zeile, Anweisung, Verzweigung usw.) misst nur, welcher Code durch die Tests ausgeführt wurde, ohne Garantie für die Erkennung von Fehlern. Es kann nur Code identifizieren, der noch ungetestet ist.
Das Testen ohne Assertion ist das offensichtliche Beispiel, da der Code zwar ausgeführt, aber nicht wirklich getestet wird. Glücklicherweise bleibt dieses Szenario selten, am häufigsten trifft man auf Code, der teilweise von der Testsuite getestet wurde. Eine Suite, die nur teilweise Code testet, der dennoch alle seine Zweige ausführen kann.
In einigen Fällen ist die Codeabdeckung kein Indikator für den Schutz. Hier ist ein einfaches Beispiel:
function isAdult(user) {
return user.age >= 18;
}
Angenommen, wir möchten das Alter eines Benutzers überprüfen. Wir werden den folgenden Code schreiben, um sicherzustellen, dass es sich um einen Hauptcode handelt.
Um diesen Code zu testen, können wir es mit 12 und 38 als Eingabe versuchen. Diese Aktion würde ausreichen, um diesen Code zu 100 % abzudecken.
Das Ergebnis wäre dasselbe, wenn wir bei diesem Tippfehler in unserem Code 18 nicht als Mehrheit betrachten würden:
function isAdult(user) {
return user.age > 18;
}
…oder wenn wir den Wert nur 12 Jahre getestet haben, oder noch schlimmer, wenn wir die Behauptung in unserem Test weggelassen haben.
Der Mutationstest wird tatsächlich erkennen können, ob jede Aussage sinnvoll getestet ist. Es ist das Standardmaß für alle anderen Deckungsarten.
Andere Probleme im Code
Nehmen wir dann an, dass wir keinen unnötigen Code in unserer Anwendung haben wollen. Tatsächlich ist jeder ungetestete Teil eine Quelle potenzieller Fehler oder sogar zusätzlicher Komplexität, wenn dies nicht unbedingt erforderlich ist.
Hier ist der Grund, warum Mutationstests eine großartige Möglichkeit sind, die Relevanz eines solchen Codes zu testen:
if (someVariable !== null && someVariable.hasValue()) {}
Müssen wir den Wert überprüfen “null“? Wurde die Bedingung aus Gewohnheit hinzugefügt? Es könnte bedeuten, dass wir uns der Variablen nicht sicher sind.“einigeVariable“ und würde eine weitere Analyse rechtfertigen. Wir können nicht tiefer gehen, ohne es zu merken. Dabei helfen uns auch Mutationstests.
Mutationstests: Was ist das?
Um Fehler in unseren Unit-Tests zu erkennen, gibt es eine Lösung: Mutationstests.

Diese Technik gibt mehr Vertrauen in unsere Tests. Mutationstests sind ein ziemlich einfaches Konzept. Sein Prinzip besteht darin, den Quellcode zu manipulieren, indem er geändert wird, um zu überprüfen, ob die zugehörigen Tests entsprechend fehlschlagen. Fehler (oder Mutationen) werden automatisch in unseren Code gesät, und dann werden Tests durchgeführt. Wenn die Tests fehlschlagen, wird die Mutation getötet. Wenn die Tests bestanden werden, hat die Mutation überlebt. In diesem Fall bedeutet dies, dass die Tests nicht der Komplexität des Codes entsprechen und einen oder mehrere seiner Aspekte ungetestet lassen. Dann kann die Qualität unserer Tests am Prozentsatz der abgetöteten Mutationen gemessen werden.
Mit anderen Worten, wir führen die Komponententests auf automatisch modifizierten Versionen des Codes aus. Wenn sich der Anwendungscode ändert, sollte dies zu anderen Ergebnissen führen und dazu führen, dass Komponententests fehlschlagen. Wenn ein Komponententest in dieser Situation nicht fehlschlägt, kann dies auf einen Fehler in der Testsuite hinweisen.
Hier sind die Schritte, um dies zu erreichen:
- Führen Sie die übliche Testsuite aus, um zu überprüfen, ob alle Tests grün bestehen.
- Ändern Sie einige Teile des getesteten Codes, bevor Sie die Testsuite erneut ausführen.
- Stellen Sie sicher, dass die Tests nach dem Ändern (Mutieren) des getesteten Codes wie erwartet fehlgeschlagen sind.
Wiederholen Sie die Schritte 2 und 3 so lange wie mögliche Mutationen verbleiben.
Nehmen wir ein konkretes Beispiel: Stellen Sie sich eine Mutante als zusätzliche Klasse mit nur einer Änderung gegenüber dem ursprünglichen Code vor. Dies kann die Änderung eines logischen Operators in einer if-Klausel sein, wie unten gezeigt:
if( a || b ) {…} => if( a && b ) {…}
Der Nachweis und die Zurückweisung einer solchen Modifikation durch existierende Tests wird als Töten einer Mutante bezeichnet. Mit einer perfekten Testsuite würde kein Klassenmutant überleben. Das Erstellen aller möglichen Mutanten ist jedoch ressourcenintensiv, weshalb es in realen Szenarien nicht möglich ist, diesen Ansatz manuell zu erreichen.
Glücklicherweise gibt es Tools, mit denen man Mutanten im Handumdrehen erstellen und automatisch alle Tests für jeden einzelnen durchführen kann. Die Transformationserstellung basiert auf einer Reihe von Mutationsoperatoren, die aufgerufen werden, um typische Programmierfehler aufzudecken. Der Mutationsoperator, der zum Modifizieren des obigen Codes verwendet wird, wird Bedingungsoperator genannt.

In der Praxis
Diese Technik besteht also aus zwei Teilen: der Generierung von Mutanten, dann deren Eliminierung.
Mutantengenerierung ist der Schritt zum Generieren von Mutantenklassen aus Quellklassen. Zunächst benötigen wir den Business Code, anhand dessen wir die Relevanz unserer Tests bewerten wollen. Wir nehmen dann eine Pool von möglichen Mutationen, wobei eine Mutation eine Modifikation des Quellcodes ist, wie beispielsweise die Aktion des Ersetzens eines Operators durch einen anderen.
Hier sind einige Beispiele:
- + wird zu –
- * wird /
- >= wird ==
- wahr wird falsch.
- Löschen einer Anweisung
- usw.
Wir können einen arithmetischen Ausdruck e zu |e| modifizieren (ABS), ändere einen relationalen arithmetischen Operator in einen anderen (ROR), ändere einen arithmetischen Operator in einen anderen (AOR), ändere einen booleschen Operator in einen anderen (COR), ändere einen booleschen/arithmetischen Ausdruck durch Hinzufügen von − oder ¬ ( UOI), Ändern Sie einen Variablennamen durch einen anderen, ändern Sie einen Variablennamen durch eine Konstante desselben Typs, ändern Sie eine Konstante durch eine andere Konstante desselben Typs …
Die eigentliche Generierung besteht darin, alle Anweisungen des Codes durchzugehen und für jede festzustellen, ob Mutationen anwendbar sind. Wenn dies der Fall ist, führt jede Mutation zu einer neuen Mutante.
Für folgende Aussage:
if (a > 8) { x = y+1 }
Wir können die folgenden Mutanten in Betracht ziehen:
if (a < 8) { x = y+1 }
if (a ≥ 8) { x = y+1 }
if (a > 8) { x = y-1 }
if (a > 8) { x = y }
Dieser Prozess kann schnell ressourcenintensiv werden. Wenn der zu mutierende Code eine große Anzahl von Anweisungen enthält und das " Pool » der möglichen Mutationen signifikant ist, dann steigt die Zahl der erzeugten Mutanten sehr schnell an.
Sobald der Generierungsprozess der Mutanten abgeschlossen ist, werden die Mutanten bis zum nächsten Schritt gespeichert: Eliminierung!
Für den zweiten Teil des Prozesses haben wir viele Mutanten generiert, die wir nicht durch die Tests führen wollen; Das Ziel wird sein, so viele wie möglich zu eliminieren. Dabei wird unsere Waffe die Verbesserung von Unit-Tests sein.
Bilanz
Für eine bestimmte Mutante gibt es zwei mögliche Ergebnisse, entweder sind die Tests immer grün oder mindestens einer von ihnen ist rot geworden.

Normalerweise möchten wir, dass die Tests grün sind. Aber in diesem Zusammenhang suchen wir nach Rot. Wie wir bereits gesehen haben, soll jede Mutante mindestens einen der Komponententests nicht bestehen. Wenn mindestens einer der Tests fehlschlägt, beweist dies, dass sie in der Lage sind, Codeänderungen zu erkennen und somit möglichen Fehlern vorzubeugen. Wenn andererseits alle Tests grün bleiben, überlebt die Mutante, sodass sie für die Augen unserer Tests unsichtbar blieb.
Eine überlebende Mutante ist also das Zeichen für einen fehlenden Test!
Einschränkungen
Die vollständige Analyse unseres Codes kann mühsam sein. Wie wir gesehen haben, kann die Zahl der Mutanten sehr schnell zunehmen.
In einer ersten Phase können wir zum Beispiel 6000 Mutanten erzeugen. Während der zweiten Testphase werden mehr als 98 % von ihnen eliminiert, wobei der Prozentsatz je nach vorheriger Qualität Ihrer Tests variiert. Wir haben noch 150 bis 200 Mutanten übrig.
Eine manuelle Analyse von jedem von ihnen ist zeitaufwändig. Darüber hinaus sind unsere Unit-Tests nicht allein für deren Überleben verantwortlich. Es kann eine "äquivalente Mutante" auftreten: eine Mutante, die die Syntax des Quellcodes modifiziert, ohne seine Semantik zu ändern. Dieser Mutantentyp verhindert, dass ein Komponententest ihn erkennt.
while(...) {
index++;
if (index == 10)
break;
}
Zum Beispiel eine Mutation von " == »Bis« <= erzeugt eine äquivalente Mutante. Dieses Beispiel hat die gleiche Ausgangsbedingung der Schleife.
Die Voranalyse der Codeabdeckung, die Erstellung von Mutanten im laufenden Betrieb und alle erforderlichen Tests nehmen viel Zeit in Anspruch. Beispielsweise erhöht ein Code mit 350 Tests die Ausführungszeit um das Vierfache gegenüber einem normalen Durchlauf.
Angesichts dieser Zahlen und aus praktischen Gründen können Mutationstests nicht so häufig durchgeführt werden wie Unit-Tests. Daher ist es wichtig, einen geeigneten Workflow zu finden, der den besten Kompromiss in puncto Effizienz bietet. Für große Softwaresysteme könnte dies bedeuten, dass Mutationstests auf nächtliche Läufe beschränkt würden.
Bevor Sie sie implementieren, müssen Sie sich in einem fortgeschrittenen Qualitätsansatz befinden. Die Tests müssen in den Mittelpunkt der Entwicklung gestellt werden, um zu umfangreiche Ergebnisse für die Analyse zu vermeiden. Wenn die Codeabdeckung jedoch ihre Grenzen erreicht hat, kann dies ein guter Ansatz zum Experimentieren sein. Leider wirken die aktuellen Tools nicht industrialisiert genug.
Mutationstests und Javascript
Mutationstests sind viel bekannter und werden in der Welt von Java oder in PHP verwendet. Seit 2016 gibt es jedoch dank Stryker Mutator eine Möglichkeit, Mutationstests in JavaScript durchzuführen. Es gibt auch Grunt-Mutationstests, von denen der Großteil des Quellcodes gerade auf Stryker migriert wird.
Hier der Link zum Github: http://stryker-mutator.github.io
Fazit
Dieser Artikel war eine kurze Einführung in Mutationstests. Wir haben uns mit Testmutanten befasst, die direkte Beziehung zwischen der Mutantenrate und der Qualität einer bestehenden Testsuite gewürdigt und die Korrelation mit der Codeabdeckung beobachtet.
Da die Codeabdeckung keine sehr zuverlässige Metrik ist, Mutationstests sind eine schnelle und einfache Möglichkeit, die Zuverlässigkeit von Unit-Tests zu messen. Wir werden Mutationstests fördern, wo es um ein echtes Problem geht: den Geschäftscode.
Alles in allem scheint der Mutationstest eine nette Ergänzung zu einer Reihe von Qualitätssicherungsinstrumenten zu sein., basierend auf automatisierten Tests. Diese Praxis ist in JavaScript ziemlich neu und noch unbekannt. Es wird interessant sein, die Meinungen und Rückmeldungen von fortgeschrittenen Benutzern zu lesen.
Aurélie Ambal, (@Souvir) JS Handwerkerin @JS-Republic
[actionbox color=“default“ title=““ description=“JS-REPUBLIC Training wird von Datadock referenziert.
Alle unsere Schulungen finden Sie auf unserer Website training.ux-republic.com:
- UX-Design
- Agil
- JavaScript
” btn_label=“Unsere Trainings“ btn_link=“http://training.ux-republic.com“ btn_color=“primary“ btn_size=“big“ btn_icon=“star“ btn_external=“1″]
