Checkliste Unit Test

werdas34

Bekanntes Mitglied
Hallo,

gibt es bei Unit Tests eine Checkliste an Dingen, worüber man immer Tests schreiben sollte? Damit man die meisten Testfälle abdecken kann?
Sowas wie,
  • macht die Methode das was sie machen soll
  • Parameter auf null prüfen
  • Parameter auf Range prüfen + Grenzwerte inclusive/exclusive
  • werden Exception geworfen
  • ...

mfg werdas34
 
Ich würde sagen, es kommt vor allem auf den Prüfling an.

Grundsätzlich würde ich sagen, daß der Test das testen soll was die Dokumentation vorgibt. Wenn in der Doku nichts über Null-Rückgabe oder Exceptions steht, was will man dann da testen? Wenn in der Doku etwas darüber steht, sollte es natürlich getestet werden.

Das ist aber alles wieder viel zu einzelfallbetrachtungslastig, ein starres Regelwerk wird dir in etwa 95% der Fälle nutzen und in den restlichen 5% im Weg stehen bzw. untauglich sein, dann sollte man es lieber lassen.
 
Keine feste "Checkliste", aber man kann es so verallgemeinern:

Da man in der Regel nicht alle möglichen Eingabewerte einzeln testen kann (weil es einfach zu viele gibt), macht es Sinn, Deine Eingabe-Parameter in "Klassen" zu unterteilen. Bei einem int-Parameter könnten das postive Zahlen, negative Zahlen und die Null sein. Falls die zu testende Funktion nur Eingabe-Werte aus einem begrenzten Bereich erlaubt, kannst Du weiter unterteilen, in Eingabe-Werte die noch im zulässigen Bereich liegen und Werte jenseits davon. Es sollte dann mindestens aus jeder "Klasse" ein Wert getestet werden, um zu schauen, ob sich die Funktion jeweils wie erwartet verhält (also das korrekte Ergebnis liefert, oder die zu erwartende Exception wirft). Wenn Die zu testende Funktion mehrere Parameter hat, dann sollte mindestens für jede mögliche Kombination der Eingabe-"Klassen" ein Beispiel getestet werden.

Außerdem sollten Deine Tests natürlich möglichst den kompletten Code abdecken. Du kannst Dir, z.B. mit Eclipse + JUnit, die Testabdeckung (Code Coverage) anschauen, um Code-Abschnitte zu finden, die bisher von keinem Testfall abgedeckt sind. Entsprechend kannst Du dann weitere Testfälle ergänzen, um diese zu erwischen. Idealerweise sollte die Testabdeckung am Ende gegen 100% gehen...

Siehe auch:
 
Zuletzt bearbeitet:
Ein paar Ergänzungen zu der sehr guten Antwort von @LucasGlockner

Wichtig sind die Randfälle bzw. ungültigen Kombinationen. Die Standard-Ok Fälle hat man in der Regel schnell durch und meistens funktionieren die auch recht gut. Interessant sind aber die anderen. Da gewinnt man mit der Zeit eine Erfahrung was gute Äquivalenzklassen sind, was man also testen will. Ein paar Beispiele:

  • Null Werte will man immer testen
  • Bei Strings auch immer den Leerstring ("") testen
  • Bei Datumsfeldern den 1.1 / 31.12 und Schaltjahre (28/29.2 bzw. 1.3) testen

Man sollte versuchen beim Test-Schreiben ein "destruktives" Mindset zu bekommen. Das hilft ungemein Konstellationen zu finden, die man beim schreiben des Codes nicht bedacht hat.

Bezüglich der 100% Testabdeckung. Danach sollte man streben - erreichen wird man oft in der Praxis aber nicht bzw. nur mit unverhältnismäßig hohem Aufwand. Aber - da man in der Regel Unit Tests für eigenen Code schreibt - wenn man an bestimmte Code-Stellen mit Tests nicht oder nicht sinnvoll rankommt, ist das oft ein Zeichen, dass man den Code refaktoren sollte und nicht das man die Tests aufwendig bauen soll. Aber manchmal gibt es auch Stellen, die sind im Code Sicherheitsnetze, die man aber einfach nicht aufrufen kann, weil die Konstellation nicht erzeugbar ist.

