Blog

Mutationstests in C#

Michael Contento

Michael Contento

Aktualisiert Oktober 15, 2025
8 Minuten

Das Problem

Seien wir ehrlich, Softwareentwicklung ist schwierig. Es ist eine höchst kreative Aufgabe, die vollständig in "nicht-physischen Welten" wie unserem Verstand und in IT-Geräten stattfindet. Als physische Menschen leben wir in der realen Welt, wir erleben die reale Welt, wir atmen und sprechen die reale Welt. Die unmittelbare Folge ist, dass wir aus all den kleinen Dingen, die passieren können, lernen. Aufgrund früherer Erfahrungen wissen wir, dass wir mit einer frischen Tasse Kaffee vorsichtig sein müssen, da sie ziemlich heiß sein könnte.

Bei Software ist das ein bisschen anders. Sicher, auch wir sammeln mit der Zeit Erfahrungen. Wir lernen, Situationen zu antizipieren und Wissen aus der Vergangenheit wiederzuverwenden, aber wir können früheres "Wissen aus der realen Welt" nicht einfach auf unseren Beruf übertragen. Das ist ein großer Unterschied zu anderen Berufen wie Tischler oder Maler, bei denen unser menschliches Urteilsvermögen in der realen Welt etwas leichter angewendet werden kann. Ich meine, man muss kein erfahrener Schreiner sein, um zu überprüfen, ob ein Stuhl seine Aufgabe erfüllt, einen Menschen zu tragen.

Das Testen oder Überprüfen von Software hingegen fügt unserem Konstrukt in der nicht-physischen Welt eine weitere Komplexitätsebene hinzu. Wenn Ihr primärer Code bereits recht komplex ist, wie können wir dann unsere Unit-Tests einfach halten? Das Refactoring unseres primären Codes wird mit einem guten Satz von Unit-Tests einfach, zugegeben. Aber wie können wir unsere Unit-Tests refaktorisieren? Sind wir sicher, dass unsere Tests nach einer Umstrukturierung das gleiche Maß an Vertrauen/Sicherheit bieten? Können wir sicher sein, dass sich unsere Tests immer mit dem Primärcode weiterentwickeln? Vielleicht wurden in der Vergangenheit versehentlich ein paar kleine Fehlerbehebungen ohne einen begleitenden Unit-Test durchgeführt. Wer weiß das schon?

Qualität messen

Wie können wir also die Qualität unserer Unit-Tests bewerten? Sicher, ein einfaches Bauchgefühl wäre einfach, aber auch sehr subjektiv und nichts, was wir in unsere CI-Pipeline einbauen könnten. Das Sammeln von Codeabdeckungsmetriken während der Ausführung unserer Unit-Tests ist hingegen etwas, das wir leicht in unsere CI-Pipeline einbauen könnten und das uns einige objektive Zahlen liefern würde. Aber wie sollen wir diese Zahlen interpretieren?

Abdeckungsmetriken sagen Ihnen nur, wie viel Prozent Ihres Codes ausgeführt wurden. Nicht, welcher Prozentsatz der Geschäftslogik hinter diesen Codezeilen ausgewertet wurde!

Und in Kombination mit Abdeckungsmetriken hört oder liest man schnell Hinweise wie "70% Abdeckung ist genug, denn 100% sind den Aufwand nicht wert". Warum sollten wir nicht nach 100% streben? Warum müssen wir bei der Interpretation dieser Zahlen vorsichtig sein?

Gibt es denn keine besseren Metriken? Vielleicht etwas mit einer hohen Entwicklererfahrung, das sich auf umsetzbare Dinge statt auf theoretische Werte konzentriert? Wir Entwickler wollen Dinge verbessern und uns nicht über Zahlen streiten!

Mutationstests zur Rettung

Normalerweise verwenden wir Unit-Tests, um unseren primären Code zu bewerten, aber mit Mutationstests stellen wir die Dinge auf den Kopf! Wir mutieren unseren primären Code, um das bestehende Verhalten aktiv zu brechen oder umzukehren, und testen, ob unsere Unit-Tests in der Lage sind, diese brechende Änderung zu erkennen. Wenn die Unit-Tests erfolgreich sind, wissen wir, dass das ursprüngliche Verhalten nicht richtig von einem Test abgedeckt wurde und wir unsere Tests in dieser Hinsicht überarbeiten/schärfen müssen.

