Mutationstests in C#: Code kaputt machen, um ihn besser zu machen

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Wenn Tests bestehen, der Code aber trotzdem „kaputt“ ist

\n\n

Stellen Sie sich vor: Sie haben Modultests für ein C#-Projekt geschrieben, dotnet test zeigt grün, die Codeabdeckung (Code Coverage) liegt bei 85%. Das Team ist zufrieden, der CI/CD-Lauf läuft einwandfrei. Aber einen Monat später tritt in der Produktion ein Fehler auf, den die Unit-Tests eigentlich hätten abfangen sollen. Kommt Ihnen das bekannt vor?

\n\n

Das Problem ist, dass die Codeabdeckung (Code Coverage) eine trügerische Metrik ist. Sie zeigt, welche Codezeilen berührt wurden, prüft aber nicht die Qualität der Prüfungen selbst. Sie können 100% Abdeckung haben, aber keinen einzigen sinnvollen Assert. Genau hier kommt Mutationstests in C# ins Spiel – eine Methode, die Ihren Code „kaputt macht“, um zu überprüfen, wie gut Ihre Tests Fehler erkennen können.

\n\n

In diesem Artikel werden wir uns ansehen, was Mutation Testing ist, wie man es mit Stryker.NET in .NET-Projekte integriert und warum es ein Must-have für jeden ernsthaften C#-Entwickler ist.

\n\n

Was ist Mutationstests (Mutation Testing)?

\n\n

Mutationstests ist eine Methode zur Bewertung der Testqualität, die auf der Einführung kleiner, absichtlicher Änderungen (Mutationen) im Quellcode des Programms basiert. Jede dieser geänderten Codeversionen wird als Mutant bezeichnet.

\n\n

Die Logik ist einfach:

\n
    \n
  • Ihr Produktionscode wird genommen.
  • \n
  • Automatisch werden Mutanten generiert – zum Beispiel wird der Operator + durch - ersetzt, die Bedingung if (x > 5) wird zu if (x >= 5) geändert und true wird zu false.
  • \n
  • Ihre Unit-Tests werden gegen jeden Mutanten ausgeführt.
  • \n
  • Wenn ein Test fehlschlägt, ist der Mutant „getötet“ (killed). Ein guter Test.
  • \n
  • Wenn ein Test bestanden wird, hat der Mutant „überlebt“ (survived). Ein schlechtes Zeichen: Ihr Test hat die Logikänderung nicht bemerkt.
  • \n
\n\n

Das Ziel ist es, einen hohen Prozentsatz getöteter Mutanten (normalerweise 80%+) zu erreichen. Dies ist eine echte Garantie dafür, dass Ihre Tests das Verhalten prüfen und nicht einfach nur über den Code „laufen“.

\n\n

Warum ist Code Coverage nicht ausreichend?

\n\n

Betrachten wir ein einfaches Beispiel in C#. Angenommen, wir haben eine Methode, die prüft, ob eine Zahl gerade ist:

\n\n
public class MathUtils\n{\n    public bool IsEven(int number)\n    {\n        return number % 2 == 0;\n    }\n}
\n\n

Und einen „idealen“ Test mit 100% Abdeckung:

\n\n
[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n    var utils = new MathUtils();\n    var result = utils.IsEven(4);\n    Assert.IsTrue(result);\n}
\n\n

Der Test ist grün, die Abdeckung 100%. Aber was ist, wenn jemand versehentlich (oder böswillig) die Methode in return number % 2 == 1; ändert? Wird unser einziger Test fehlschlagen? Nein! Er prüft die Zahl 4 (4 % 2 == 0, aber jetzt gibt die Methode false zurück), der Assert wird fehlschlagen – der Mutant wird getötet. Alles gut? Moment.

\n\n

Was ist, wenn der Code in return number % 2 != 1; geändert wird? Logisch ist es dasselbe, aber Mutationstests erzeugen einen Mutanten: return number % 2 != 0;. Unser Test mit der Zahl 4 wird immer noch bestanden (4 % 2 != 0 — false, die Methode gibt false zurück, aber der Assert erwartet true — der Test schlägt fehl). Hier ist alles in Ordnung. Aber stellen Sie sich vor, wir haben den Test so geschrieben:

\n\n
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n    var utils = new MathUtils();\n    Assert.DoesNotThrow(() => utils.IsEven(4));\n}
\n\n

Die Abdeckung ist immer noch 100%, aber der Assert prüft das Ergebnis nicht! Jetzt wird jeder Mutant überleben. Mutationstests zeigen sofort, dass Ihre Tests leere Hüllen sind.

\n\n

Tools für Mutationstests in C#

\n\n

Das wichtigste und praktisch standardmäßige Tool für .NET ist Stryker.NET. Es ist ein Open-Source-Framework, das sich leicht in jedes C#-Projekt integrieren lässt. Es unterstützt xUnit, NUnit und MSTest.

\n\n

Andere Tools (weniger verbreitet):

\n
    \n
  • VisualMutator – ein veraltetes Plugin für Visual Studio.
  • \n
  • Nester
  • \n

Blogs

Buchempfehlungen