Generell sollten aber Unit Tests möglichst einfach sein. Einfach im Sinne von kleines Setup, Aufruf der Methode, ein paar Asserts. Sobald man umfangreiches Setup braucht (im Worst Case noch mit haufenweise Mockito-Code), hat man ein Problem. In weit über 90% der Fälle, wo nach Code Änderungen Unit-Tests kaputt waren, mussten die Tests angepasst werden - nicht der Code. Sprich der Code war korrekt, aber die Tests nicht mehr. Und je aufgeblähter das Test-Setup ist, um so aufwendiger sind solche Anpassungen. Bis hin dazu, dass der Test-Code nicht verständlich war und man ihn weggeworfen hat und neu geschrieben hat. Der Reflex bei aufwendigen Test-Setup sollte immer sein "Warum brauche ich das, kann ich nicht meinen produktiven Code refaktoren, dass ich weniger Test-Setup brauche". Das ist auch einer der großen Vorteile von Test-Driven-Development. Dadurch das man da erst die Tests schreibt, wird sowas größtenteils vermieden.
 
Ich tendiere sehr zu dem, was @White_Fox geschrieben hat. Statt Dokumentation würde ich Anforderung oder Spezifikation sagen.

Bei TDD sieht man das recht gut. Man hat Anforderungen und die setzt man nach und nach um. Und dafür gibt es dann Unit Tests. Die sind dann übrigens eine gute Dokumentation der Spezifikation.

Wenn da nichts von Null Werten spezifiziert wurde, dann ist da auch kein Unit Test. Dann gibt es da auch keinen Test zu. Das ist dann ggf. ein Fehler in der Spezifikation. Da muss man also drauf achten, aber das ist etwas, das streng genommen eben an eine andere Stelle gehört.

Der Ansatz, Code zu schreiben und dann im Nachhinein Unit Tests zu schreiben, ist generell schlecht, da oft schwer zu testender Code geschrieben wurde. TDD ist aus meiner Sicht daher wichtig - man kann gerne ohne TDD vorgehen, aber als Übung sollte man es nutzen, weil man so viel mehr Gefühl für testbaren Code bekommt und dann halt keinen Code schreibt, der nur schwer testbar ist.

Das ist aber einfach nur meine bescheidene Sicht.
 
Wenn da nichts von Null Werten spezifiziert wurde, dann ist da auch kein Unit Test.
Da bin ich anderer Ansicht

Das ist dann ggf. ein Fehler in der Spezifikation. Da muss man also drauf achten, aber das ist etwas, das streng genommen eben an eine andere Stelle gehört.
Dem stimme ich zu.

Aber ich glaube so weit liegen wir nicht auseinander 🙂

Ich gehe beim Unit-Test schreiben davon aus, dass ich in den meisten Fällen Unit Tests für meinen eigenen Code schreibe. Und wenn ich keine Spezifikation für Null-Werte an meiner Funktion habe, kann ich keinen Unit Test schreiben - wie auch. Aber dann sehe ich mich als Entwickler in der Verantwortung eine Spezifikation zu schreiben und dann muss ich auch einen Unit-Test schreiben. Der kann (und sollte) gerne simpel sein. Einfach eine NullPointerException ist ja vollkommen ok. Aber undefiniertes Verhalten ist immer scheiße und sollte abgestellt werden.

Schreibe ich einen Unit Test für eine fremde Funktion, wirds schwieriger. Auf der einen Seite, wenn die Funktion nicht vorgibt, was sie bei null tut kann ich eigentlich keinen schreiben. Auf der anderen Seite möchte ich gerne für mich das Verhalten der Funktion definieren und insbesondere mitbekommen, ob sich am Verhalten was ändert z.B. bei einer neuen Version (Was haben wir beim Upgrade auf Java 17 / JEE10 / Spring Boot 3.x gekotzt, weil sich manches Verhalten von Libs geändert hat...). Und spätestens da möchte ich auch unspezifiziertes Verhalten dokumentieren.


