Zum Hauptinhalt Zur Navigation Zur Suche

Software-Tests neu denken: Perspektiven aus der Welt der Hardware

Hardware und Software erscheinen oft wie zwei verschiedene Welten – und in vielerlei Hinsicht sind sie das auch. Aber vor allem beim Testen kann die eine viel von der anderen lernen.
/ Rajiv Prabhakar
23 Kommentare Auf Google folgen (öffnet im neuen Fenster)
Hardware, wie Prozessoren, wird anders getestet als Software. Einige Prinzipien aus Hardwaretests könnten und sollten jedoch übernommen werden. (Bild: Yoshikazu Tsuno/AFP via Getty Images))
Hardware, wie Prozessoren, wird anders getestet als Software. Einige Prinzipien aus Hardwaretests könnten und sollten jedoch übernommen werden. Bild: Yoshikazu Tsuno/AFP via Getty Images)

Dieser Artikel ist eine Übersetzung. Das Original des Softwareentwicklers und Bloggers Rajiv Prabhakar ist hier(öffnet im neuen Fenster) zu finden.

Computeringenieure bei Intel verbringen, genau wie Softwareentwickler, die meiste Zeit am Schreibtisch und schreiben (Verilog-)Code, der das gewünschte Systemverhalten implementiert. Anschließend kompilieren (synthetisieren) sie ihren Code, um Ausgaben auf niedrigerer Ebene (digitale Schaltungen und physikalische Layouts) zu erzeugen. Dann schreiben sie automatisierte Tests für ihr SUT(öffnet im neuen Fenster), um sicherzustellen, dass der Code funktional korrekt ist.

Ich kenne das alles gut, war selbst als Hardwareentwickler tätig und bin später in die Softwareentwicklung gewechselt. Nach meinem Masterabschluss in Rechnerarchitektur habe ich mehr als fünf Jahre bei Intel und Sun als Hardware-Verifikationsingenieur gearbeitet, bevor ich eine leitende Position bei Apple ablehnte, um meine Karriere als Softwareentwickler neu zu starten.

In den vergangenen Jahren habe ich in einigen großartigen Software-Teams bei Unternehmen wie Google gearbeitet und in meiner Freizeit zudem die Entwicklung mehrerer persönlicher Projekte geleitet. Die Programmierer, die ich kennengelernt und mit denen ich zusammengearbeitet habe, sind zweifellos intelligent und verfügen über einige Fähigkeiten, die sich meine Hardware-Kollegen zum Vorbild nehmen sollten. Eine Sache, die mir jedoch aufgefallen ist: Wenn es ums Testen geht, sind ihre Instinkte ... falsch. Und zwar ziemlich falsch.

Dieser Text ist mein Versuch, komprimiert jene Lektionen zu vermitteln, die ich aus meinen Hardware-Tagen gelernt habe, und zu zeigen, wie sie angewendet werden können, um unsere Software-Testmethoden und -Ergebnisse zu verbessern.

Disclaimer: Dieser Beitrag konzentriert sich auf Nicht-UI-Programmierung, bei der die Funktionalität ohne Eyeballing, also die visuelle oder Sichtkontrolle zu 100 Prozent geprüft werden kann. Front-End-/UI-Tests sind ein ganz anderes Thema und eines, von dem ich mich lieber fernhalte.

Den Einsatz erhöhen

In den meisten Softwareunternehmen ist der Blick auf das Testen ein Problem. In der Hardware ist die Pre-Silicon-Verifikation(öffnet im neuen Fenster) im Entwicklungsprozess quasi ein First-Class-Objekt. Engagierte Verifikationsingenieure verdienen sechsstellige Gehälter, sitzen bei allen Planungsmeetings neben den RTL(öffnet im neuen Fenster)-Designern und haben Karrieren, die genauso prestigeträchtig und lukrativ sind.

Im Vergleich dazu wird das Testen in den meisten Softwarefirmen, die ich kenne, als "Second-Class-Objekt" behandelt. "Testingenieur" oder schlimmer noch: "Tester" zu sein, gilt oft als weniger prestigeträchtig oder lukrativ.

Dieser Unterschied ist kein Zufall, sondern eine natürliche Folge des viel höheren Einsatzes, der bei Hardware auf dem Spiel steht. Da der Tapeout-Prozess(öffnet im neuen Fenster) so teuer und zeitaufwendig ist, kann das Auffinden eines einzigen Fehlers die Markteinführung eines Produkts um Monate verzögern und Millionen Dollar kosten.

Anzeige

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.

Oder schlimmer: Wird ein Fehler gefunden, nachdem die Kunden die Chips bereits gekauft und installiert haben, kann dies zu sehr teuren Rückrufaktionen führen(öffnet im neuen Fenster) – selbst wenn der Fix aus einer simplen, einzeiligen Codeänderung besteht.

Die Folgen von Softwarefehlern können ebenfalls katastrophal sein. Aber zumindest ist die Behebung logistisch gesehen extrem billig. Code-Verteilung und Software-Patches sind weitaus schneller und billiger zu haben als die Herstellung und Verteilung von neuem Silizium. Daher nehmen Hardwareunternehmen das Testen viel ernster als vergleichbare Softwareunternehmen.

So entsteht eine CPU bei Intel – From Sand to Silicon (2020)
So entsteht eine CPU bei Intel – From Sand to Silicon (2020) (04:36)

Die Ergebnisse sprechen für sich: Hardware-Produkte, die in den Händen der Kunden landen, haben sehr viel weniger Bugs. Der Prozentsatz der Bugs, die vor der Veröffentlichung abgefangen werden, ist in der Hardwareindustrie wesentlich höher als in der Softwareindustrie.

Wie es besser geht

Die Versuchung ist groß zu sagen, dass Hardware-Teams allein wegen ihrer größeren finanziellen Möglichkeiten besser testen können. Und dass Software-Teams bereits am Optimum arbeiten und Verbesserungen in der Testqualität nur durch mehr Zeit oder höhere Kosten erreicht werden könnten.

Eine solche Sichtweise ist zu optimistisch, was den Stand der Dinge angeht, und zu pessimistisch, was das Verbesserungspotenzial betrifft. In den vergangenen Jahrzehnten haben wir unsere Praktiken und Methoden der Softwareentwicklung enorm verbessert. Es gibt keinen Grund zu glauben, dass wir jetzt einen Zustand des Nirwana erreicht haben, in dem keine weiteren Verbesserungen mehr möglich sind.

