Zum Inhalt springen

Suchen...

Wie Property Based Testing hilft

Ein Fehler, der erst nach genau 17 Schritten auftritt, klingt untestbar. Eigenschaftsbasierte Tests befunden ihn trotzdem, indem sie Fälle generieren, die Menschen niemals schreiben würden.

9 Min. Lesezeit
Cover für Wie Property Based Testing hilft

Eigenschaftsbasiertes Testen ist eine Methode, bei der du die Verhaltenseigenschaften eines Systems beschreibst und automatisierte Werkzeuge Tausende von Testfällen aus dieser Beschreibung generieren lässt. Sie ersetzt manuell geschriebene Testfälle durch einen dreiteiligen Prozess: ein Spezifikationsmodell, einen Generator, der automatisch Kombinationen erstellt, und einen Shrinker, der eine entdeckte Fehlerwirkung für das menschliche Review auf ihre genaue Ursache reduziert.

Das Wichtigste in Kürze

  • Das eigenschaftsbasierte Testen generiert automatisch Tausende von Testkombinationen aus einer formalen Beschreibung des Systemverhaltens und erreicht dabei Fehlerauswirkungen, die keine von Menschen geschriebene Testsuite vorhersehen kann.
  • In verteilten Systemen mit vielen Diensten steigt die Anzahl der möglichen Fehlerwirkungen exponentiell an, so dass die manuelle Erstellung von Testfällen in großem Umfang unpraktisch ist.
  • Die Shrinker-Komponente macht das eigenschaftsbasierte Testen nutzbar: Sie reduziert eine lange generierte Fehlerwirkung auf den genauen Schritt, bei dem der Fehler auftrat, so dass Ingenieure darauf reagieren können.
  • Property-basiertes Testen wird kostspielig, wenn jede Testdurchführung kostenpflichtige Ressourcen verbraucht, wie z. B. Cloud-Speicherplatz oder Live-Netzwerkanrufe, da das automatisierte Testvolumen die Kosten außer Kontrolle geraten lässt.
  • Die Einführung des eigenschaftsbasierten Testens in einem großen Unternehmen ist in erster Linie eine kulturelle Herausforderung: Es ist effektiver als eine Einführung von oben nach unten, zuerst ein Team zu gewinnen, das bereit ist, sich bei den anderen Teams dafür einzusetzen.

Was das eigenschaftsbasierte Testen tatsächlich leistet

Beim eigenschaftsbasierten Testen werden Testfälle automatisch aus einer Beschreibung des Verhaltens eines Systems generiert. Anstatt feste Eingaben und erwartete Ausgaben von Hand zu schreiben, gibst du eine Regel an, die immer zutreffen muss, und das Tool erzeugt Tausende von Kombinationen, um diese Regel zu überprüfen.

Der Ausgangspunkt ist die Skalierung. In großen Microservice-Landschaften kann ein einziger fehlgeschlagener Dienst seine Fehlerwirkung weitergeben, ohne dass du es merkst, bis der fünfte oder sechste Dienst zusammenbricht. Nikhil Barthwal macht die Rechnung einfach: Bei n Diensten und Fehlern, die auftreten, wenn zwei oder drei von ihnen zusammen ausfallen, steigt die Anzahl der Fehlerauswirkungen mit der Kubikzahl von n an. Bei Tausenden von Diensten geht die Zahl der Kombinationen in die Millionen oder Milliarden.

Keiner schreibt eine Million Testfälle von Hand. Diese Lücke ist der Grund, warum es das eigenschaftsbasierte Testen gibt.

Ein einfaches Beispiel verdeutlicht die Idee

Nimm einen Dienst, der zwei Eingaben, A und B, entgegennimmt und A plus B zurückgibt. Der herkömmliche Ansatz ist, ihn mit Null, Eins, Zwei, einigen negativen und einigen positiven Werten zu füttern, die Ergebnisse zu überprüfen und weiterzumachen.

Das Problem beginnt an den Rändern. Ganzzahlige Überläufe und Pufferüberläufe treten nur bei bestimmten Kombinationen auf, und eine Handvoll handverlesener Eingaben wird direkt an ihnen vorbeigehen.

Ein eigenschaftsbasierter Ansatz beschreibt stattdessen die Regel. Wenn A plus B gleich C ist, dann muss C minus B gleich A sein. Du gibst diese Beziehung einmal an. Das Tool generiert dann Tausende von Kombinationen aus A und B, berechnet die Ausgabe, zieht sie ab und überprüft, ob sie wieder bei A landet. Du beschreibst die Eigenschaft, und das System schreibt die Fälle.

Die drei Komponenten: Modell, Generator, Shrinker

Jedes eigenschaftsbasierte System besteht aus drei Teilen, die nacheinander funktionieren.

Der erste Teil ist eine Modellierungssprache. Für das Additionsbeispiel ist das eine einzeilige Gleichung. Für echte Systeme ist es eine formale Spezifikation. Nikhil verwendet TLA+, obwohl es mehrere formale Beschreibungssprachen für den gleichen Zweck gibt.