Daher bin ich der Ansicht - man sollte auch für nicht spezifizierte Dinge Unit-Tests schreiben, aber im ersten Schritt dafür sorgen, dass es keine unspezifierten Dinge mehr gibt.
 
  • macht die Methode das was sie machen soll
  • Parameter auf null prüfen
  • Parameter auf Range prüfen + Grenzwerte inclusive/exclusive
  • werden Exception geworfen
Die Reihenfolge ist umgekehrt:

  • null/leer prüfen
  • mögliche Exceptions prüfen
  • Parameter auf Range prüfen, Grenzwerte und Äquivalenzklassen
  • Andere Nebeneffekte prüfen
  • Verhalten der Methode prüfen

Und immer zuerst Konstruktor prüfen
 
Aber ich glaube so weit liegen wir nicht auseinander 🙂
Ja, das siehst Du richtig. 🙂

In der Praxis ist es doch auch relativ einfach:
  • Entweder es wird eine non null Referenz benötigt. Dann ist das direkt ein Bestandteil der Spezifikation, die aus meiner Sicht auch direkt als Annotation einfließen sollte. (Die Annotations NotNull / NonNull / Nullable sind aus meiner Sicht etwas, das benutzt werden sollte. Das unterstützt die statische Codeanalyse und verhindert so, dass NPEs im Code auftreten).
  • Wenn null erlaubt wird, dann ist dies doch in der Regel klar in der Spezifikation benannt.

Es kann etwas problematisch werden, wenn Werte nur durchgereicht werden, da dies da dann nicht deutlich wird und in der Spezifikation untergehen könnte. Das ist aber dann auch ein Fall, der keinen expliziten Unit Test mit null benötigt, denn der Parameter wird ja nur durchgereicht und wenn ich nur die Methode selbst testen will, dann würde das doch gemockt und für meinen Unit Tests wird nur generell sicher gestellt:
  • Der Parameter wird nicht weiter verwendet
  • Die andere Methode ist mit dem Parameter aufgerufen worden.
==> In so einem Fall ist also dann jeder konkrete Wert für den Parameter uninteressant.

Ich kann mich jetzt ehrlich gesagt nicht daran erinnern, dass ich sowas schon einmal gehabt hätte, aber das ist gerade so eine Konstellation, die mir durch den Kopf geht. Wenn ich für sowas ein Beispiel erfinden müsste, dann wäre es evtl. ein Service, der irgendwas macht. Ein createEmployee bekommt diverse Dinge übergeben wie Name und so. Es wird aber auch eine Abteilung angegeben, die eine eigene Klasse ist. Die kann dann null sein - manche Angestellte haben einfach keine Abteilung oder die Software verwaltet auch kleine Firmen und bei 3 Angestellten hat man vermutlich keine Abteilungen. Der Service hat dann die Spezifikation, dass er da irgendwas an Business Logik prüfen soll um dann im Repository entsprechend ein Employee anzulegen. Und da kann dann die Abteilung auch null sein.
-> Im Test des Repositories will ich sicher gehen, dass es Employees mit und ohne Abteilung anlegen kann.
-> Im Service ist es egal - da gibt es keine Spezifikation bezüglich der Abteilung außer eben, dass die Abteilung (egal mit welchem Wert) an das Repository übergeben wird.

War der Gedanke nachvollziehbar? Oder war das jetzt alles zu wirr?

Aber ja: Wir sind auf jeden Fall auf der gleichen Seite, wenn wir diese Frage nach null Werten sehr genau betrachten. Und in der Implementation wäre dies dann auch per @Nullable vorhanden und sichtbar. Nur eben gibt es halt keinen Unit Test, der diese Methode dann auch mal mit null aufruft 🙂
 
Ich bin ja bekanntermaßen kein Softwerker und habe von Programmierung sowieso keine Ahnung, aber....