Auch wenn viele Programmierer dazu neigen, ihr zu wenig Beachtung zu schenken: Die Testmethodik ist ein Skill-Set. Eines, das im Lauf der Zeit von einer ganzen Branche erlernt und verbessert wird, und zwar proportional zur Höhe ihrer Investitionen. In diesem Sinne ist die Hardwareindustrie meilenweit voraus, wenn es um Best Practices beim Testen geht. Nicht weil sie in irgendeiner Weise "schlauer" ist, sondern schlicht, weil ihr Überleben davon abhängt.

Man würde nicht erwarten, dass ein Fußballspieler so hoch springen kann wie ein Basketballspieler. Man würde nicht erwarten, dass ein Restaurant so hohe Hygienestandards hat wie ein Krankenhaus. Man sollte definitiv nicht erwarten, dass eine Softwarefirma das Testen so gut beherrscht wie ein Hardwareunternehmen.

Aber wenn man die Kunst des Testens beherrschen möchte, sollte man unbedingt mit einem Hardware-Verifikationsingenieur sprechen.

Das 0. Gesetz des Testens: Nur die Paranoiden überleben

"Wenn es nicht getestet wurde, funktioniert es nicht." Selbst wenn dieser Satz(öffnet im neuen Fenster) nicht immer stimmt, ist er sicherlich eine gute Arbeitsgrundlage für die Projektarbeit. Diese Regel bildet jedenfalls die Grundlage für die meisten anderen hier aufgeführten Lektionen.

Eine Warnung vorab: Die Welt der möglichen Eingaben und selten auftretenden Sonderfälle ist unendlich. Daher werden Sie mit empirischen Tests niemals eine hundertprozentige Abdeckung erreichen. Sie werden dieses Ziel nie erreichen. Wenn Sie jemals denken, dass Sie mit dem Testen "fertig" sind, werden Sie eine Überraschung erleben. Alles, was Sie tun können ist, so viel Abdeckung zu erreichen, wie es mit der verfügbaren Zeit und den verfügbaren Ressourcen möglich ist.

Manuelles Testen ist nicht gut genug

Dinge, die ich Entwickler sagen höre:

"Das hier ist so wichtig, dass wir es manuell testen müssen. Ich traue einem automatisierten Test diese Aufgabe nicht zu."

"Denken Sie nicht darüber nach, automatisierte Tests zu erstellen. Wir haben diese Änderungen schon manuell getestet."

Ein Hardwareentwickler würde hingegen sagen:

"Das ist so wichtig, dass wir eine automatisierte Testsuite dafür bauen müssen. Ich traue den menschlichen Testern das nicht zu."

"Führen Sie ein paar Tests von Hand durch, als letzte Plausibilitätsprüfung. Verbringen Sie aber nicht zu viel Zeit damit, es wurde ja schon ziemlich gut automatisch getestet."

Ein paar Tests von Hand durchzuführen und die Ergebnisse durchzusehen, mag in einer VLSI-Vorlesung an der Uni funktionieren. Aber in der Industrie wird man Sie dafür auslachen. Manuelles Testen kann nicht auf Github Code-reviewt werden. Manuelles Testen unterliegt menschlichen Fehlern, sei es aus Nachlässigkeit oder Faulheit. Manuelles Testen ist extrem zeit- und arbeitsintensiv, wenn es bei jedem einzelnen Release gemacht wird.

Es mag Ausnahmen geben, bei denen ein Test nicht automatisiert werden kann. Aber es sollten eben Ausnahmen sein – nicht die Regel. Zuverlässigkeit ergibt sich letztlich aus der Stärke der automatisierten Testsuite und nicht daraus, wie viele manuelle Tests gemacht werden. Alles, was wichtig genug ist, um es von Hand zu testen, ist auch wichtig genug, um eine automatisierte Testsuite dafür zu erstellen.

Zwei Eingaben für sich zu testen != sie zusammen zu testen

Angenommen, Ihr Team implementiert und testet die folgende Methode:

public static double myCustomDivider(double numerator, double divisor);

Alice: "Haben wir Tests, die das korrekte Verhalten bei negativen Eingaben überprüfen?"

Bob: "Ich habe einen Test, bei dem der Zähler negativ ist und einen weiteren Test, bei dem der Nenner negativ ist."

Alice: "Hast du einen dritten Test, bei dem beide negativ sind?"

Bob: "Nein, den brauchen wir auch nicht. Wir haben bereits beide Fälle einzeln behandelt."

Lachen Sie ruhig, aber ich habe solche Sätze schon sehr oft gehört, von vielen Senior-Entwicklern.

Wenn die Eingaben vollständig voneinander entkoppelt sind, mag es sinnvoll sein anzunehmen, dass sie nicht in Kombination getestet werden müssen. Aber oft sind zwei Eingaben, von denen man annimmt, dass sie entkoppelt sind, gar nicht so entkoppelt, wie man denkt.

Und selbst wenn die Implementierung zum Zeitpunkt des Schreibens eines Tests tatsächlich entkoppelt ist, kann sie sich zu einem späteren Zeitpunkt zu einer gekoppelten Implementierung entwickeln. Es ist wie in diesem Sprichwort: "Nicht das, was du nicht weißt, bringt dich in Schwierigkeiten, sondern das, was du sicher zu wissen glaubst, obwohl es gar nicht wahr ist."

Als allgemeine Heuristik könnte man sagen: Wenn zwei Eingaben innerhalb derselben Methode geparst werden, ist es definitiv sinnvoll, sie in Kombination zu testen.

Vielleicht haben Sie sich entschieden, dass es das Verhältnis von Risiko und Nutzen rechtfertigt, keine Tests für solche Kombinationen zu schreiben. Das kann durchaus eine vernünftige Entscheidung sein, abhängig von den jeweiligen Projektumständen. Aber seien Sie sich des Risikos bewusst, das Sie damit eingehen. Machen Sie sich nicht vor, es habe keinen Wert, Kombinationen von mehreren Ereignissen zu testen.

Test der Ausgabe_A für Ereignis_1 != Test des Ausgabe_A für Ereignis_2

Angenommen, Sie müssen die folgende addPerson-Methode testen:

public int getAge(String name); public int getHeight(String name); public int getWeight(String name); // Returns true if a previous value was overwritten public boolean addPerson(Person person);

Sie schreiben die folgenden Tests:

@Test public void addNewPerson_shouldReturnFalse() {  Person person = new Person("john", 30, 175, 70);  boolean result = system.addPerson(person);  Truth.assertThat(result).isFalse();  getAndCheckPerson(person); } @Test public void addPerson_alreadyExists_shouldReturnTrue() {  Person originalJohn = new Person("john", 30, 175, 70);  system.addPerson(originalJohn);  Person updatedJohn = new Person("john", 31, 174, 71);  boolean result = system.addPerson(updatedJohn);  Truth.assertThat(result).isTrue();  getAndCheckPerson(updatedJohn); } private void getAndCheckPerson(Person person) {  Truth.assertThat(system.getAge(person.name))    .isEqualTo(person.age);  Truth.assertThat(system.getHeight(person.name))    .isEqualTo(person.height);  Truth.assertThat(system.getWeight(person.name))    .isEqualTo(person.weight); }

Daraufhin kommt es zu folgendem Gespräch:

John: "Hey, warum prüfst du beim zweiten Test Alter/Größe/Gewicht noch einmal?"

Sie: "Warum nicht?"

John: "Wir haben bereits im ersten Test überprüft, ob Alter/Größe/Gewicht korrekt eingegeben wurden. Die einzige inkrementelle Änderung, die im zweiten Test geprüft werden muss, ist die Rückgabe des Booleschen Wertes. Sie können die anderen Prüfungen löschen."

Sie können sich selbst mit verschiedenen logischen Argumenten davon überzeugen, dass es unmöglich ist, dass die zusätzlichen Prüfungen im zweiten Test fehlschlagen, wenn der erste Test bestanden wurde – und diese zusätzlichen Prüfungen deshalb nicht nötig sind. "Ich habe mir den Code angesehen, und wir prüfen nicht einmal, ob die Person existiert, bevor wir sie blindlings einfügen und überschreiben, was schon da ist!"

Anzeige

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.

Der ganze Sinn des Schreibens automatisierter Tests ist, (potenziell fehlerhafte) Annahmen zu minimieren. Es geht darum, Sicherheitsnetze einzurichten für den Fall, dass später jemand beschließt, diesen Code umzuschreiben, den Sie überprüft und zu dem Sie mündlich eine Garantieerklärung abgegeben haben.

Abhängig von der spezifischen Implementierung, den Prioritäten Ihres Projekts und dem Aufwand, der mit der Durchführung der Checks verbunden ist, kann es ein vertretbares Risiko sein, sie zu überspringen. Machen Sie sich jedoch nicht vor, dass es kein Risiko gibt. Nur weil etwas für ein Ereignis gut funktioniert, heißt das nicht, dass es auch für alle anderen Ereignisse gut funktioniert. Wenn eine einzeilige Hilfsmethode hilft, solche Annahmen zu vermeiden, gibt es wenig Gründe, sie nicht zu verwenden.

Testen Sie jeden erdenklichen Sonderfall

Mir war nie bewusst, wie paranoid ich bin, bis ich mich dabei ertappte, wie ich immer wieder Gespräche wie dieses führte:

@Test public void removeElementFromCollection() {  var collection = MyCustomCollection.of(1);  collection.remove(1);  Truth.assertThat(collection).isEmpty(); }

"Woher weißt du, dass die Methode nicht die gesamte Sammlung löscht, statt nur das eine Element zu entfernen?"

@Test public void removeElementFromCollection_otherElementsRemaining() { var collection = MyCustomCollection.of(1, 2);  collection.remove(1);  Truth.assertThat(collection).contains(2); }

"Woher weißt du, dass die Methode nicht einfach jedes Mal das 1. Element rauszieht?"

@Test public void removeElementFromCollection_lastElementRemoved() { var collection = MyCustomCollection.of(1, 2); collection.remove(2); Truth.assertThat(collection).contains(1); }

"Woher weißt du, dass das in Fällen, in denen du das mittlere Element entfernen willst, gut funktioniert?"

All das mag pingelig erscheinen, aber es ist der Grad an Paranoia, der nötig ist, um eine sichere Testsuite zu bauen. Eine, die den Release-Prozess gegen eine Vielzahl von Bugs und durch Codeänderungen hervorgerufene Fehler absichern kann. Eine, bei der Sie sich wohlfühlen, wenn die Anwendung nach dem erfolgreichen Durchlauf des Tests ausgerollt wird (deploy-on-success) – und zwar egal, wie tiefgreifend die Änderungen sind.

Wenn eine Methode eine ganze Sammlung von Elementen zurückgibt, schreiben Sie nicht nur Tests, die eine Ausgabe mit einem einzigen Element erzeugen. Schreiben Sie Tests, von denen erwartet wird, dass sie eine ganze Reihe von Ausgaben produzieren, und überprüfen Sie dann, ob jedes einzelne davon auftaucht.

Schreiben Sie Tests, von denen erwartet wird, dass sie überhaupt keine Ausgaben erzeugen. Jede einzelne Prüfung mag trivial und unbedeutend erscheinen, aber in der Summe ergeben sie einen Sinn. Testen Sie jeden Sonderfall, den Sie sich vorstellen können.

White-Box-Tests zur Verbesserung der Testabdeckung

Eine kurze Einführung:

Black-Box-Tests(öffnet im neuen Fenster): Testen einer Methode rein auf Basis ihrer Spezifikation, ohne Rücksicht auf die konkrete Implementierung.

White-Box-Tests(öffnet im neuen Fenster): die Verwendung der spezifischen Implementierungsdetails, um die Testprioritäten festzulegen.

White-Box-Tests können, wenn sie richtig durchgeführt werden, die Testabdeckung erheblich verbessern, indem sie besser auf korrektes Verhalten in wichtigen Sonderfällen testen. Nehmen Sie zum Beispiel an, Sie testen die folgende Klasse:


public class MyCustomSet <T> implements Set <T> { … }

Reines Black-Box-Testen: Ich werde versuchen, verschiedene Elemente hinzuzufügen und zu entfernen, und dabei sicherstellen, dass diese Operationen alle verschiedenen Anforderungen des Set-Interface abdecken, unabhängig von der spezifischen Implementierung.