Der zweite ist ein Generator. Er liest die Spezifikation und erstellt die Testfälle, wobei er nach Verstößen sucht. Du kannst einen eigenen Generator schreiben oder dich auf einen Standardgenerator verlassen, der mit dem Framework geliefert wird.

Der dritte ist ein Shrinker, der ein Problem löst, das der Generator verursacht. Da die generierten Fälle lang und maschinell erstellt sind, kann eine Fehlerwirkung in einer Folge von vielen Schritten auftreten. Der Shrinker reduziert diese Abfolge auf die kleinste Reproduktion, so dass ein Mensch genau sehen kann, wo etwas kaputt gegangen ist.

Warum lange Fehlerwirkungen einen Shrinker brauchen

Generierte Testfälle schlagen oft erst nach einer langen Kette von Schritten fehl, und eine rohe Kette sagt einem Menschen fast nichts.

Nikhil zeigt auf einen Testlauf gegen eine Google LevelDB Datenbank. Ein Fehler tauchte erst auf, nachdem siebzehn bestimmte Schritte wiederholt worden waren - eine Abfolge, mit der kein Mensch rechnen würde. Ein eigenschaftsbasiertes Tool befand ihn innerhalb einer Stunde.

Aber siebzehn Schritte sind keine brauchbare Fehlermeldung. Du weißt, dass etwas fehlgeschlagen ist, aber nicht, welcher Schritt es verursacht hat. Der Shrinker nimmt die fehlgeschlagene Sequenz und kürzt sie, bis der eigentliche Fehler isoliert ist. Durch diese Reduzierung wird aus einem maschinellen Befund etwas, auf das ein Entwickler reagieren kann.

Die Frameworks gehen alle auf eine Idee zurück

Eigenschaftsbasiertes Testen ist ein Konzept, kein einzelnes Produkt, und die Implementierungen variieren je nach Sprache und Stack.

Der Ansatz begann mit QuickCheck, das in Haskell geschrieben wurde, und wurde dann weithin portiert. Es gibt Frameworks für .NET, wie z. B. FsCheck, und Optionen im Python-Ökosystem, um nur einige zu nennen. Es gibt auch kommerzielle Tools, die u. a. in der Automobilsoftware eingesetzt werden, wo eine Fehlerwirkung Leben kosten kann.

Verschiedene Teams setzen die Generierung und Reduzierung unterschiedlich um, weshalb die Framework-Landschaft breit gefächert ist. Der zugrundeliegende Vertrag bleibt derselbe: Beschreibe das System und lass das Tool es testen.

Das eigentliche Ziel ist es, die menschliche Vorstellungskraft zu übertreffen

Beim eigenschaftsbasierten Testen geht es um Zuverlässigkeit, und der Weg dahin ist eine Überdeckung, die kein Mensch von Hand schreiben könnte.

Ein Mensch, der Testfälle schreibt, stößt an eine Grenze. Die Anzahl der Permutationen, die in einem großen System schiefgehen können, übersteigt bei weitem das, was sich ein Mensch vorstellen, geschweige denn kodieren kann. Formale Methoden sind im Allgemeinen dazu da, diese Grenze zu überschreiten.

Die Nebenläufigkeit zeigt das gleiche Prinzip aus einem anderen Blickwinkel. Threads und Sperren schlagen nur dann fehl, wenn eine bestimmte Reihe von äußeren Bedingungen zusammentreffen. Deine Unit Tests bestehen. Integrationstests bestehen. Die Produktion läuft zwei Monate lang einwandfrei und bricht dann am ersten Tag des dritten Monats zusammen.

TLA+ löst genau dieses Problem, indem es einen Zustandsraum mit allen möglichen Bewegungen und Kombinationen erstellt und dann auf Deadlocks und Nebenläufigkeit prüft. Nikhil merkt an, dass AWS Arbeiten veröffentlicht hat, in denen die formalen Methoden von TLA+ verwendet werden, um Fehler aufzudecken, die sonst nur sporadisch auftauchen und manchmal Designfehler aufdecken, die eine Neufassung erfordern.

Der Grundgedanke bleibt derselbe: Die menschliche Vorstellungskraft funktioniert nur bis zu einem bestimmten Punkt. Sobald man den Maßstab überschreitet, ist die Anzahl der Kombinationen von Dingen, die schief gehen können, so groß, dass es für einen Menschen nicht mehr praktikabel ist, diese Testfälle zu denken oder zu implementieren.

  • Nikhil Barthwal

Beschreibe Eigenschaften, nicht Transaktionen

Du legst fest, was an einem System immer wahr sein muss, und das kannst du auf verschiedenen Ebenen tun.

Betrachte einen E-Commerce-Einkauf. Wenn du n Kisten Schokolade kaufst, müssen zwei Eigenschaften zutreffen: Der Bestand sinkt um genau n und der Umsatz steigt um das n-fache des Preises. Einfach ausgedrückt, scheint dies trivial zu sein.