Wenn eine Funktion ein Objekt als Parameter erwartet, würde ich es generell als Fehler ansehen der Funktion Null zu übergeben. (Außer die Doku/Spezifikation sagen etwas anderes.)
Normalerweise schreibe ich in den JavaDocs dann auch sowas wie "Methode m wird ein Objekt o übergeben und macht damit x. ...". Wenn ich der Methode jetzt Null übergebe, dann läuft daß der Spezifikation zuwider, denn auf Null kann die Methode naturgemäß nichts machen. Im Unittest der Methode Null zu übergeben erscheint mir da dann unsinnig.

Nehmen wir mal an, die Methode kann mit Null umgehen und tut bei Null einfach nur nichts, aber in der Doku/Spezifikation steht dazu nur nichts. Dann wäre die Prüfung im Sinne von "If you fail, fail fast" eher problematisch. Man kann natürlich auch hergehen und sagen, der Unittest soll gerade zeigen daß die NPE auch wirklich kommt wenn man versehentlich Null übergeben würde...aber da hört es irgendwann bei mir mal auf, aber ich bin ja wie gesagt auch kein Softwerker. 😛

Mit Annotationen bezüglich Null habe ich aber noch nie irgendwas gemacht, was würde eine Annotation denn da machen können wenn Null übergeben wird wo es nicht sein soll?
 
Mit Annotationen bezüglich Null habe ich aber noch nie irgendwas gemacht, was würde eine Annotation denn da machen können wenn Null übergeben wird wo es nicht sein soll?
Die Annotations machen in erster Linie nichts (Mit Ausnahme von Lomboks @NotNull - das baut noch Code ein, das direkt eine Exception wirft). Der Sinn ist aber, dass Du bei
  • Parametern
  • Rückgabetypen
  • Variablen
angeben kannst, ob diese auch null Werte erlauben.

Das kann dann durch Tools ausgewertet werden. SpotBugs ist z.B. ein Tool zur statischen Codeanalyse, das dies prüfen kann.

Wenn also eine Methode aufgerufen wird und da darf kein null übergeben werden, dann kann es direkt prüfen: Woher kommt der Wert, der übergeben wird? Wenn der Wert null sein kann und kein Check auf null vorab gemacht wurde, dann gibt es in SpotBugs einen Fehler.

Wenn Du damit etwas spielen willst: Unter kneitzel/JavaMavenApp (github.com) findest Du ein Projekt, das sowohl PMD als auch SpotBugs enthält. Und in dem Code ist ein minimales Beispiel, wie es mit den Annotations laufen könnte.

In Greeting kannst Du ja mal den null Check heraus nehmen:
Java:
    public Greeting(@Nullable final String name) {
        this.name = name; // was: this.name == null ? DEFAULT_NAME : name;
    }

Das Übersetzt auch, aber dann wird SpotBugs Dich auf den Fehler hinweisen. (Und mit dem Fehler gibt es keine NPE sondern "Hallo null!" wäre dann die Ausgabe.

Wenn man dies relativ konsequent nutzt, dann hat man schon einen sehr guten Schutz vor NPEs. Je mehr Code das nicht konsequent nutzt, desto schlechter ist der Schutz natürlich.

Und aus meiner Sicht ist das auch top, wenn es dann um die Lesbarkeit des Codes geht. Da sieht man dann direkt: Wird null erlaubt? Kann ein null zurück kommen? (An der Stelle aber natürlich auch der Hinweis auf Optional. Bei Rückgabewerten macht es Sinn, statt null den Typ von T auf Optional<T> zu ändern um dann ein Optional.empty zurück zu geben wenn es eben keine Instanz gibt.)
 
In weit über 90% der Fälle, wo nach Code Änderungen Unit-Tests kaputt waren, mussten die Tests angepasst werden - nicht der Code. Sprich der Code war korrekt, aber die Tests nicht mehr.
Hi, darf ich da mal kurz noch etwas nachfragen? Ich habe vor kurzem eine Aufgabe bearbeitet bei der es ums Testen ging. Das Prinzip, nach dem verfahren werden sollte, war das TDD. Was hältst du davon? Lt. meinen Literaturrecherchen soll es sinnvoll sein, erst die Tests und dann den Code zu schreiben (kann mir allerdings iwie nicht vorstellen, dass das in der Praxis gemacht wird..).
 
Erst den minimal nötigen Code um die Tests zu bestehen ("grün") und dann den Produktivcode schreiben.
Den Satz verstehe ich jetzt nicht. Aber soll erst einmal egal sein.

Bezüglich TDD ist aus meiner Sicht das beste Buch immer noch "Test Driven Development by example" von Kent Beck.

Und TDD in ganz kurz: red, green & refactor

red: Man schriebt einen neuen Unit Test und dieser ist rot.
green: Man schreibt mit minimalem Code etwas, das dann grün ist. (Das ist dann produktiver Code!)
refactor: Man schaut, ob es notwendig ist, den Code umzustellen.

Wenn klar ist, was zu tun ist und keine besondere Komplexität vorliegt, dann mache ich manchmal mehrere Zyklen zusammen. Dann wird nicht ein Test geschrieben sondern gleich mehrere.

TDD ist aus meiner Sicht zumindest eine Pflichtübung. Wenn man das kann und da gewisse Praxis gesammelt hat, dann schreibt man testbaren Code. Und ob ich dann erst Code schreibe und dann die Tests oder erst die Tests und dann den Code ist dann fast egal. So die Tests geschrieben werden!

Das Umdrehen der Reihenfolge führt zu mehreren Dingen:
a) Wenn die Zeit eng wird, dann lässt man da evtl. Tests weg.
b) man schreibt Code ohne Spezifikation (und damit auch ungetestet)