Besseres White-Box-Testing: Ich weiß, dass die Implementierung Hashing(öffnet im neuen Fenster) und Lineare Sondierung(öffnet im neuen Fenster) verwendet, um die gewünschte Funktionalität zu erreichen. Die kniffligsten Sonderfälle treten auf, wenn zwei verschiedene Elemente am gleichen Array-Offset kollidieren, und vor allem, wenn eines dieser zuvor eingefügten Elemente später wieder entfernt wird, wodurch ein Tombstone-Eintrag entsteht. Daher werde ich zusätzlich zu den obigen Black-Box-Tests spezifische Tests mit spezifischen Eingaben schreiben, die diese kniffligen Sonderfälle auslösen.

Der erste Ansatz kann angemessen sein, wenn eine ausreichend große Testsuite verwendet wird. Mit dem zweiten Ansatz ist es jedoch wahrscheinlicher, dass Bugs mit einer viel kleineren Testsuite gefunden werden, indem die spezifischen Sonderfälle, die am wahrscheinlichsten auftreten, identifiziert und ausgelöst werden.

Besonders umstritten sind White-Box-Tests, wenn sie dazu verwendet werden, die Testsuite zu schwächen, anstatt sie zu stärken. Am häufigsten werden Sie so etwas wie dies hören: "Ich weiß, dass die Implementierung die Funktionalität XYZ mit der Implementierung ABC erreicht, und wenn man sich die Implementierung ABC ansieht, ist es offensichtlich, dass sie wie beabsichtigt funktioniert. Daher müssen wir uns nicht darum kümmern, Tests zu schreiben, die die Funktionalität XYZ abdecken."

Typischerweise wird in solchen Fällen davon ausgegangen, dass ABC sicher ist, entweder weil es sehr einfach ist oder weil es zuverlässige Bibliotheken verwendet. Wenn es richtig gemacht wird, kann es dabei helfen, Bereiche mit höherem/niedrigerem Risiko zu identifizieren und entsprechend zu priorisieren. Wenn es falsch gemacht wird, kann das zu gefährlichen Löchern in Ihrer Testabdeckung führen. Denn es besteht immer das Risiko, dass Sie etwas übersehen haben oder dass jemand den Code später in einer Art und Weise umschreibt, die Ihre Annahmen über den Haufen wirft.

White-Box-Tests als Rechtfertigung für die Vernachlässigung bestimmter Sonderfälle zu verwenden, hat zwei Seiten. Es kann ein notwendiges Übel sein, wenn die Zeit knapp ist (aber es ist besser, sich nicht zu sehr darauf zu verlassen). Auf der anderen Seite können White-Box-Tests Ihre Testsuite enorm verbessern und Ihre Codebasis wirklich sicher machen.

Integrationstests sind Gold wert

"Kennen Sie den Unterschied zwischen Theorie und Praxis? In der Theorie gibt es das nicht. In der Praxis schon." Dieser Spruch passt genau auf Unit- und Integrationstests: In der Theorie können Unit-Tests genau die gleiche Abdeckung liefern, die Sie mit Integrationstests haben. In der Praxis tun sie das aber nicht.

Hardware-Teams haben das im Laufe der Jahre schmerzlich gelernt, deshalb wird bei keinem Hardwareprojekt an den Integrationstests gespart. Egal wie gründlich Sie in Ihren Unit-Tests sind, Sie werden Fehler finden, wenn Sie Integrationstests durchführen.

Viele Softwareentwickler, vor allem die klügeren, haben das noch nicht akzeptiert. "Wenn wir nur einen wirklich guten Job bei den Unit-Tests machen, werden wir keine Integrationstests brauchen!", sagen sie. Ich sage: leider nein. Bei jedem Projekt mit hoher Komplexität werden Sie nie gut genug sein können.

Anzeige

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.

Sie werden immer wieder Fakes bauen, die sich von der echten Komponente unterscheiden, auf eine Art, die sich als subtil, aber entscheidend herausstellen wird. Sie werden es immer wieder versäumen, das katastrophale Verhalten vorherzusehen, das aus scheinbar harmlosen Änderungen resultieren kann.

Ich war einmal in einem Team mit brillanten und sehr fähigen Entwicklern, die sich voll und ganz auf Unit-Tests konzentrierten und Integrationstests buchstäblich untersagten. Wir hatten nahezu perfekte Testabdeckungsmetriken, aber irgendwie funktionierte in der Produktion immer wieder mal etwas nicht – und zwar mitunter auf eine ziemlich desaströse Art und Weise.

Wir wurden immer paranoider, machten viele "unnötige" Änderungen und verbrachten immer mehr Zeit mit manuellen Tests. Nichts schien zu funktionieren. Erst als wir eine End-to-End-Testsuite zusammenstellten, wurde es endlich besser. In der Folge fügten wir alle möglichen neuen Funktionen und Codeänderungen ein und unsere Testsuite ließ uns nie im Stich.

Das ist nur ein Beispiel, aber nicht das einzige. So gibt es etwa einen großartigen Artikel von den Entwicklern des Rust-Compilers darüber, wie sie es schafften, alle sechs Wochen eine neue stabile Version zu produzieren, obwohl die meisten anderen Compiler viel längere Release-Zyklen haben.

Sie schreiben einen großen Teil ihres Erfolges den End-to-End-Tests zu(öffnet im neuen Fenster). Sie hatten eine solide Suite von Unit-Tests aufgebaut und dennoch traten einige größere Fehler auf, die nur durch End-to-End-Tests gefunden wurden. Durch die Verbesserung der Effektivität ihrer Testsuite waren sie in der Lage, sowohl größere Produktionsfehler zu verhindern als auch ihre Entwicklungszyklen zu beschleunigen – eine echte Win-Win-Lösung, die wir alle anstreben sollten.

Stärken und Beschränkungen

Ironischerweise werden Sie trotz meiner obigen Bekehrungsversuche feststellen, dass die meisten Hardwaretests auf der Ebene der Units (Cluster) durchgeführt werden. Warum ist das so? Bestätigt das nicht den vorherrschenden Ansatz der Softwareindustrie, ebenfalls Unit-Tests vorzuziehen?

Der Kontext ist hier entscheidend. Bei Hardware kann ein Unit-Test (Cluster) in 5 bis 15 Minuten abgeschlossen sein, während Integrationstests (Full-Chip) viele Stunden, manchmal sogar Tage dauern. Aus diesem Grund werden die meisten Hardwaretests auf Unit-Ebene durchgeführt.