Das hat den großen Vorteil, dass es sehr praxisnah ist. Denn das Ergebnis eines Mutationstestlaufs lautet immer: "Wenn ich diesen Teil Ihres primären Codes zerstöre, beschweren sich keine Unit-Tests!". Es gibt keine abstrakte Zahl zu interpretieren. Kein weichgespültes "70% ist gut genug". Mutationstests können entweder Stellen finden, an denen Ihre Unit-Tests Lücken aufweisen oder nicht. So einfach ist das.

Wie können wir dies in C# nutzen?

Stryker.NET ist hier, um zu helfen

Um die Dinge konkreter zu machen, lassen Sie uns mit einem kurzen Stück Code beginnen:

public class Calculator { public int Multiply(int a, int b) { return a * b; } }

Ja, dies ist eine sehr einfache Klasse und wirklich für diesen Artikel gemacht. Dieses Stück Code dient nur dazu, die Idee und die Verwendung von Stryker.NET zu vermitteln. 1 und Mutationstests im Allgemeinen zu vermitteln. Auch in diesem Szenario versuchen wir, gute Entwickler zu sein, denen die Qualität am Herzen liegt. Daher haben wir auch einen entsprechenden Unit-Test, der wie folgt aussieht:

public void Multiply_test(int a, int b, int c) { var calc = new Calculator();

var actual = calc.Multiply(a, b);

Assert.AreEqual(c, actual); }

Hier haben wir ein einfaches Stück Code und einen Unit-Test, der es ausführt. Unser Unit-Test ist grün, also ist alles in Ordnung, oder? Wenn wir unsere Code Coverage-Metrik von vorhin anwenden würden, wären wir bei 100%! Großartig.

Lassen Sie uns sehen, was Stryker.NET über unser Projekt denkt. Dazu müssen wir schnell das Kommandozeilen-Tool dotnet-stryker über installieren:

$ dotnet tool install -g dotnet-stryker

Wie Sie sehen können, ist Stryker.NET ein einfaches NuGet-Paket, das global auf Ihrem Rechner (wie wir es gerade getan haben) oder projektbezogen installiert werden kann. Welchen Weg Sie bevorzugen, ist letzten Endes eine Frage der Test- und/oder Projektkonvention. Nach der Installation können wir Stryker.NET gegen unseren Code ausführen und die Ergebnisse sehen:

$ cd pfad/zu/ihrer/lösung/ordner

$ dotnet stryker

Sie haben nicht erwartet, dass es so einfach ist, oder? Stryker.NET versucht sein Bestes, um eine hochwertige Entwicklererfahrung zu gewährleisten und übernimmt so viel wie möglich. Es stehen mehrere Befehlszeilenoptionen zur Verfügung, mit denen Sie das Standardverhalten ändern können, z. B. das Filtern von Mutationen auf eine Teilmenge Ihrer Dateien, das Ändern der Ausgabestufe, die Auswahl der Art der zu erstellenden Berichte und vieles mehr. Aber für den Moment können wir es bei den Standardeinstellungen belassen und den HTML-basierten Bericht öffnen, der standardmäßig erstellt wird:

Hier sehen wir, dass Stryker.NET unseren ursprünglichen Code mutiert hat, indem er die Multiplikation durch eine Division ersetzt hat, und unsere Unit-Tests waren immer noch grün! Oder in den Worten von Stryker.NET: der generierte Mutant konnte überleben (keine fehlgeschlagenen Unit-Tests, die ihn erwischten).

Das ist richtig, denn unser Unit-Test hat nur mit einem begrenzten Parametersatz getestet! Wir können die Mutation selbst durchführen, die Geschäftslogik völlig umkehren und unser Test überwacht uns nicht. Die Verbesserung unseres Unit-Tests ist so einfach wie das Hinzufügen einer weiteren Parametervariante:

[TestCase(1, 1, 1)] [TestCase(4, 2, 8)] public void Multiply_test(int a, int b, int c) { var calc = new Calculator();

var actual = calc.Multiply(a, b);

Assert.AreEqual(c, actual); }

