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!"

Stellenmarkt
  1. Referentin / Referent für den Bereich Organisations- und Informationsmanagement (m/w/d)
    Kommunale Gemeinschaftsstelle für Verwaltungsmanagement (KGSt), Köln
  2. Systemanalytikerin / Systemanalytiker (m/w/d)
    Bürgerschaft der Freien und Hansestadt Hamburg, Hamburg
Detailsuche

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.

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 und auch randomisierte Tests einführen. Das ist eine Lektion, die die meisten Software-Teams erst noch lernen müssen, wobei einige Unternehmen wie Dropbox schon dabei sind.

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.

Golem Akademie
  1. Webentwicklung mit React and Typescript
    6.-10. Dezember 2021, online
  2. Scrum Product Owner: Vorbereitung auf den PSPO I (Scrum.org)
    24.-25. November 2021, online
  3. IT-Sicherheit für Webentwickler
    2.-3. November 2021, online
Weitere IT-Trainings

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 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.

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

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. 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 und möchten überprüfen, ob die Contains-Methode korrekt funktioniert. Hier ist die Art von gerichteten Tests, die ich in den meisten Softwareprojekten sehen würde:

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

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

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

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.

  1. @Test public void contains_randomizeElementsAndSize() {
  2.  
  3. List<Integer> list = MyCustomList.of(); int size = pickRandomSize();
  4.  
  5. // Biased RNG that equally weights empty/small/large sizes
  6.  
  7. for (int i=0; i<size; i++) {
  8.  
  9. list.add(RNG.nextInt());
  10.  
  11. }
  12.  
  13. int valueToAdd = RNG.nextInt(); list.add(valueToAdd); Truth.assertThat(list).contains(valueToAdd);
  14.  
  15. }

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.

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

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. Aber wir sind noch nicht fertig:

  1. @Test
  2. public void contains_randomizeElementsSizeAndPosition_moreCoverage() {
  3. for (int i=0; i<1000; i++) {
  4. contains_randomizeElementsSizeAndPosition();
  5. }
  6. }

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.

Bitte aktivieren Sie Javascript.
Oder nutzen Sie das Golem-pur-Angebot
und lesen Golem.de
  • ohne Werbung
  • mit ausgeschaltetem Javascript
  • mit RSS-Volltext-Feed
 White-Box-Tests zur Verbesserung der TestabdeckungVerwendung von Referenzmodellen 
  1.  
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7.  


xUser 20. Feb 2021

UI Tests sind prinzipbedingt langsam. Außerdem sind ihre Failures eher unspezifisch zum...

NoGoodNicks 13. Feb 2021

Korrekt. Und wenn es nicht der Tester ist, dann ist es der Integrator. Der braucht immer...

freebyte 09. Feb 2021

Dann muss sich in den letzten Monaten etwas geändert haben, was ich nicht mitbekommen...

Steffo 09. Feb 2021

Kann mich dem nur anschließen. Ich teste auch ziemlich ausführlich und bin für jeden...

Netzweltler 09. Feb 2021

Daher wird man auch in Zukunft Tests der Software so weit wie möglich beschränken. Daher...



Aktuell auf der Startseite von Golem.de
Infiltration bei Apple TV+
Die Außerirdischen sind da!

Nach Foundation wartet Apple innerhalb kürzester Zeit gleich mit der nächsten Science-Fiction-Großproduktion auf. Diesmal landen die Aliens auf der Erde.
Eine Rezension von Peter Osteried

Infiltration bei Apple TV+: Die Außerirdischen sind da!
Artikel
  1. Truth Social: Trumps soziales Netz bekommt Probleme mit Hackern und Lizenz
    Truth Social
    Trumps soziales Netz bekommt Probleme mit Hackern und Lizenz

    Hacker starten in Trumps-Netzwerk einen "Online-Krieg gegen Hass" mit Memes. Der Code scheint illegal von Mastodon übernommen worden zu sein.

  2. Krypto: NRW versteigert beschlagnahmte Bitcoin
    Krypto
    NRW versteigert beschlagnahmte Bitcoin

    Nordrhein-Westfalen hat Bitcoin im achtstelligen Eurobereich beschlagnahmt und will diese jetzt loswerden - im Rahmen einer Auktion.

  3. Rust, Deepfake, Sony, Microsoft: Konsolen-Termin für Among Us, mehr Speicher für die Xbox
    Rust, Deepfake, Sony, Microsoft
    Konsolen-Termin für Among Us, mehr Speicher für die Xbox

    Sonst noch was? Was am 22. Oktober 2021 neben den großen Meldungen sonst noch passiert ist, in aller Kürze.

Du willst dich mit Golem.de beruflich verändern oder weiterbilden?
Zum Stellenmarkt
Zur Akademie
Zum Coaching
  • Schnäppchen, Rabatte und Top-Angebote
    Die besten Deals des Tages
    Daily Deals • Seagate SSDs & HDD günstiger (u. a. ext. HDD 14TB 326,99) • Dualsense PS5-Controller Weiß 57,99€ • MacBook Pro 2021 jetzt vorbestellbar • World of Tanks jetzt mit Einsteigerparket • Docking-Station für Nintendo Switch 9,99€ • Alternate-Deals (u. a. iPhone 12 Pro 512GB 1.269€) [Werbung]
    •  /