Bei Software kann eine ganze Suite von End-to-End-Tests in etwa 30 Sekunden ausgeführt werden und die gesamte, projektweite Suite in fünf Minuten – kaum genug Zeit für einen Entwickler, sich einen Kaffee zu holen, und der Traum eines jeden Hardwaretesters. "Sie wollen mir sagen, dass ich in 30 Sekunden eine ganze Sammlung von End-to-End-Tests ausführen kann, ohne viel Zeit und Mühe in das Schreiben von Tests für jede einzelne Unterkomponente und in das Erstellen/Einrichten/Debuggen von Mocks und Fakes zu investieren, die die Feinheiten des echten Geräts nur unzureichend nachahmen?"

Unit-Tests haben sicher ihre Berechtigung in einer Testsuite. Vor allem wenn es darum geht, obskure Fehlerzustände (zum Beispiel Netzwerk-Timeouts) und andere seltene Fälle zu reproduzieren, die in einem realen System nur schwer zu erzeugen sind.

Das A und O Ihrer Testsuite sollten jedoch Integrationstests sein. Sie können nicht nur Ihre gesamte Codebasis mit einer weitaus kleineren und einfacheren Testsuite abdecken, sondern auch eine grundsolide Abdeckung der komplexen Interaktionen zwischen verschiedenen Komponenten erreichen. Interaktionen, die wir auf der Unit-Ebene viel zu leicht übersehen. ''Schreiben Sie Tests. Nicht zu viele. Hauptsächlich Integration.''(öffnet im neuen Fenster)

Zufallstests: Was die Amateure von den Profis unterscheidet

An dieser Stelle fragen Sie sich vielleicht, wie all die oben genannten Ratschläge überhaupt umgesetzt werden können."Jede Kombination von Ereignissen testen? Jeden Ausgang für jede Kombination von Ereignissen testen? Jeden möglichen Sonderfall testen? Dazu bräuchte man eine enorme Anzahl von Tests!"

Das stimmt ... aber nur wegen anderer Einschränkungen, die sich Softwareentwickler selbst auferlegen. Wie zum Beispiel die Regel, dass alle Tests zu 100 Prozent deterministisch sein sollten, ohne Platz für Zufälliges(öffnet im neuen Fenster).

In der Hardware-Welt würde man für eine solche Regel ausgelacht werden. Bei jedem größeren Hardwareprojekt sind "gezielte" Tests ein Muss, um sicherzustellen, dass der Chip nicht defekt ist. Aber wenn Sie milliardenschwere Rückrufe wirklich vermeiden wollen, müssen Sie sich weiterentwickeln(öffnet im neuen Fenster) und auch randomisierte Tests(öffnet im neuen Fenster) einführen. Das ist eine Lektion, die die meisten Software-Teams erst noch lernen müssen, wobei einige Unternehmen wie Dropbox schon dabei sind(öffnet im neuen Fenster).

Warum randomisierte Tests funktionieren

Es gibt zwei hauptsächliche Vorteile von randomisierten Tests, die zugleich erklären, warum sie ein so wesentlicher Bestandteil jedes Hardware-Verifizierungs-Toolkits sind.

Der erste besteht in der Minimierung der Testfülle. Betrachten Sie die Divisionsmethode von weiter oben: Um sie erschöpfend zu testen, könnten Sie Dutzende gerichteter Tests schreiben, die eine Vielzahl von Szenarien abdecken. Wahrscheinlich könnten die meisten durch einen einfachen Test eliminiert werden, der einen zufälligen Dividenden und einen zufälligen Divisor auswählt und die Ausgabe mit der Ausgabe eines Referenzmodells vergleicht. Sie können diesen Test dann tausendmal loopen und am Ende etwas erhalten, das Ihnen genauso viel Sicherheit gibt wie die meisten Ihrer mühsam geschriebenen, gerichteten Tests.

Der zweite Vorteil ist etwas subtiler. Wenn Sie gerichtete Tests schreiben, vermindern Sie die Risiken, die sich durch bekannte Unbekannte ergeben. Sie zählen zuerst alle Sonderfälle auf, die Sie sich vorstellen können, und schreiben dann Tests für jeden davon. Das funktioniert hervorragend, um die bekannten Risiken zu minimieren, aber es versagt völlig, wenn es um die unbekannten Unbekannten(öffnet im neuen Fenster) geht. Per Definition können Sie keine gezielten Tests schreiben, um unbekannte Risiken zu anzugehen. Denn weil sie nicht vorhergesehen wurden, haben Sie nicht daran gedacht, einen Test für sie zu schreiben.

Vielleicht haben Sie zum Beispiel daran gedacht, für den Fall zu testen, dass der Zähler null ist, und auch für den Fall, dass der Nenner null ist – aber nicht für den Fall, dass beide null sind.

Hier bietet das randomisierte Testen einen großen Nutzen. Indem Sie Ihre Eingaben randomisieren, testen Sie eine extrem große Vielfalt an Eingabekombinationen. Darunter auch solche, von denen Sie nicht erwartet haben, dass sie problematisch sind, die es aber sind. So können Sie die Testabdeckung deutlich erhöhen, auch für die verborgenen, unbekannten Risiken.

Aber was ist mit der Konsistenz?

Ein häufiger Einwand gegen Zufälligkeit ist, dass sie zu lückenhaften Tests führen kann. Solche Kritik geht am Sinn des Testens vorbei. Das Ziel des Testens ist ja nicht, eine deterministische Testsuite zu haben. Das Ziel ist es, Fehler zu finden. Ein lückenhafter Test ist ein Ärgernis. Ein Test, der trotz des Vorhandenseins von Fehlern durchläuft, ist katastrophal. Alles, was das Risiko des Letzteren reduziert, ist gut.

Wenn Sie einen Fehler in Ihrer Testsuite bemerken, sollte dieser durch Debuggen und Beheben der Grundursache behoben werden. Wenn die vorhandene Fehlermeldung und die Protokolle nicht ausreichen, können Sie Ihre Assertion und die Protokollierung aktualisieren, um die benötigten Debug-Informationen zu erhalten. Falls erforderlich, können Sie den Fehler auch manuell auslösen, indem Sie den Test in einer Schleife laufen lassen, bis er fehlschlägt. Auf diese Weise haben Sie alle Debug-Informationen, die Sie brauchen, um die Fehlerursache zu finden, sie zu beheben und Ihre Testergebnisse zu bereinigen.

Anzeige

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.

Eine kurze Anmerkung zur Konsistenz der Testabdeckung: Das ist ein erstrebenswertes Ziel und kann auf zwei Arten angegangen werden. Die erste ist, jeden einzelnen Test in einer Schleife x-mal laufen zu lassen, wobei x die minimale Anzahl ist, die Ihnen die Menge an Abdeckung gibt, die Sie von diesem speziellen Test benötigen.