In der nächsten Runde von dotnet stryker würde dieser Mutant nicht mehr überleben, und wir verbesserten aktiv die Qualität unseres Tests!

Dinge, die Styker.NET mutiert

Wir haben gesehen, dass Stryker.NET in der Lage war, unsere Multiplikation mit einer Division zu mutieren, und die Frage ist nun: Was kann Stryker.NET noch mutieren? Denn letztendlich bestimmen die Menge und Vielfalt dieser Mutationen das Spektrum und die Qualität der erzeugten Mutanten.

Das ist die gute Nachricht: Die Anzahl der verfügbaren Mutationen in Stryker.NET ist überwältigend und umfasst eine Vielzahl von Kategorien:

Kategorie Original Mutiert
Arithmetische Operatoren+-
Gleichheitsoperatoren!===
Logische Operatorenundoder
Boolesche Literalewahrfalsch
Zuweisungsanweisungen+=-=
Initialisierungenneue int[] { 1, 2 }new int[] {}
Unäre Operatoren-var+var
Operatoren aktualisierenvar++var--
LINQ-MethodenErste()Letzte()
String-Operatoren"foo"""
Bitweise Operatoren<<>>
Mathematik-OperatorenFloor()Plafond()
Null-Koaleszenz-Operatorena ?? bb
Regex-Operatorenabc{5,}abc{4,}
Entfernen von Mutatorenbrechen (einfach entfernt)

Wie Sie sehen können, ist die Liste riesig! Und ich habe nur ein Beispiel aus jeder Kategorie ausgewählt. Für eine vollständige Liste aller unterstützten Mutationen sollten Sie einen Blick in die Dokumentation werfen, die sehr detailliert ist. Wenn Sie Fragen haben, finden Sie in der Dokumentation immer eine Antwort - nicht nur auf eine Liste aller Mutationen.

Mutationswert als KPI (Key Performance Indicator)

Stryker.NET erstellt Mutanten und zählt, wie viele von ihnen entkommen konnten oder von unseren Tests gefangen wurden. Diese Informationen lassen sich zu einer einzigen Punktzahl zusammenfassen: Die Mutationsbewertung.

Die Berechnung ist einfach, denn wir dividieren einfach die Anzahl der gefangenen Mutanten durch die Gesamtzahl der von uns erstellten Mutanten. Angenommen, wir haben 120 Mutanten erschaffen und nur 5 von ihnen haben überlebt, dann erhalten wir einen Mutationswert von 92% (je höher, desto besser).

Diese einfache Bewertung ist auch in den verschiedenen Berichtsformaten sichtbar, die Stryker.NET erzeugen kann. Im Standard-HTML-Bericht, den wir zuvor verwendet haben, können wir dies als unkomplizierten Leitfaden verwenden, um Klassen zu finden, die mehr Escape-Mutanten und damit weniger effektive Unit-Tests aufweisen.

Fazit

Mutationstests stellen die Welt auf den Kopf und verwenden den primären Code, um die Qualität/Vollständigkeit/Robustheit unserer Unit-Tests zu bewerten. Dies geschieht, indem eine Armee von Mutanten (logisch invertierte Varianten unseres primären Codes) erzeugt wird, die von unseren vorhandenen Unit-Tests abgefangen werden müssen. Jede Mutante, die entkommt (und keinen fehlgeschlagenen Unit-Test auslöst), weist auf einen Teil der Logik in unserem primären Code hin, für den es keinen verifizierenden Unit-Test gibt.

Letztendlich ist diese Methodik für mich als Softwareentwickler sehr praxisnah und schafft umsetzbare Erkenntnisse. Wenn Stryker.NET irgendwann nicht mehr in der Lage ist, Mutanten zu erzeugen, die unsere Unit-Tests überleben, stehen die Chancen gut, dass auch mein zukünftiges Ich beim nächsten Refactoring nicht versehentlich Mutanten erzeugen kann. Und das ist es, was mir wirklich am Herzen liegt: Vertrauenswürdige Unit-Tests.

Hier herunterladen


  1. https://stryker-mutator.io/︎

Verfasst von

Michael Contento

Contact

Let’s discuss how we can support your journey.