Es ist nicht mehr trivial, wenn die Konsistenz gegeben ist. Da verteilte Systeme über mehrere Datenbanken und Rechenzentren laufen, kann es sein, dass eine Eigenschaft in einem Rechenzentrum gilt und in einem anderen nicht. Das sind die Bedingungen, die generiert und überprüft werden sollten, nicht einfach vorausgesetzt.

Die Ebene, auf der du eine Spezifikation erstellst, entscheidet über die Art des Tests. Eine Spezifikation auf der Funktions- oder Dienstebene verhält sich wie ein Unit Test. Eine Spezifikation auf der Systemebene verhält sich wie ein Integrationstest. Die meisten Teams machen beides, und beides ist sinnvoll.

Wo eigenschaftsbasierte Tests nicht hingehören

Die Methode bricht zusammen, wenn jede Testdurchführung echte Kosten verursacht, denn eine Million generierte Testfälle bedeuten eine Million echte Kosten.

Nikhil erinnert sich an das Testen bei BlackBerry, wo ein Test einen tatsächlich abgerechneten Telefonanruf bedeutete. Eine Million Testfälle bedeuteten eine Million Anrufe und eine dazugehörige Rechnung.

Die gleiche Falle gilt für Cloud-Ressourcen. Wenn deine Tests in den Speicher von AWS schreiben, zahlst du für jede Ressource, die die Tests verbrauchen. Wenn du eine Million Testfälle automatisch generierst, können die Kosten außer Kontrolle geraten. Wenn Tests Ressourcen zerstören oder Kosten verursachen, ist das eigentumsbasierte Testen das falsche Werkzeug.

Wie du anfängst: erst klein, dann wachsen

Beginne mit einem Dienst, beweise, dass der Ansatz in der Praxis funktioniert und erweitere dann den Umfang. Dieser Ratschlag gilt für jedes neue Test-Tool, nicht nur für das eigentumsbasierte Testen.

Durch organisches Wachstum werden Hindernisse früh erkannt, ganz im Sinne der Fail-Fast-Idee. Wenn das Tool bei einer Dienstleistung funktioniert, wird es wahrscheinlich auch bei der nächsten funktionieren, und das gibt dir etwas Konkretes, das du zeigen kannst.

Der schwierigste Teil ist selten der Code. Es ist die Kultur. Die Einführung einer neuen Methode zwingt die Menschen dazu, anders zu denken, und in einem großen Unternehmen bedeutet das, dass viele erfahrene Ingenieure, die bereits auf ihre eigene Arbeitsweise vertrauen, umdenken müssen.

Nikhil betrachtet dies als ein Verkaufsproblem. Du bist der Verkäufer, die Entwickler sind die Kunden, und sie müssen die Idee kaufen. Ein Verkäufer, der sein eigenes Produkt anpreist, überzeugt niemanden. Ein Kollege, der sagt, dass das Tool ihm wirklich geholfen hat, hat viel mehr Gewicht. Wähle also ein bereitwilliges Team aus, zeige ihm den Wert des Tools und lass es den anderen empfehlen.

EntscheidungspunktBefürwortet eigenschaftsbasiertes TestenSpricht sich dagegen aus
KombinationsraumMillionen von Fehlerauswirkungen, zu viele, um sie von Hand aufzuschreibenWenige, gut verstandene Eingaben
FehlerwirkungIntermittierende Fehler, NebenläufigkeitEinfache, deterministische Pfade
Kosten pro TestlaufGünstig und wiederholbarJeder Testlauf ist kostenpflichtig oder destruktiv
Umfang des RolloutsErst ein Service, dann erweiternAlles auf einmal über viele Teams hinweg

Das Tool unterstützt den Tester, es ersetzt ihn nicht

Eigenschaftsbasiertes Testen grenzt eine riesige Anzahl möglicher Fehlerwirkungen auf eine kurze Liste ein, die ein Mensch tatsächlich untersuchen kann.

Eine Million Dinge können schiefgehen, und kein Mensch kann das in seinem Kopf behalten. Das System wählt die zwanzig Fälle aus, die wie echte Probleme aussehen, und du konzentrierst dich darauf, statt auf den gesamten Raum.

Einige dieser zwanzig Fälle können falsch negativ sein, d.h. die gemeldete Fehlerwirkung ist eher ein Fehler in der Beschreibung des Systems als ein echter Fehler. Die menschliche Aufsicht bleibt in der Schleife. Wenn ein Fall fehlgeschlagen ist, erhältst du die vollständige Sequenz, überprüfst, ob es sich um eine echte Fehlerwirkung handelt, und der Shrinker zeigt dir, wo etwas schief gelaufen ist. Wenn du einen echten Fehler bestätigst, kannst du einen eigenen Testfall dafür schreiben.

Nikhil zieht die Parallele zu der breiteren Debatte über generative KI, die den Menschen ersetzen soll. Seine Position ist in beiden Fällen dieselbe: Das Werkzeug macht dich produktiver, es ersetzt dich nicht.

Diese Seite teilen