Die zweite Möglichkeit ist, die gesamte Testsuite x-mal laufen zu lassen, um den gleichen Effekt auf eine etwas gröbere Weise zu erzielen. Unternehmen wie Intel lassen ihre Testsuite in einer Endlosschleife laufen und beauftragen Ingenieure damit, alle auftretenden Fehler zu debuggen und zu beheben. Mit einer Kombination aus beiden Techniken können Sie eine robuste Abdeckung sicherstellen, bevor Sie Änderungen implementieren.

Eine Bemerkung am Rande: Hardware-Testsuites versehen üblicherweise alle RNGs mit einem konsistenten Seed und geben diesen Seed dann bei Fehlern aus. Auf diese Weise können Sie jeden Fehler reproduzieren, indem Sie den fehlerhaften Seed wieder verwenden. Das wird oft gemacht, weil ein Loop hier nicht praktikabel ist – ein einziger Test könnte viele Stunden in Anspruch nehmen. Ich persönlich habe diese Funktionalität in den Softwareprojekten, an denen ich gearbeitet habe, noch nicht gebraucht. Es wäre aber sicher gut, sie zu haben.

Kampfspuren

Hier einmal ein peinliches Beispiel für einen Fehler, den wir dank zufälliger Tests gefunden haben. Wir hatten einen S3-Uploader, der eine vom Benutzer bereitgestellte Datei aufnimmt, sie in einen File-Input-Stream konvertiert, das AWS S3 SDK mit diesem Input-Stream aufruft, die S3-URL durch Verkettung von Bucket, Pfad und Dateiname ermittelt und diese URL dann zurückgibt.

Bei den ersten gezielten Tests funktionierte alles einwandfrei. Erst als wir anfingen, die Testeingaben zu randomisieren und Integrationstests zu schreiben, die den Inhalt der zurückgegebenen URL herunterluden, bekamen wir Fehlermeldungen. Das Debuggen dieser Fehler führte uns zu einem Bug der Art "Wie konnten wir das nur übersehen?". Denn das obige Schema konnte nicht mit von den Nutzern bereitgestellten Dateien umgehen, deren Namen Leerzeichen enthielten(öffnet im neuen Fenster). Wie sollte es auch, wenn URLs keine Leerzeichen enthalten dürfen.

Im Nachhinein scheint das Problem ganz offensichtlich zu sein. Natürlich können Dateinamen Leerzeichen enthalten, URLs jedoch nicht, das muss man doch berücksichtigen! Die meisten Softwarefehler treten jedoch nicht in Szenarien auf, die Sie berücksichtigt haben, sondern in Szenarien, über die man nie nachgedacht hat. Hätten wir nur gerichtete Tests mit einem S3-Mock verwendet, hätten wir diesen Fehler nie vor der Veröffentlichung gefunden. Es bedurfte eines Integrationstests mit zufälligen Eingaben, um diesen Fehler aufzudecken und zu beheben.

Zufallsgrade

Es gibt viele verschiedene "Ebenen" der Randomisierung und jede hat ihre eigenen Komplexitätskosten und Abdeckungsvorteile. Sie können von Fall zu Fall entscheiden, wie weit Sie randomisieren wollen, um die Abdeckung und Zuverlässigkeit zu maximieren, ohne dass alles zu komplex wird.

Ein Beispiel: Nehmen wir an, Sie bauen eine benutzerdefinierte Listenimplementierung(öffnet im neuen Fenster) und möchten überprüfen, ob die Contains-Methode(öffnet im neuen Fenster) korrekt funktioniert. Hier ist die Art von gerichteten Tests, die ich in den meisten Softwareprojekten sehen würde:



@Test
public void contains_directedTest_noRandomization()
 {
List<Integer> list = MyCustomList.of(4, 5, 6);
list.add(7); Truth.assertThat(list).contains(7);
 }

Nehmen wir an, dass wir uns entschieden haben, einige randomisierte Eingaben anzuwenden. Hier für den Anfang eine einfache Möglichkeit:



@Test public void contains_randomizeElements() {
List<Integer> list = MyCustomList.of(RNG.nextInt(), RNG.nextInt(), RNG.nextInt());
int valueToAdd = RNG.nextInt();
list.add(valueToAdd); Truth.assertThat(list).contains(valueToAdd);
}

Der Test ist dem gerichteten Test immer noch ähnlich, außer dass wir die hartcodierten durch zufällige Zahlen ersetzt haben. Oberflächlich betrachtet, bringt uns das keine größere Abdeckung. Aber es ist ein Anfang und es kostet uns fast nichts. Und auch wenn wir es vielleicht nicht merken, bietet es uns Abdeckung für doppelte Elemente und obskure Sonderfälle wie Vergleiche für große Integers, die sich anders verhalten als kleine(öffnet im neuen Fenster).



@Test public void contains_randomizeElementsAndSize() {

List<Integer> list = MyCustomList.of(); int size = pickRandomSize();

// Biased RNG that equally weights empty/small/large sizes

for (int i=0; i<size; i++) {

list.add(RNG.nextInt());

}

int valueToAdd = RNG.nextInt(); list.add(valueToAdd); Truth.assertThat(list).contains(valueToAdd);

}

Jetzt kommen wir voran: Wir haben nicht nur den Inhalt der Liste randomisiert, sondern wir randomisieren jetzt auch die Größe der Liste und decken damit die Bandbreite von Einzelelementlisten bis zu sehr großen Listen ab. Wenn es irgendwelche Fehler in den Größenüberprüfungen gibt, ist es viel wahrscheinlicher, dass sie gefunden werden. Wir erhalten sogar eine Abdeckung für Sonderfälle wie naiv-rekursive Implementierungen, die einen Stack Overflow erzeugen(öffnet im neuen Fenster).



@Test
public void contains_randomizeElementsSizeAndPosition() {
  List<Integer> list = MyCustomList.of();
  int size = pickRandomSize();   // Biased RNG that equally weights empty/small/large sizes
  for (int i=0; i<size; i++) {
    list.add(RNG.nextInt());
  }
  int valueToAdd = RNG.nextInt();
  int index = RNG.nextInt(i + 1);
  list.add(index, valueToAdd);
  Truth.assertThat(list).contains(valueToAdd);
}

