Vorab eine wichtige Klarstellung: Ich mag keinen Käse. Punkt. Diese gelblichen, manchmal stinkenden Blöcke, die zu lange in dunklen Höhlen gereift haben, sind mir suspekt. Es gibt jedoch zwei Ausnahmen, die mein ganzes Weltbild erschüttern: Pizza und Pasta. Plötzlich ist der Käse nicht mehr mein Feind, sondern mein bester Freund. Bin ich ein Heuchler? Vielleicht. Doch darum geht es in diesem Artikel nicht.
Warum also ziert ein Bild von Schweizer Käse diesen Artikel?
Die Antwort ist das Schweizer-Käse-Modell. Und nein, dabei geht es nicht um den Geschmack. Es handelt sich um ein berühmtes Sicherheitskonzept, bei dem sich mehrere Sicherheitsebenen wie Scheiben Käse überlagern. Jede Scheibe hat Löcher (Schwachstellen), aber zusammen schützen sie davor, dass der „Feind“ ungebremst durchbricht.
Ich hoffe wirklich, dass das Bild oben echten Schweizer Käse zeigt. Von Käse verstehe ich nämlich absolut nichts, das sollte mittlerweile klar geworden sein. Es könnte auch niederländischer oder französischer Käse sein oder irgendein zufälliger gelber Block aus dem Supermarkt. Aber eines weiß ich sicher: Er sollte große Löcher haben. Große, charakteristische Löcher. Denn die Löcher sind das Wichtigste an diesem Käse – genauso wie in unseren Sicherheitsnetzen in der Softwareentwicklung.
Warum ein Ansatz nicht ausreicht
Hillel Wayne stellt fest:
[U]nit tests are not enough. Type systems are not enough. Contracts are not enough, formal specs are not enough, code review isn't enough, nothing is enough. We have to use everything we have to even hope of writing correct code, because there's only one way a program is right and infinite ways a program can be wrong, and we can't assume that any tool we use will prevent more than a narrow slice of all those wrong ways.
Diese grundlegende Erkenntnis treibt die moderne PHP-Entwicklung in Richtung eines mehrschichtigen Ansatzes für Softwarequalität und -sicherheit voran. Das Verständnis der Beziehung zwischen statischer Analyse, dynamischem Testen mit PHPUnit, eigenschaftsbasierten Tests, Mutationstests und Fuzzing zeigt eine umfassende Strategie, die verschiedene Arten von Fehlern in verschiedenen Entwicklungsphasen identifiziert. Jede Technik befasst sich mit einem eigenen kleinen Ausschnitt der unendlichen Möglichkeiten, wie Code versagen kann.
Bei der klassischen Testpyramide sind Unit Tests unten, Integrationstests in der Mitte und End-to-End Tests oben. Wenn wir aber moderne Techniken wie eigenschaftsbasiertes Testen und Fuzzing mit einbeziehen, entsteht eher ein Kontinuum von Testgranularität und Fehlererkennungsfähigkeit als eine strenge Hierarchie.
Jede Schicht arbeitet auf einer anderen Abstraktionsebene, identifiziert verschiedene Arten von Fehlern und erfüllt einen bestimmten Zweck in deiner Qualitätssicherungsstrategie.
Die erste Verteidigungslinie
Statische Codeanalyse testet deinen Code, ohne ihn auszuführen, und findet Typfehler, undefinierte Methoden und logische Unstimmigkeiten, bevor du ihn ausführst. Sie arbeitet am schnellsten und spezifischsten Ende des Spektrums und analysiert einzelne Codeeinheiten und ihre Aufrufgraphen, um ihre Richtigkeit anhand des PHP-Typsystems und deiner dokumentierten Verträge zu überprüfen.
Bezug zu anderen Techniken
- Ergänzt dynamische Tests, indem sie Fehler findet, für die wir sonst Dutzende von Testfällen bräuchten (z. B. die Weitergabe falscher Typen durch mehrere Aufrufebenen)
- Leitet eigenschaftsbasierte Tests an, indem sie Randfälle in Typsignaturen und Funktionsverträgen identifiziert
- Informiert Fuzzing-Ziele, indem sie komplexe Codepfade hervorhebt, die eine tiefere dynamische Analyse verdienen
- Reduziert die Kosten für Mutationstests, indem sie offensichtlich falschen Code vor der kostspieligen Testausführung eliminiert
Das Fundament
Dynamische Tests, die mit PHPUnit implementiert werden, testen bestimmte Verhaltensweisen mit bestimmten Eingaben und sind das Fundament deiner Testsuite. Jeder Testfall zeigt ein bestimmtes Szenario, wie zum Beispiel „Wenn ich diese Methode mit genau diesen Argumenten aufrufe, erwarte ich genau dieses Ergebnis“.
Bezug zu anderen Techniken
- Validiert Ergebnisse der statischen Analyse, indem bestätigt wird, dass theoretisch korrekter Code auch tatsächlich korrekt funktioniert
- Liefert Ausgangspunkte für eigenschaftsbasierte Tests:, indem dynamische Testfälle zu Ausgangspunkten für Generatoren werden
- Dient als Grundlage für Mutationstests: Wenn Mutanten überleben, sind deine dynamischen Tests nicht gut genug
- Erstellt Harnesses für Fuzzing: Dynamische Tests können durch Ersetzen fester Eingaben durch mutierte Daten an Fuzzing-Ziele angepasst werden
Die Grenze des traditionellen dynamischen Testens ist die menschliche Vorstellungskraft: Wir testen nur das, was uns einfällt. Hier bringen nachfolgende Ebenen einen Mehrwert.
Generalisierung
Das eigenschaftsbasierte Testen stellt den traditionellen Ansatz des dynamischen Testens auf den Kopf. Anstatt bestimmte Testfälle zu schreiben, legst du Eigenschaften fest, die für alle Eingaben gelten sollen, und generierst dann automatisch Hunderte von zufälligen Testfällen.
Bezug zu anderen Techniken
- Erweitert das dynamische Testen, indem es Eingabebereiche untersucht, die Menschen nicht in Betracht ziehen würden
- Validiert die statische Analyse, indem Typbeschränkungen dynamisch über zufällige Eingaben getestet werden
- Liefert Informationen für Fuzzing-Strategien: Generatoren können für Fuzzing-Korpora angepasst werden
- Ergänzt Mutationstests: Eigenschaftsbasiertes Testen findet logische Fehler, Mutationstests validieren die Testqualität
Eigenschaftsbasiertes Testen arbeitet auf einer allgemeineren Ebene als herkömmliche dynamische Tests, ist aber strukturierter als Fuzzing, da du immer noch Eigenschaften angeben musst. Der entscheidende Punkt ist, dass eigenschaftsbasiertes Testen die Lücke zwischen von Menschen festgelegten Tests und vollständig automatisierter Erkundung schließt.
Die Qualität der Tests überprüfen
Mutationstests finden keine Fehler in deinem Code, sondern in deinen Tests. Wenn deine Tests nach der Mutation deines Codes immer noch erfolgreich sind, bedeutet das, dass sie diesen Codepfad nicht ausreichend abdecken.
Bezug zu anderen Techniken
- Validiert dynamische Tests und eigenschaftsbasierte Tests, indem es ihre Fähigkeit misst, Verhaltensänderungen zu erkennen
- Leitet Fuzzing-Maßnahmen: Unentdeckte Mutanten zeigen Codepfade auf, die Fuzzing-Aufmerksamkeit erfordern
- Ergänzt die statische Analyse: Statische Tests stellen sicher, dass der Code typsicher ist, Mutationstests stellen sicher, dass die Tests diesen Code sinnvoll ausführen
- Qualitätskontrolle für die Reife der Testsuite, bevor in teure Fuzzing-Kampagnen investiert wird
Mutation Testing kann langsam sein, da die ganze Testsuite mehrmals ausgeführt wird. Es liefert aber objektive Messwerte zur Testqualität. Eine Testsuite mit hoher Mutationsabdeckung ist bereit für Fuzzing, während eine mit geringer Abdeckung mehr dynamische Tests benötigt.
Explorative Fehlersuche
Fuzzer sind die explorativsten und langsamsten Tools. Sie machen zufällige Eingaben, um Abstürze und andere unerwartete Verhaltensweisen zu finden, ohne dass du vorher sagen musst, was die Ergebnisse sein sollen.
Wichtige Unterschiede zu anderen Techniken
- Kein Orakel erforderlich: anders als bei dynamischen Tests und eigenschaftsbasierten Tests
- Abdeckungsorientiert: anders als bei Zufallstests
- Findet unbekannte Unbekannte: anders als bei der statischen Analyse, die bekannte Fehlermuster findet
Bezug zu anderen Techniken
- Geht über traditionelle dynamische Tests hinaus und findet Randfälle, an deren Testen du nicht gedacht hast
- Validiert die statische Analyse und bestätigt, dass potenzielle Schwachstellen tatsächlich ausnutzbar sind
- Profitiert von eigenschaftsbasierten Generatoren, die hervorragende Seed-Korpora liefern
- Profitiert von Mutationstests, da Fuzzing erst dann seinen vollen Nutzen entfaltet, wenn deine Testsuite Verhaltensänderungen zuverlässig erkennen kann
Der Kreis schließt sich: Die statische Analyse identifiziert komplexen Code, traditionelle dynamische Tests decken grundlegende Pfade ab, eigenschaftsbasierte Tests untersuchen Eingabebereiche, Mutationstests validieren die Abdeckung und Fuzzing entdeckt Randfälle, die neue Regeln für die statische Analyse liefern.
Fazit
Keine einzelne Technik reicht aus. Statische Analysen finden offensichtliche Fehler zu geringen Kosten. Traditionelles dynamisches Testen mit PHPUnit überprüft bestimmte Verhaltensweisen. Eigenschaftsbasierte Tests verallgemeinern diese Verhaltensweisen. Mutation Testing überprüft die Qualität der Testsuite. Fuzzing erkundet das Unbekannte.
Diese Methoden sind nicht hierarchisch, sondern bilden eine zirkuläre, sich gegenseitig verstärkende Beziehung. Beginne mit statischer Analyse und Unit Tests und füge dann eigenschaftsbasierte Tests für kritische Algorithmen hinzu. Verwende Mutationstests, um die Qualität zu messen, und setze Fuzzing für sicherheitsrelevanten Code ein. Jede Schicht informiert und stärkt die anderen und schafft so einen umfassenden Schutz vor Fehlern, den kein einzelner Ansatz allein erreichen könnte.