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?

Stellenmarkt
  1. Teamleiter (m/w/d) Softwareentwicklung
    nox NachtExpress, Mannheim
  2. Bioinformatiker*in (m/w/d)
    Universitätsmedizin der Johannes Gutenberg-Universität Mainz, Mainz
Detailsuche

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.

Golem Akademie
  1. Scrum Product Owner: Vorbereitung auf den PSPO I (Scrum.org)
    24.-25. November 2021, online
  2. Einführung in die Programmierung mit Rust
    21.-25. März 2022, online
  3. Advanced Python - Fortgeschrittene Programmierthemen
    27.-28. Januar 2022, online
Weitere IT-Trainings

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:

  1. @Test
  2. public void testA() {
  3. System system = new System();
  4. Output output = system.apply(A_I);
  5. Truth.assertThat(output).isEqualTo(A_O);
  6. }
  7.  
  8. @Test
  9. public void testB() {
  10. System system = new System();
  11. Output output = system.apply(A_I);
  12. output = system.apply(B_I);
  13. Truth.assertThat(output).isEqualTo(B_O);
  14. }
  15.  
  16. @Test
  17. public void testC() {
  18. System system = new System();
  19. Output output = system.apply(A_I);
  20. output = system.apply(B_I);
  21. output = system.apply(C_I);
  22. Truth.assertThat(output).isEqualTo(C_O);
  23. }
  24.  
  25. @Test
  26. public void testD() {
  27. System system = new System();
  28. Output output = system.apply(A_I);
  29. output = system.apply(B_I);
  30. output = system.apply(C_I);
  31. output = system.apply(D_I);
  32. Truth.assertThat(output).isEqualTo(D_O);
  33. }
  34.  
  35. @Test
  36. public void testE() {
  37. System system = new System();
  38. Output output = system.apply(A_I);
  39. output = system.apply(B_I);
  40. output = system.apply(C_I);
  41. output = system.apply(D_I);
  42. output = system.apply(E_I);
  43. Truth.assertThat(output).isEqualTo(E_O);
  44. }

Oder Sie schreiben einen Test, der alles abdeckt:

  1. @Test
  2. public void testABCDE() {
  3. System system = new System();
  4.  
  5. Output output = system.apply(A_I);
  6. Truth.assertThat(output).isEqualTo(A_O);
  7.  
  8. output = system.apply(B_I);
  9. Truth.assertThat(output).isEqualTo(B_O);
  10.  
  11. output = system.apply(C_I);
  12. Truth.assertThat(output).isEqualTo(C_O);
  13.  
  14. output = system.apply(D_I);
  15. Truth.assertThat(output).isEqualTo(D_O);
  16.  
  17. output = system.apply(E_I);
  18. Truth.assertThat(output).isEqualTo(E_O);

Wenn Sie die von vielen Entwicklern gepredigte Regel "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.

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

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, um die Abdeckung ihrer Sync-Funktionalität zu verbessern

jqwik - Eigenschaftsbasierte Testbibliothek für Java (Dank an Dan Turner für die Empfehlung)

QuickTheories - Eine weitere eigenschaftsbasierte Testbibliothek für Java

Online-Diskussionsforen:

/r/programming - 2019/05

/r/programming - 2019/11

Bitte aktivieren Sie Javascript.
Oder nutzen Sie das Golem-pur-Angebot
und lesen Golem.de
  • ohne Werbung
  • mit ausgeschaltetem Javascript
  • mit RSS-Volltext-Feed
 Zufallstests: Was die Amateure von den Profis unterscheidet
  1.  
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5


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
Klimaforscher
Das Konzept der Klimaneutralität ist eine gefährliche Falle

Mit der Entnahme von CO2 in den nächsten Jahrzehnten netto auf null Emissionen zu kommen, klingt nach einer guten Idee. Ist es aber nicht, sagen Klimaforscher.
Von James Dyke, Robert Watson und Wolfgang Knorr

Klimaforscher: Das Konzept der Klimaneutralität ist eine gefährliche Falle
Artikel
  1. Elektroauto: Xiaomis Autoproduktion startet Anfang 2024
    Elektroauto
    Xiaomis Autoproduktion startet Anfang 2024

    2024 sollen erste Elektrofahrzeuge von Xiaomi auf den Markt kommen - also etwas später als zunächst gedacht.

  2. M1 Pro/Max: Dieses Apple Silicon ist gigantisch
    M1 Pro/Max
    Dieses Apple Silicon ist gigantisch

    Egal ob AMD-, Intel- oder Nvidia-Hardware: Mit dem M1 Pro und dem M1 Max schickt sich Apple an, die versammelte Konkurrenz zu düpieren.
    Eine Analyse von Marc Sauter

  3. Werbeversprechen kassiert: Teslas Solarschindeln sind doch nicht so stabil
    Werbeversprechen kassiert
    Teslas Solarschindeln sind doch nicht so stabil

    Tesla hat die Website zu seinen Solarschindeln aktualisiert. Die Behauptung, dass diese dreimal stärker als Standardschindeln sind, wurde fallengelassen.

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 • Cyber Week: Bis zu 16% auf SSDs & RAM von Adata & bis zu 30% auf Alternate • 3 Spiele für 49€: PC, PS5 uvm. • Switch OLED 369,99€ • 6 Blu-rays für 40€ • MSI 27" Curved WQHD 165Hz HDR 479€ • Chromebooks zu Bestpreisen • Alternate (u. a. Team Group PCIe-4.0-SSD 1TB 152,90€) [Werbung]
    •  /