Warum nur die Listengröße randomisieren, wenn wir auch die Position des gesuchten Elements randomisieren können? Jetzt haben wir die Abdeckung, die wir brauchen, um eine noch größere Vielfalt an Off-by-One-Fehlern abzufangen(öffnet im neuen Fenster). Aber wir sind noch nicht fertig:



@Test
public void contains_randomizeElementsSizeAndPosition_moreCoverage() {
  for (int i=0; i<1000; i++) {
    contains_randomizeElementsSizeAndPosition();
  }
}

Ein einziger Aufruf des Basistests gibt uns nicht die nötige Abdeckung. Es gibt zu viele Kombinationen von leeren/kleinen/großen Größen, mit kleinen/großen Einträgen, die eindeutig/dupliziert sind, und der Suche nach etwas am Anfang/in der Mitte/am Ende. Deshalb verpacken wir es in einen Meta-Test, der 1.000-mal durchläuft. So stellen wir sicher, dass ein einziger erfolgreicher Durchlauf die nötige Sicherheit bietet.

Alles zusammengefügt

Mit jedem Grad der Randomisierung steigt Ihre Testkomplexität, aber auch die Zuverlässigkeit der Tests. Wenn Sie an Tests gewöhnt sind, die die meisten, aber nicht alle Fehler abfangen, mögen diese Techniken unnötig erscheinen. Wenn Sie aber ein höheres Maß an Zuverlässigkeit anstreben, sind solche Techniken unerlässlich.

Unabhängig davon, wo Sie die Grenze ziehen wollen: Es ist fast nie die richtige Antwort, jegliche Randomisierung komplett auszuschließen. In den meisten Fällen können Sie einige Eingaben randomisieren, um die Abdeckung zu erhöhen, und die Komplexität dabei nur minimal erhöhen. Der letzte, oben gezeigte Test zum Beispiel erreicht auf sehr kompakte Weise, was sonst Dutzende gerichteter Tests erfordern würde. Und selbst dann würden Ihre gerichteten Tests wahrscheinlich einige Sonderfälle auslassen.

Mein größter Moment als Verifikationsingenieur war, als ich einen komplett obskuren Fehler in dem System aufdeckte, das ich testete. Der Fehler trat nur bei einer kleinen Menge einander überschneidender Sonderfälle auf. Man musste eine sehr spezifische Operation durchführen, wobei ein bestimmtes Flag aktiviert sein musste, die Größe der Operation musste über einem bestimmten Schwellenwert liegen und die betroffene Speicheradresse musste sich leicht mit einer anderen Speicherseite überschneiden.

Ich hätte Jahre damit zubringen können, einen hartcodierten Test zu schreiben, und hätte trotzdem nie daran gedacht, diese spezielle Kombination von Szenarien zu testen. Da ich jedoch Tests mit zufälligen Eingaben geschrieben hatte, konnten wir den Fehler vor der Produktion beheben.

Verwendung von Referenzmodellen

Eine der größten Fragen, die sich stellen, wenn Sie mehr und mehr zufällige Tests durchführen, ist: Wie kann der Test herausfinden, wie die richtige Antwort lauten sollte?

Zurück zu dem Divisionsbeispiel: Wenn wir einen gerichteten Test für Divider.divide (27.0, 3.0) geschrieben haben, können wir manuell ableiten und prüfen, ob die Antwort 9.0 lautet. Wenn wir aber zufällige Eingaben verwenden, wie können wir dann herausfinden, was die richtige Antwort wäre? Mit einem dynamisch generierten/aktualisierten Referenzmodell.

Ich habe viele Testrichtlinien gesehen, die sich strikt gegen ein dynamisches Referenzmodell aussprechen, das das erwartete Ergebnis liefert. In einem Punkt sind sie sicherlich richtig: Wenn Ihr Testreferenzmodell dem tatsächlichen Produktionscode ähnelt, enthält es genau die gleichen Fehler wie Ihr Produktionscode und führt zu falsch-positiven Ergebnissen.

Die Lösung besteht jedoch nicht darin, Referenzmodelle vollständig abzulehnen, sondern welche zu verwenden, die sich ausreichend von Ihrem Produktionscode unterscheiden, um zu vermeiden, dass Fehler repliziert werden.

Angenommen, Sie möchten eine CRUD-API testen, um ein Ereignis zu erstellen, andere Benutzer zu dem Ereignis einzuladen und alle RSVPs zu bekommen. Die reale API führt all dies mithilfe von Datenbankerstellungen/-aktualisierungen/-suchen sowie verschiedenen Analysen der Ergebnisse durch. Hier gibt es viel Raum für Fehler. Ein Referenzmodell kann hingegen einfache POJOs und In-Memory-Speicher wie Hashmaps verwenden. Dieses Referenzmodell kann dann mit den tatsächlichen Daten verglichen werden, die vom Integrationstest zurückgegeben wurden.

Es ist richtig, dass die Verwendung von Referenzmodellen die Komplexität Ihrer Tests erhöht. Leider ist das oft ein notwendiges Übel. Die Vorteile bei der Abdeckung, die wir durch zufällige Tests mit Referenzmodellen erhalten, sind viel zu groß, um ein allgemeines Verbot zu rechtfertigen. Bei großen Hardwareprojekten wird das häufig genau so gemacht – eben wegen der großen Vorteile.

Testen Sie einen gesamten Pfad, nicht nur eine einzelne Ausgabe

Angenommen, Sie haben ein System, in dem die folgende Kette gekoppelter Ereignisse auftreten kann: Ai -> Bi -> Ci -> Di -> Ei

Und Sie möchten testen, ob die obige, spezifische Sequenz von Eingaben die folgende Kette von Ausgaben erzeugt: Ao -> Bo -> Co -> Do -> Eo

Sie können entweder die folgende Testsequenz schreiben:

@Test public void testA() {  System system = new System();  Output output = system.apply(A_I);  Truth.assertThat(output).isEqualTo(A_O); } @Test public void testB() {  System system = new System();  Output output = system.apply(A_I);  output = system.apply(B_I);  Truth.assertThat(output).isEqualTo(B_O); } @Test public void testC() {  System system = new System();  Output output = system.apply(A_I);  output = system.apply(B_I);  output = system.apply(C_I);  Truth.assertThat(output).isEqualTo(C_O); } @Test public void testD() {  System system = new System();  Output output = system.apply(A_I);  output = system.apply(B_I);  output = system.apply(C_I);  output = system.apply(D_I);  Truth.assertThat(output).isEqualTo(D_O); } @Test public void testE() {  System system = new System();  Output output = system.apply(A_I);  output = system.apply(B_I);  output = system.apply(C_I);  output = system.apply(D_I);  output = system.apply(E_I);  Truth.assertThat(output).isEqualTo(E_O); }

