Einleitung: Tests bestehen, aber Bugs bleiben?
\n\nStellen Sie sich folgende Situation vor: Sie haben ein Modul in Go geschrieben, es mit Unit-Tests zu 95% abgedeckt (laut go test -cover), alle Tests sind grün. Sie sind überzeugt, dass der Code einwandfrei funktioniert. Aber in der Produktion taucht ein Bug auf. Kommt Ihnen das bekannt vor?
Das Problem ist, dass die Metrik der Codeabdeckung (Code Coverage) lediglich die Anzahl der ausgeführten Zeilen misst. Sie sagt nichts darüber aus, was die Tests tatsächlich prüfen. Man kann 100% Abdeckung haben, aber nie überprüfen, ob eine Funktion den richtigen Wert zurückgibt, sondern nur, dass sie nicht mit einer Panic abstürzt.
\n\nHier kommt Mutationstests (Mutation Testing) in Go ins Spiel – eine Methode, die künstlich Fehler in Ihren Code einbaut und prüft, ob die Tests diese erkennen können. Wenn die Tests beim mutierten Code nicht fehlschlagen, sind Ihre Tests nutzlos.
\n\nIn diesem Artikel erklären wir, was Mutationstests in Go sind, wie man sie einrichtet und warum sie ein Must-have für Projekte sind, bei denen Zuverlässigkeit kritisch ist. Los geht's!
\n\nWas sind Mutationstests?
\n\nMutationstests sind eine Methode zur Bewertung der Testqualität, bei der kleine Änderungen (Mutationen) in das Programm eingefügt werden, die typische Entwicklerfehler simulieren. Anschließend werden die Tests ausgeführt. Wenn mindestens ein Test fehlschlägt, ist die Mutation "getötet" – der Test funktioniert. Wenn alle Tests bestehen, hat die Mutation "überlebt", und das ist eine rote Flagge: Sie haben ein Loch in Ihren Tests.
\n\nIn Go werden Mutationstests oft als Mutation Testing Golang bezeichnet, und sie sind besonders nützlich für die Überprüfung von Geschäftslogik, Validatoren, Parsern und anderen kritischen Komponenten.
\n\nBeispiel einer Mutation
\n\nAngenommen, Sie haben eine einfache Funktion:
\n\nfunc IsEven(n int) bool {\n return n%2 == 0\n}\n\nEine typische Mutation ist das Ersetzen von == durch !=:
func IsEven(n int) bool {\n return n%2 != 0\n}\n\nWenn Ihr Test nur prüft, dass IsEven(2) nicht abstürzt, aber nicht, dass das Ergebnis true ist, wird die Mutation überleben. Der Test besteht, obwohl die Funktion jetzt genau das Gegenteil tut.
Warum reicht einfache Codeabdeckung nicht aus?
\n\nViele Go-Entwickler verlassen sich auf go test -cover. Das ist eine nützliche Metrik, aber sie kann täuschen. Betrachten wir ein Beispiel:
func Divide(a, b int) (int, error) {\n if b == 0 {\n return 0, errors.New("division by zero")\n }\n return a / b, nil\n}\n\nTest:
\n\nfunc TestDivide(t *testing.T) {\n result, err := Divide(10, 2)\n if err != nil {\n t.Fatal(err)\n }\n if result != 5 {\n t.Errorf("expected 5, got %d", result)\n }\n}\n\nAbdeckung? 100% – alle Zeilen wurden ausgeführt. Aber was, wenn jemand versehentlich a / b durch a * b ersetzt? Der Test bemerkt es nicht, weil 10 * 2 = 20 ist, wir aber 5 erwartet haben. Schlägt der Test fehl? Nein, wenn wir nicht den genauen Wert prüfen.
Mutationstests in Go würden dieses Problem sofort aufdecken, indem sie einen Mutanten mit Multiplikation erstellen und prüfen, ob der Test fehlschlägt.
\n\nWerkzeuge für Mutationstests in Go
\n\nDas wichtigste Werkzeug ist go-mutesting. Es ist eine beliebte Bibliothek, die automatisch Mutationen erstellt und Tests ausführt. Installation:
\n\ngo get github.com/zimmski/go-mutesting/cmd/go-mutesting\n\nEs gibt auch Alternativen wie mutesting von der Firma "Go Test It", aber go-mutesting ist der De-facto-Standard.
Wie man es ausführt
\n\nBasierender Aufruf:
\n\ngo-mutesting ./...\n\nDieser Befehl findet alle _test.go-Dateien im Projekt und prüft, welche Mutationen überleben. Das Ergebnis wird mit einem detaillierten Bericht in der Konsole ausgegeben.
Praktisches Beispiel: Mutationstests in Go
\n\nErstellen wir ein einfaches Paket calculator mit Additions- und