Zum Inhalt

Mutationstests, Fuzzing und Property-Tests

Die Tests über die Prüfungen eines Pull Requests hinaus: ob die Tests eine Änderung des Codes bemerken, und Eingaben, die niemand aufgeschrieben hat.

Mutationstests

Jede Woche laufen Tausende kleiner Änderungen des Codes gegen die Tests, ein großes Projekt in Teilen von je einem Job, und die Zusammenfassung zeigt den Anteil, den sie bemerken, und zuerst die Überlebenden, die die Logik ändern, mit ihren Änderungen; ein niedriger Wert ist ein Bericht, nie ein Fehlschlag, aber ein gefallener Wert von main öffnet ein Issue, und ein Lauf, der nichts misst, schlägt fehl, mit den Tests, die ohne jeden Mutanten fehlschlagen; ein Pull Request, der ändert, wie sie laufen, probiert sie aus.

Womit: mutation.yml: mutmut, Infection.

Die Mutationstests zeigen die Zeilen, die die Tests ausführen, aber nicht prüfen: Jeden Samstag ändern sie den Code auf Tausende kleine Weisen, die Mutanten, und führen die Tests aus, die jeden davon abdecken. Ein Mutant, den kein Test bemerkt, überlebt.

Wie die Mutationstests laufen: Jeden Samstag, auf Abruf und für einen Pull Request, der ändert, wie sie laufen, führt der Mutation-Workflow die Mutanten aus. Für Python teilt der Plan den Code in Teile mit etwa gleich vielen Mutanten, und mutmut führt jeden Teil in einem eigenen Job aus, in einer Kopie des Repositorys und ohne die Tests mit der Markierung mutation_unsafe; Infection ändert das PHP einer Nextcloud-App, mit dem Bootstrap ihrer Tests. Die Zusammenfassung zeigt einen Wert und zuerst die Überlebenden, die die Logik ändern, mit ihren Änderungen, das Artefakt bewahrt sie auf und das Dashboard zeigt den Wert. Fällt der Wert des Standard-Branches unter den des Laufs davor, öffnet sich ein Issue, das sich schließt, sobald ein Lauf wieder dort ist. Ein Lauf, der nichts gemessen hat, schlägt fehl und nennt die Tests, die ohne jeden Mutanten fehlschlugen.Wie die Mutationstests laufen: Jeden Samstag, auf Abruf und für einen Pull Request, der ändert, wie sie laufen, führt der Mutation-Workflow die Mutanten aus. Für Python teilt der Plan den Code in Teile mit etwa gleich vielen Mutanten, und mutmut führt jeden Teil in einem eigenen Job aus, in einer Kopie des Repositorys und ohne die Tests mit der Markierung mutation_unsafe; Infection ändert das PHP einer Nextcloud-App, mit dem Bootstrap ihrer Tests. Die Zusammenfassung zeigt einen Wert und zuerst die Überlebenden, die die Logik ändern, mit ihren Änderungen, das Artefakt bewahrt sie auf und das Dashboard zeigt den Wert. Fällt der Wert des Standard-Branches unter den des Laufs davor, öffnet sich ein Issue, das sich schließt, sobald ein Lauf wieder dort ist. Ein Lauf, der nichts gemessen hat, schlägt fehl und nennt die Tests, die ohne jeden Mutanten fehlschlugen.

  • Der Wert ist der Anteil der Mutanten, den ein Test bemerkt hat, unter denen des Codes, den die Tests ausführen: bei mutmut die getöteten unter den getöteten und den überlebenden, bei Infection sein Covered Code MSI. Das Dashboard zeigt ihn als gefangene Mutanten.
  • Die Überlebenden, die die Logik ändern, kommen zuerst: Die Zusammenfassung ordnet jeden Überlebenden von mutmut nach seiner Änderung, wie mutmut show sie ausgibt. Zuerst kommen die, die die Logik ändern, einen Operator, einen Vergleich, eine Zahl oder eine Bedingung, die ein Test bemerken sollte, nach Funktion, die meisten zuerst, jeder mit seiner Änderung; dann eine Tabelle der Funktionen, deren Überlebende None übergeben oder ein Argument auslassen, und eine derer, die nur einen Text ändern, etwa eine Meldung, eine Hilfe oder eine Zeile des Logs. Der Wert der Logik zählt nur die Überlebenden der Logik, und survivors.md im Artefakt mutation enthält die Änderung jedes Überlebenden. Eine Zeile, deren Text kein Test prüfen muss, bekommt # pragma: no mutate, und mutmut lässt sie in Ruhe.
  • Ein großes Projekt läuft in Teilen: Der Job plan schätzt die Mutanten jeder Funktion und teilt den Code in Teile mit etwa 3.000 Mutanten, höchstens acht: ganze Module, und die Funktionen eines Moduls, das größer ist als ein Teil. Jeder Teil ist ein eigener Job, der nur seine Mutanten ausführt, damit ein Projekt mit 10.000 Mutanten innerhalb der sechs Stunden eines Jobs von GitHub fertig wird, und der Job report fügt die Teile zu einem Wert zusammen. Die Variable MUTATION_PARTS des Repositorys setzt die Zahl der Teile anstelle der Schätzung.
  • Ein gefallener Wert öffnet ein Issue: Der Bericht vergleicht einen vollständigen Lauf des Standard-Branches mit dem Lauf davor, aus mutation.json seines Artefakts. Fällt der Wert oder der Wert der Logik um mehr als einen halben Punkt, öffnet er das Issue Mutation score dropped, mit beiden Werten und den Funktionen, die Überlebende der Logik dazubekamen, hält es aktuell, solange der Wert darunter bleibt, und schließt es, sobald ein Lauf wieder beim Wert vor dem Fall ist. Ein Lauf, der an seinem Zeitlimit stoppte oder einen Teil verlor, wird mit keinem anderen verglichen.
  • Ein niedriger Wert lässt nichts fehlschlagen, auch nicht unter einem Minimum, das die eigene Konfiguration des Projekts setzt, aber ein Lauf, in dem kein Mutant lief, schon: mutmut führt jeden Test zuerst einmal ohne Mutanten aus, und schlägt einer dort fehl, nennt ihn die Zusammenfassung unter The tests fail without any mutant, mit dem Ende der Ausgabe von mutmut oder Infection, und eine Annotation des Jobs nennt den ersten, die der Bot-Health-Bot in seinem Issue zitiert. Eine kaputte Konfiguration oder ein Test, der in der Kopie nicht laufen kann, zeigt sich in der Woche, in der sie kaputtgeht, und der Bot-Health-Bot öffnet sein Issue.
  • Ein Pull Request, der ändert, wie die Mutanten laufen, mutation.yml oder das gepinnte mutmut, probiert sie aus, jeden Teil 25 Minuten lang, und zeigt einen Fehler als roten Check vor dem wöchentlichen Lauf. Der Check ist nicht erforderlich, also wartet ein Pull Request, der sich selbst zusammenführt, etwa ein Update von mutmut, nicht auf ihn: Sein roter Check ist eine Warnung zum Lesen.
  • mutmut führt die Tests in einer Kopie des Repositorys aus, mutants/, mit dem geänderten Code und ohne Git. Ein Test, der dort nicht funktionieren kann, weil er den Code als Text liest, Git fragt oder das Werkzeug startet, das in der Kopie der Code mit den Mutanten ist, bekommt die Markierung @pytest.mark.mutation_unsafe, und die Mutationstests lassen ihn aus. Ebenso ein Test, der ein Modul des Codes in ein eigenes Repository kopiert und dort ausführt, etwa pins.py für den Fix-Bot: Außerhalb von mutants/ findet der Code mit den Mutanten keine Konfiguration von mutmut, und ein Test des Blueprints schlägt fehl, wenn einem solchen Test des Blueprints die Markierung fehlt. Die pyproject.toml eines neuen Python-Projekts registriert die Markierung, und MUTANT_UNDER_TEST, das mutmut setzt, sagt einem Test, dass er dort läuft. mutmut lernt, welche Tests eine Funktion abdecken, indem es alle in einem Prozess ausführt: Ein Test einer Funktion mit einem Cache von functools, etwa @functools.cache, leert ihn mit cache_clear(), damit er den Code ausführt und nicht das Ergebnis, das ein anderer Test hinterließ.
  • mutmut kopiert eine Funktion für jeden ihrer Mutanten ganz, daher macht eine lange Funktion mit vielen davon, etwa ein Parser mit den Optionen jedes Befehls, ein Modul von vielen Megabyte, das jeder Teil und jeder Test, der in der Kopie das Werkzeug startet, minutenlang kompiliert, über ihre Zeitgrenzen hinaus. Eine solche Funktion wird in kleinere geteilt: Der Parser von blueprint.py fügt jede Gruppe seiner Befehle in einer eigenen Funktion hinzu, und ein Test des Blueprints schlägt fehl, wenn eine Funktion seines Codes mehr als 50.000 Zeilen Kopien machen würde.
  • Eine Konfiguration des Projekts hat Vorrang: eine Tabelle [tool.mutmut] in pyproject.toml oder ein Abschnitt [mutmut] in setup.cfg für mutmut, eine infection.json5 für Infection. Ohne sie mutiert mutmut custom_components/, src/ oder die Module und Pakete der Wurzel, und Infection mutiert lib/ auf PHP 8.3, mit tests/bootstrap.php, das die Klassen von Nextcloud lädt. Der Plan folgt source_paths, only_mutate und do_not_mutate von mutmut.

Fuzzing

Für ein Python-Paket oder eine GitHub Action: abdeckungsgesteuertes Fuzzing jedes Ziels in fuzz/, je eine Minute in jedem Pull Request, der den Code, die Ziele oder die Werkzeuge ändert, und jede Woche 25 Minuten, die sich alle Ziele teilen; ein Fund lässt den Lauf scheitern, mit der Eingabe, die ihn auslöste, im Log.

Womit: fuzz.yml: Atheris.

Property-Tests

In Python- und Home-Assistant-Projekten: die Property-Tests jede Nacht mit neuen zufälligen Eingaben, über die festen Eingaben der CI hinaus; ein Fehlschlag zeigt seinen Seed in der Zusammenfassung, und ein Lauf von Hand mit diesem Seed wiederholt ihn.

Womit: properties.yml: Hypothesis.