Oder Sie schreiben einen Test, der alles abdeckt:

@Test public void testABCDE() {  System system = new System();  Output output = system.apply(A_I);  Truth.assertThat(output).isEqualTo(A_O);  output = system.apply(B_I);  Truth.assertThat(output).isEqualTo(B_O);  output = system.apply(C_I);  Truth.assertThat(output).isEqualTo(C_O);  output = system.apply(D_I);  Truth.assertThat(output).isEqualTo(D_O);  output = system.apply(E_I);  Truth.assertThat(output).isEqualTo(E_O);

Wenn Sie die von vielen Entwicklern gepredigte Regel(öffnet im neuen Fenster) "Eine Assertion pro Test" befolgen würden, wären Sie gezwungen, die erste Option zu wählen. Ich weiß nicht, was Sie bevorzugen, aber ich finde die zweite Option bei Weitem besser.

Sie ist viel skalierbarer, insbesondere wenn Sie komplexe Systeme mit langen Ereignisketten oder Ähnlichem haben, die in jeder Phase überprüft werden müssen. Bei Regeln wie den oben genannten ist es kein Wunder, dass Entwickler beim Testen Abkürzungen nehmen – das Befolgen aller vorgeschriebenen Regeln ist viel zu aufwändig!

Wenn Sie mit einem Test-Framework arbeiten, bei dem die einzige Information, die zur Verfügung gestellt wird, der Name des fehlgeschlagenen Tests ist, wäre diese Argumentation vielleicht gültig. Glücklicherweise bieten die meisten modernen Test-Frameworks viel mehr Debug-Informationen.

Ein gut geschriebener Test sollte Fehlermeldungen erzeugen, die klar angeben, wo im Test er fehlgeschlagen ist, warum, und was die Unterschiede zwischen den erwarteten und den tatsächlichen Ausgaben sind. Das Debuggen des Testfehlers sollte dann eine einfache Angelegenheit sein – nämlich indem man sich schlicht die Fehlermeldung ansieht.

Anzeige

Handbuch für Softwareentwickler: Das Standardwerk für professionelles Software Engineering

Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.

Prägnanz in Ihrer Test-Codebasis ist extrem wertvoll, hauptsächlich aus den gleichen Gründen wie bei der Produktions-Codebasis. Daher beinhalten Hardwaretests oft Hunderte verschiedene Prüfungen, die alle zu unterschiedlichen Zeiten ausgeführt werden und unterschiedliche Dinge überprüfen, in einem einzigen Test.

Angesichts der riesigen Menge an Abdeckung, die benötigt wird, ist es unrealistisch, für jede einzelne Ereignis-Ergebnis-Kombination dedizierte Tests zu schreiben. Verzichten Sie auf dogmatische Regeln, die zu einer ausufernden Menge an Tests führen. Es ist in Ordnung, wenn ein einzelner Test mehrere Dinge entlang eines einzelnen Codepfades überprüft.

Wie viele 9en streben Sie an?

"Automatisierte Integrationstests für alle Funktionen schreiben? Jeden möglichen Eckfall testen? Zufällige Eingaben und Referenzmodelle? Ist das alles wirklich notwendig?"

Das ist eine gute Frage und die Antwort lautet: Es kommt darauf an.

Beim Systemdesign ist die erste Frage, die wir uns stellen, wie viele 9en der Zuverlässigkeit wir anstreben. Und wenn die Antwort hoch genug ist, entwerfen wir fantastisch komplexe Systeme, um diese Ziele zu erreichen. Das gleiche Prinzip gilt für das Testen. Je zuverlässiger Ihre Testsuite sein soll, desto komplexere Techniken müssen Sie einsetzen, um diese Ziele zu erreichen.

Wenn es für Ihr Projekt kein großes Problem ist, dass regelmäßig Sonderfall-Fehler in die Produktion kommen, können Sie mit den gleichen Testmethoden auskommen, die von den meisten Softwareprojekten verwendet werden. Wenn Sie aber eine wirklich sichere Testsuite erstellen wollen, eine, die Fehler in der Produktion extrem selten werden lässt, müssen Sie auf hohe Zuverlässigkeitswerte abzielen. Sie müssen Integrationstests, zufällige Eingaben und Referenzmodelle einbeziehen. Sie müssen paranoid sein, wenn es darum geht, alles und jedes zu testen, was schiefgehen könnte.

In vielen Fällen werden Sie feststellen, dass die Verwendung von Integrationstests und randomisierten Eingaben die Testabdeckung tatsächlich verbessert und gleichzeitig die Entwicklungszeit und den Umfang der Tests reduziert.

In anderen Fällen, wenn Sie versuchen, das letzte Quäntchen Zuverlässigkeit herauszuquetschen, wird Ihre Testsuite immer komplexer werden. Auf der anderen Seite werden Sie jedoch so viel Vertrauen in Ihre Testsuite haben, dass Sie größere Code-Änderungen machen können – und zwar mit minimalen manuellen Tests und Problemen.

Es gibt hier kein Richtig oder Falsch. Je nachdem, wie viel Zuverlässigkeit Sie anstreben, können Sie von Fall zu Fall Kompromisse zwischen Komplexität, Verbosität und Abdeckung eingehen, indem Sie viele der oben beschriebenen Techniken anwenden. Seien Sie ehrlich zu sich selbst, was die Prioritäten Ihres Projekts angeht, und entscheiden Sie dann, welche Opfer Sie zu bringen bereit sind, um diese zu erreichen.

Weiterführende Links:

Der Einsatz von randomisierten Tests bei Dropbox(öffnet im neuen Fenster), um die Abdeckung ihrer Sync-Funktionalität zu verbessern

jqwik(öffnet im neuen Fenster) – Eigenschaftsbasierte Testbibliothek für Java (Dank an Dan Turner für die Empfehlung)

QuickTheories(öffnet im neuen Fenster) – Eine weitere eigenschaftsbasierte Testbibliothek für Java

Online-Diskussionsforen:

/r/programming – 2019/05(öffnet im neuen Fenster)

/r/programming – 2019/11(öffnet im neuen Fenster)


Relevante Themen