• IT-Karriere:
  • Services:

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. über grinnberg GmbH, Stuttgart
  2. Endress+Hauser Conducta GmbH+Co. KG, Gerlingen (bei Stuttgart)

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.

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.  


Anzeige
Hardware-Angebote
  1. (reduzierte Überstände, Restposten & Co.)

xUser 20. Feb 2021 / Themenstart

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

NoGoodNicks 13. Feb 2021 / Themenstart

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

freebyte 09. Feb 2021 / Themenstart

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

Steffo 09. Feb 2021 / Themenstart

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

Netzweltler 09. Feb 2021 / Themenstart

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

Kommentieren


Folgen Sie uns
       


Immortals Fenyx Rising - Fazit

Im Video zeigt Golem.de das Actionspiel Immortals Fenyx Rising.

Immortals Fenyx Rising - Fazit Video aufrufen
AOC Agon AG493UCX im Test: Breit und breit macht ultrabreit
AOC Agon AG493UCX im Test
Breit und breit macht ultrabreit

Der AOC Agon AG493UCX deckt die Fläche zweier 16:9-Monitore in einem Gerät ab. Dafür braucht es allerdings auch ähnlich viel Platz.
Ein Test von Mike Wobker

  1. Agon AG493UCX AOC verkauft 49-Zoll-Ultrawide-Monitor mit USB-C und 120 Hz

Logitech vs. Cherry: Leise klackert es im Büro (oder auch nicht)
Logitech vs. Cherry
Leise klackert es im Büro (oder auch nicht)

Tastaturen für die Büroarbeit brauchen keine Beleuchtung - gut tippen muss man auf ihnen können. Glücklich wird man sowohl mit der Logitech K835 TKL als auch mit der Cherry Stream Desktop.
Ein Test von Tobias Költzsch

  1. SPC Gear Mechanische TKL-Tastatur mit RGB kostet 55 Euro
  2. Launch Neue Details zur Open-Source-Tastatur von System76
  3. Youtube Elektroschock-Tastatur bestraft schlampiges Tippen

Whatsapp, Signal, Telegram: Regierung fordert Nutzerverifizierung bei Messengern
Whatsapp, Signal, Telegram
Regierung fordert Nutzerverifizierung bei Messengern

Ebenfalls auf der Wunschliste des Innenministeriums: Provider sollen für Staatstrojaner Datenströme umleiten und Ermittlern Zugang zu Servern erlauben.
Ein Bericht von Friedhelm Greis

  1. Whatsapp, Signal, Telegram Datenschutzbeauftragter gegen Verifizierung bei Messengern
  2. Großbritannien Datenleck bei Kindergarten-Überwachungskameras
  3. Überwachungsgesamtrechnung "Weiter im Überwachungsnebel waten"

    •  /