Zurück

Was sind Mutation Tests und warum sollten wir uns damit beschäftigen?

von Dr. Carsten Otto

Um zu verstehen, welchen Mehrwert man aus Mutation Tests ziehen kann, betrachten wir ein fiktives Code-Beispiel:

public class UserService {
	[...]
	public boolean logout(User user) {
		notificationService.unsubscribe(user);
		user.deleteLoginToken();
		return true;
	}
	[...]
}

Unsere Tools sagen uns zu diesem Code, dass die Methode durch wenigstens einen Test abgedeckt ist. Wer sagt uns aber, ob der Test gut und hilfreich ist? Ist der Test vielleicht der folgende?

@Test
void logout() {
	assertThat(service.logout(USER)).isTrue();
	verify(notificationServiceMock).unsubscribe(USER);
}

Dieser Test ist "grün" und sorgt für eine perfekte Testabdeckung der "logout"-Methode. Leider hilft der Test aber nur bedingt dabei, die Arbeit der Entwickler:innen zu unterstützen, da der "deleteLoginToken"-Aufruf im Test nicht erwähnt wird. Ich kann also aus Versehen den Aufruf von "user.deleteLoginToken" entfernen, ohne dass der Test mich darauf aufmerksam machen würde.

Als Entwickler:in wünsche ich mir im Projekt Tests, die möglichst schnell und möglichst genau auf Fehler hinweisen. Jede "kritische" Änderung am Code sollte zu einem Testfehler führen, der mich eben auf jene Änderung hinweist und klarstellt, warum die Änderung so nicht in Ordnung war. Mit dieser Information kann ich dann

  • die Änderung hinterfragen,
  • eventuelle Fehler korrigieren oder
  • den Test an die neue Wahrheit anpassen.

Eine inhaltliche Code-Änderung, die über Refactoring hinausgeht, sollte immer zu einem Testfehler führen. Und genau dabei hilft die Idee der Mutation Tests.

Ohne manuellen Aufwand kann man beispielsweise das Tool "PIT" (https://pitest.org/) nutzen, um mit Hilfe der vorhandenen Tests Warnungen zu generieren. Hierfür verändert (mutiert) das Tool den Code, führt anschließend die Tests aus und überprüft, ob ein Test die Mutation erkennt und deshalb scheitert. Hierbei ist es also explizit gewünscht, dass Tests scheitern! Wenn trotz einer Mutation kein Test scheitert, wird der Benutzer auf diese Mutation hingewiesen.

Im Beispiel oben würde PIT als eine von mehreren Mutationen den Aufruf zu "user.deleteLoginToken" entfernen und die Tests mit folgendem Code ausführen:

public class UserService {
	[...]
	public boolean logout(User user) {
		notificationService.unsubscribe(user);
		return true;
	}
	[...]
}

Da der oben gezeigte Test nur auf den Rückgabewert "true" und die Interaktion mit "notificationService" prüft und deshalb nicht scheitert, produziert PIT eine entsprechende Warnung. Als Entwickler:in kann man dann überlegen, ob der in der Mutation entfernte Aufruf vielleicht gar nicht mehr nötig ist oder ob man den gewünschten Seiteneffekt in einem Test überprüfen kann.

Das Entfernen eines Methodenaufrufs ist nur eine von vielen unterstützten Mutationen. Einige Beispiele:

  • In Berechnungen werden Konstanten und Rechenoperationen ersetzt
  • Aus "x > y" wird  "x >= y"
  • Ein Stück Code, das nur unter bestimmten Bedingungen ausgeführt wird, wird dank einer Mutation immer (oder nie) ausgeführt
  • Code, der eine Menge (Set) zurückgibt, gibt nach der Mutation immer nur eine leere Menge zurück

Die einzelnen Arbeitsschritte (Mutation, Test ausführen, Testergebnisse überprüfen, Report generieren) werden automatisch durchgeführt. Hierbei wird auch analysiert, welche Tests überhaupt ausgeführt werden müssen, um einen Fehler erkennen zu können: Wenn ein Test die mutierte Zeile nicht abdeckt, muss man diesen Test im Kontext der Mutation nicht noch einmal ausführen.

Der Mehrwert dieser Mutation Tests hängt stark vom Projekt und der Risikobewertung ab. Generell ist es hilfreich, gute (und schnelle) Tests zu haben. Mit etwas Fleißarbeit können Mutation Tests aber auch dabei helfen, eine suboptimale Testlandschaft nach und nach aufzuräumen.

Da zusätzlich zu den Mutationen auch viele Tests mehrfach ausgeführt werden müssen, empfiehlt es sich, die Mutation Tests am Ende eines Build-Jobs laufen zu lassen. Die Laufzeit ist grob eine Größenordnung langsamer als die der dazugehörigen Unit Tests. Ein Projekt, das die Unit Tests in ca. 30 Sekunden laufen lassen kann, braucht also grob mehrere Minuten für die dazugehörigen Mutation Tests.

 

Zurück