Und man darf nie vergessen: Die Tests dienen nicht nur dem reinen Test des produktiven Codes. Er ist auch eine Form der Dokumentation. Die Tests sind also durchaus gut, um die Spezifikation nachzuschlagen.
 
Was hältst du davon? Lt. meinen Literaturrecherchen soll es sinnvoll sein, erst die Tests und dann den Code zu schreiben (kann mir allerdings iwie nicht vorstellen, dass das in der Praxis gemacht wird..).
Es wird auch genug Firmen geben, die erst danach oder gar nicht testen. Es gibt erstaunlich viele Firmen mit einer erschreckend ineffizienten, kurzsichtigen Arbeitsweise.

Aber auch als Nichtprogrammierer kann ich dir versichern, daß diese Vorgehensweise tatsächlich sinnvoll ist. Einfach mal selber in einem kleinen Projekt ausprobieren, vorher habe ich mich auch darüber gewundert.
 
Ich tendiere sehr zu dem, was @White_Fox geschrieben hat. Statt Dokumentation würde ich Anforderung oder Spezifikation sagen.

Bei TDD sieht man das recht gut. Man hat Anforderungen und die setzt man nach und nach um. Und dafür gibt es dann Unit Tests. Die sind dann übrigens eine gute Dokumentation der Spezifikation.

Wenn da nichts von Null Werten spezifiziert wurde, dann ist da auch kein Unit Test. Dann gibt es da auch keinen Test zu. Das ist dann ggf. ein Fehler in der Spezifikation. Da muss man also drauf achten, aber das ist etwas, das streng genommen eben an eine andere Stelle gehört.

Der Ansatz, Code zu schreiben und dann im Nachhinein Unit Tests zu schreiben, ist generell schlecht, da oft schwer zu testender Code geschrieben wurde. TDD ist aus meiner Sicht daher wichtig - man kann gerne ohne TDD vorgehen, aber als Übung sollte man es nutzen, weil man so viel mehr Gefühl für testbaren Code bekommt und dann halt keinen Code schreibt, der nur schwer testbar ist.
Alternativ können Sie auf der Techzapk-Website einige Lern-Apps konsultieren, die Ihnen beim schnellen Lernen helfen. Die Website ist völlig kostenlos, Sie können also beruhigt sein
Das ist aber einfach nur meine bescheidene Sicht.
Vielen Dank für Ihre klare und präzise Sichtweise auf das Thema. Ich stimme Ihnen zu, dass klare Spezifikationen und TDD wichtige Elemente sind, um besseren und leichter testbaren Code zu schreiben
 

Zurück
Oben