Wie ermittelt man alle testbaren (zu testenden) Klassen in seinem Maven-Projekt?

Zrebna

Bekanntes Mitglied
Hallo!

Die im Fokus stehende Anwendung besteht aus 18 Maven-Projekten, wobei ein Maven-Projekt davon das Test-Projekt ist, welches vorwiegend Integrationstests enthält.
Die Unit-Tests sollten ("sollten" weil bis dato kaum welche vorhanden) ganz normal in den anderen 17 (Nicht Test-)-Projekten sein, und zwar im src/test/java-Pfad.
Es geht mir nun darum, bezogen auf eine vereinfachte und limitierte Zielsetzung, erstmal nur die Anzahl aller Klassen in der Anwendung zu ermitteln, für die man eine Unit-Test-Klasse im korrespondierendem src/test/java-Pfad erstellen könnte.

Meiner Recherche nach werden Interfaces üblicherweise nicht mit Unit-Test-Klassen direkt getestet (also eine Unit-Klasse direkt für ein Interface), abstrakte Klassen
aber schon - ist das so richtig?

Falls Ja, dann bräuchte ich also
1. alle Klassen (abstakte und konkrete, aber KEINE Interfaces)
2. dürfen sich nur im src/main/java-Pfad befinden
3. dürfen sich nicht in dem einen Test-Projekt befinden.

Fragen;
1. Sind meine obigen Filterüberlegungen bzgl. was ich brauche korrekt?
2. Ich bin nun nach Recherche über so viele Möglichkeiten gestolpert, Anzahl von Klassen zu ermitteln, dass ich mir nicht sicher bin, welche Möglichkeit zuverlässig ist und in meinem Fall genau das liefert, was ich haben möchte. Daher frage ich euch, ob ihr Möglichkeiten für meinen use case kennt und empfehlen könnt.


Lg
Zrebna
 
Mal so gefragt:Was willst du mit diesen Informationen machen? Du hast bestimmt in deiner IDE sowas wie "Test Coverage". Also darüber gibt es meist Ansichten oder direkt eine Code Lens oder ähnliches. Ich habe aber noch nicht versucht das zu exportieren. Man sieht ja den aktuellen Stand auch immer auf dem SonarServer. Besser kann man es doch fast nicht aufbereiten.
 
Meiner Recherche nach werden Interfaces üblicherweise nicht mit Unit-Test-Klassen direkt getestet (also eine Unit-Klasse direkt für ein Interface), abstrakte Klassen
Mal ein paar Überlegungen von meiner Seite.

Die Sache hat eigentlich weniger was mit Interface und abstrakter Klasse zu tun. Voraussetzung ist schlicht, dass eine Methodendefinition existieren muss, denn was willst Du testen, wenn keine Implementierung vorhanden ist?

Jetzt kann man sich überlegen, dass der Test auf die Methode Zugriff haben muss: private Methoden scheiden damit von vornherein aus (die werden innerhalb der Klasse benutzt und sind damit Teil eines anderen Tests).

Welche Methoden kommen für einen Test überhaupt in Frage? Es muss eine Implementierung vorhanden sein, d. h. statische Methoden, default-Implementierungen in Interfaces und natürlich Instanzmethoden (definierte, nicht nur abstrakt deklarierte, versteht sich), die mindestens nicht private sind.

Damit haben wir den Kreis schon einmal eingegrenzt. Wir nähern uns weiter über die Frage, ob und wie ich sinnvoll testen kann. Bei Methoden, die sich nicht überschreiben lassen (static oder final), bietet es sich natürlich an, den Test einmalig für die Basisklasse zu schreiben. Es wird keine zweite Implementierung geben.

Doch was ist mit Methoden, die in abgleiteten Klassen (bzw. Implementierungen des Interface) überschrieben werden können? Darf sich der Test der abgeleiteten Klasse darauf verlassen, dass die Klasse die Methode der Basisklasse nicht überschreibt? Wenn man diese Frage mit "nein" beantwortet, muss der Test sowieso in den abgeleiteten Klassen ausgeführt werden. Wozu soll man dann noch in der Basisklasse testen? Umgekehrt gilt dies, wenn man die Frage mit "ja" beantwortet.

Das Dilemma, in dem man sich nun findet lässt sich relativ leicht lösen, z. B. könnte man sagen, dass man möglichst weit oben in der Hierarchie testen möchte, um Tests nicht unnötig wiederholt implementieren zu müssen. Sofern die Methoden nicht überschrieben werden, ist das ja völlig ausreichend. Gleichzeitig benötigt man ein Werkzeug, um festzustellen, ob eine Methode überschrieben aber nicht getestet wurde. Hier hilft dann eben die statische Code-Analyse. So gibt die Coverage Auskunft, ob eine abgeleitete Methode getestet wurde.
 
Mal so gefragt:Was willst du mit diesen Informationen machen? Du hast bestimmt in deiner IDE sowas wie "Test Coverage". Also darüber gibt es meist Ansichten oder direkt eine Code Lens oder ähnliches. Ich habe aber noch nicht versucht das zu exportieren. Man sieht ja den aktuellen Stand auch immer auf dem SonarServer. Besser kann man es doch fast nicht aufbereiten.
Also SonarServer haben wir noch nicht.

Coverage Tool wie z.B. ECLEmma zeigt mir das nicht an - zumindest kann ich da nicht so filtern, wie ich es brauche - siehe Eingangspost. Code lens gibt es nur für VS? Ich benutze als IDE Eclipse.
 
Mal ein paar Überlegungen von meiner Seite.

Die Sache hat eigentlich weniger was mit Interface und abstrakter Klasse zu tun. Voraussetzung ist schlicht, dass eine Methodendefinition existieren muss, denn was willst Du testen, wenn keine Implementierung vorhanden ist?

Macht Sinn, stimmt - so werden also auch nur bei abstrakten Klassen die implementierten Methoden getestet und daher eben keine IFs, weil es dort definitiv keine Implementierungen gibt (außer ggf. Default-Implementierungen, die du unten ansprichst).
Jetzt kann man sich überlegen, dass der Test auf die Methode Zugriff haben muss: private Methoden scheiden damit von vornherein aus (die werden innerhalb der Klasse benutzt und sind damit Teil eines anderen Tests).
- Interessant, das wusste ich ehrlich nicht. Also ich habe instinktiv angenommen, dass das JUnit-FW es ermöglicht auch private Methoden zu testen, wenn sie korrekt platziert sind, also:
src/main/java/packageA/ClassA -> möglich private Methoden zu testen, wenn, die Test-Klasse liegt in:
src/test/java/packageA/ClassATest

Ist aber nicht so - wieder was gelernt xD

Welche Methoden kommen für einen Test überhaupt in Frage? Es muss eine Implementierung vorhanden sein, d. h. statische Methoden, default-Implementierungen in Interfaces und natürlich Instanzmethoden (definierte, nicht nur abstrakt deklarierte, versteht sich), die mindestens nicht private sind.

Damit haben wir den Kreis schon einmal eingegrenzt. Wir nähern uns weiter über die Frage, ob und wie ich sinnvoll testen kann. Bei Methoden, die sich nicht überschreiben lassen (static oder final), bietet es sich natürlich an, den Test einmalig für die Basisklasse zu schreiben. Es wird keine zweite Implementierung geben.

Doch was ist mit Methoden, die in abgleiteten Klassen (bzw. Implementierungen des Interface) überschrieben werden können? Darf sich der Test der abgeleiteten Klasse darauf verlassen, dass die Klasse die Methode der Basisklasse nicht überschreibt? Wenn man diese Frage mit "nein" beantwortet, muss der Test sowieso in den abgeleiteten Klassen ausgeführt werden. Wozu soll man dann noch in der Basisklasse testen? Umgekehrt gilt dies, wenn man die Frage mit "ja" beantwortet.

Das Dilemma, in dem man sich nun findet lässt sich relativ leicht lösen, z. B. könnte man sagen, dass man möglichst weit oben in der Hierarchie testen möchte, um Tests nicht unnötig wiederholt implementieren zu müssen. Sofern die Methoden nicht überschrieben werden, ist das ja völlig ausreichend. Gleichzeitig benötigt man ein Werkzeug, um festzustellen, ob eine Methode überschrieben aber nicht getestet wurde. Hier hilft dann eben die statische Code-Analyse. So gibt die Coverage Auskunft, ob eine abgeleitete Methode getestet wurde.
Problem bzgl. weit oben in der Hierarchie zu testen ist, dass in dem Unternehmen, bei dem ich eine externe BA schreibe, schon ein großer Teil der Methoden der Basisklasse von den erbenden Klassen überschrieben wird, weil ein großer Teil abstrakte Methoden sind.

Also EclEmma scheint da limitiert zu sein, An statischen lokalen Codeanalyse-Tools kenne ich bis dato nur SpotBugs und SonarLint - die können das glaube ich nicht. SonarQubeServer hat das Unternehmen nicht und kommt auch erstmal nicht - also keine Chance hier.

Trotzdem sind deine Gedanken gold wert, weil ich ja auch zeigen will, warum meine Heuristiken sehr grob, nicht korrekt im wirklichem Sinne und begrenzt sind. Diese Heuristiken sollen einfach nur abschätzen, wie lange man grob brauchen würde, um eine nicht existierende Unit-Abdeckung (es werden aktuell in diesem kleinen Unternehmen keine Unit-Tests geschrieben) "aufzuholen".
Da fand ich das Vorgehen über Klassen zu gehen pragmatischer als grobe Schätzung, weil man dann bzgl. Ticketkoordination eben auch einfach "Unit-Test-Tickets" pro zu testender Klasse erstellen und verteilen/zuweisen könnte. Dass das maximal ungenau ist, weil nicht jede Klasse gleich komplex ist und auch nicht jede Klasse dieselbe Anzahl an testbaren Methoden hat (siehe deinen Input) ist klar. Und genau diese Erkenntnisse sind wertvoll und werden von mir auch dargelegt, um zu verdeutlichen, dass mein Vorgehen bzgl. Aufwandschätzung ungenau und auch nicht korrekt ist, aber nach viel Austausch irgendwie am praktikabelsten.

Denn was soll man sonst machen? Einzelne Leuten müssten recht genau analysieren und bewerten, welche Methoden man testen will und dann darauf basierend ein Ticket erstellen, was dann z.B. ansagt "teste Methode x und Klasse y und dann noch Methode u in Klasse v".
Das erscheint mir nach Rücksprache ein größerer Ticketerstellungsaufwand.
Vielleicht ist der Ticketerstellungsaufwand gar nicht so groß, wenn man die richtigen Werkzeuge und Know-how hätte? Diese ist aber aktuell in dem kleinen Unternehmen (gerade mal ca. 25-30 Festangestellte und eine "Handvoll" Entwickler) nicht gegeben.
 
Du willst paar Informationen haben - wozu genau? Wenn nur die Informationen alleine reichen, dann schau Dir mal die Reporting Möglichkeiten von Maven zusammen mit diversen Plugins.

JDepend Maven Plugin - Introduction (mojohaus.org)
JDepend geht dabei mehr auf Abhängigkeiten ein aber in dem Zusammenhang wird auch die Anzahl der Klassen angegeben.

JavaNCSS Maven Plugin – Introduction (mojohaus.org)
Hier wird NCSS benutzt um diverse Informationen zu ermitteln

Evtl. gibt es noch mehr - das wären erst einmal nur die, die ich von codehaus/mojo kenne. Java NCSS könnte aber zumindest einiges holen, das interessant ist.

Evtl. ist auch dashboard von codehaus/mojo interessant, das aus diversen Quellen ein Dashboard zusammen stellt.
 
Also ich habe instinktiv angenommen, dass das JUnit-FW es ermöglicht auch private Methoden zu testen, wenn sie korrekt platziert sind, also:
Wenn beide Klassen im gleichen Package liegen, dann kannst Du "package private" Methoden testen. Zugriff auf private-Methoden ist via Reflection möglich, macht aber aus genannten Gründen in der Regel wenig Sinn. JUnit hat ReflectionUtils, die den Zugriff auf private Felder/Methoden erleichtern.

schon ein großer Teil der Methoden der Basisklasse von den erbenden Klassen überschrieben wird, weil ein großer Teil abstrakte Methoden sind.
Das ist ja auch völlig okay: abstrakte Methoden zählen mangels Implementierung nicht. Du kannst eine Methode erst testen, wenn sie nicht mehr abstrakt ist.

Einzelne Leuten müssten recht genau analysieren und bewerten, welche Methoden man testen will
Naja, wir reden von Unittests: man testet in der Regel keine Getter und Setter, man testet nicht das Framework, man testet meist keine Boundaries (wie z. B. UI). Es geht darum, Verhalten zu testen.

Dass das maximal ungenau ist, weil nicht jede Klasse gleich komplex ist und auch nicht jede Klasse dieselbe Anzahl an testbaren Methoden hat (siehe deinen Input) ist klar.
Hier könnte die zyklomatische Komplexität als Metrik etwas weiterhelfen, um den Testaufwand besser einschätzen zu können. Aber das dient auch nur der Verbesserung einer Abschätzung. Code, für den die Tests hinterher geschrieben wurden, ist z. B. oft verhältnismäßig schwer zu testen. Hier können dann Tools wie Mockito helfen.
 
Wenn beide Klassen im gleichen Package liegen, dann kannst Du "package private" Methoden testen. Zugriff auf private-Methoden ist via Reflection möglich, macht aber aus genannten Gründen in der Regel wenig Sinn. JUnit hat ReflectionUtils, die den Zugriff auf private Felder/Methoden erleichtern.


Das ist ja auch völlig okay: abstrakte Methoden zählen mangels Implementierung nicht. Du kannst eine Methode erst testen, wenn sie nicht mehr abstrakt ist.


Naja, wir reden von Unittests: man testet in der Regel keine Getter und Setter, man testet nicht das Framework, man testet meist keine Boundaries (wie z. B. UI). Es geht darum, Verhalten zu testen.


Hier könnte die zyklomatische Komplexität als Metrik etwas weiterhelfen, um den Testaufwand besser einschätzen zu können. Aber das dient auch nur der Verbesserung einer Abschätzung.

Das finde ich interessant, nur kann ich mir drunter nichts vorstellen. Ich habe mir gerade eine Abbildung von SonarQube angesehen, wie sowas aussehen kann. Das ist ein Histogramm: Die Y-Ache entrpcicht der Anzahl der Methoden. Die x-Achse entspricht der zyklomatischen Kmplexität, die wohl u.a. besagt, wie schwierig es ist eine Methode zu testen. Diese zyklomatische Komplexität ist nun mit Werten versehen:
1 bis 10: wenig komplexe Software mit geringem Fehlerrisiko
11 bis 20: mittelmäßig komplexe Software mit bescheidenem Fehlerrisiko
21 bis 50: hohe Komplexität bei hoher (und steigender) Wahrscheinlichkeit eines Fehlers
51 und mehr: zu hohe Komplexität in einem untestbaren System mot sehr hohem Fehlerrisiko

Gibt es hier irgendwie ein Mapping? Also selbst, wenn ich das gesamte Projekt auf diese Metrik prüfen würde und dann auch so ein schönes Histogramm hätte, dann hätte ich null Ahnung, wie ich nun daraus einen konkreten Arbeitsaufwand in Zahlen (Stunden die Unit-Tests zu schreiben) ableiten könnte. Wie würde das funktionieren?


Code, für den die Tests hinterher geschrieben wurden, ist z. B. oft verhältnismäßig schwer zu testen. Hier können dann Tools wie Mockito helfen.

Das Mockito helfen kann, damit meinst du, dass "schlecht" testbare Klassen oft irgendwelche externen Abhängigkeiten brauchen, die man eben bei einem Unit-Test nicht liefert und diese dann einfach mocken kann, oder?

Hm, es scheint mir, dass es hier nicht den 100% exakten Weg gibt. Daher konkrete Frage:
Wenn du derjenige wärest, der entscheiden müsste, wie man es am besten vom Arbeitsprozess so organisiert, dass eben versäumte nicht-getestete Klassen (die testbar wären, weil implementierte Methoden, die sich für Unit-Tests eignen - also keine getters und setters) nun allmählich getestet würden, wie würdest du es innerbetrieblich organisieren?

Mir scheint es immer noch am einfachsten einfach für eine testbare Klasse jeweils ein "TODO-Test-Ticket" zu erstellen und zuzuweisen.
 
Mir scheint es immer noch am einfachsten einfach für eine testbare Klasse jeweils ein "TODO-Test-Ticket" zu erstellen und zuzuweisen.
Ich würde mir die Frage stellen, wie kann ich schnell, mit angemessenen Aufwand Mehrwert erzeugen. Der Mehrwert von Tests liegt dabei (für mich) darin, dass Fehler früher auffallen und man ein Sicherheitsnetz hat für zukünftige Änderungen am Code. Ein netter Nebeneffekt von Tests ist auch, dass man gezwungen ist, seinen Code überhaupt testbar zu bauen, was in der Regel zu einfacher lesbaren und wartbaren Code führt.

Um das zu erreichen, würde ich erstmal gar nicht so sehr den Fokus auf Unit-Tests legen. Ich würde mir zu erst einmal anschauen, was so die Usecases sind und mich dann damit beschäftigen, wie ich diese automatisiert testen kann. Dabei kommt es nun stark darauf an, was für eine Art von Applikation es ist. Wäre es zum Beispiel eine Spring-Applikation, die REST-Schnittstellen nach Außen anbietet, würde ich im ersten Schritt anfangen, Blackboxtests zu bauen, die diese Schnittstellen für die Haupt-Usecases aufruft und schaut ob das Ergebnis stimmt. Dafür würde ich Tools wie Testcontainers nutzen, um eine reproduzierbare Umgebung aufzubauen oder Wiremock um andere Systeme zu simulieren. Hierbei geht es nicht darum, alle möglichen Edgecases abzudecken, sondern nur darum erstmal eine Basis zu haben.

Als nächstes würde ich mir dann anschauen, welche Testabdeckung ich damit schon erreicht habe, dabei ist das Ziel nicht, mit dieser Art von Tests eine hohe Testabdeckung zu haben, sondern zu schauen, welche Teile der Applikation noch gar nicht abgedeckt sind. Dabei können 2 verschiedene Szenarien vorliegen.

a) Der Code wurde nicht durchlaufen, weil man dazu eine komplexe Umgebung mit den richtigen Daten und richtigen Antworten von anderen Systemen simulieren muss. Hier ist der Ansatz dann, dass man Componenten-Tests baut, und deren Abhängigkeiten mit Mockito weg mockt. An der Stelle wird es auch passieren, dass man den Code umbauen muss um überhaupt richtig testen zu können, aber zum Glück hat man ja schon eine gewisse Art von Sicherheitsnetz, dass zumindest grob sicherstellt, dass man es merkt wenn was kaputt geht.

b) Code wurde nicht durchlaufen, weil es sich dabei um toten Code handelt. Es kommt immer wieder mal vor, dass durch Änderungen, Code gar nicht mehr gebraucht wird, aber trotzdem in der Codebase bleibt. Eventuell mit einem if drum, und wenn man die Bedingung weiter verfolgt, nie true ergibt. Dieser Code kann dann einfach weg.

So würde ich mich dann von Ebene zu Ebene vorarbeiten.

Betrachte die Testabdeckung als Werkzeug, das dir hilft eine wartbare Applikation zu bauen und nicht als Metrik, die erreicht werden muss.
 
Ich würde mir die Frage stellen, wie kann ich schnell, mit angemessenen Aufwand Mehrwert erzeugen. Der Mehrwert von Tests liegt dabei (für mich) darin, dass Fehler früher auffallen und man ein Sicherheitsnetz hat für zukünftige Änderungen am Code. Ein netter Nebeneffekt von Tests ist auch, dass man gezwungen ist, seinen Code überhaupt testbar zu bauen, was in der Regel zu einfacher lesbaren und wartbaren Code führt.

Um das zu erreichen, würde ich erstmal gar nicht so sehr den Fokus auf Unit-Tests legen. Ich würde mir zu erst einmal anschauen, was so die Usecases sind und mich dann damit beschäftigen, wie ich diese automatisiert testen kann. Dabei kommt es nun stark darauf an, was für eine Art von Applikation es ist. Wäre es zum Beispiel eine Spring-Applikation, die REST-Schnittstellen nach Außen anbietet, würde ich im ersten Schritt anfangen, Blackboxtests zu bauen, die diese Schnittstellen für die Haupt-Usecases aufruft und schaut ob das Ergebnis stimmt. Dafür würde ich Tools wie Testcontainers nutzen, um eine reproduzierbare Umgebung aufzubauen oder Wiremock um andere Systeme zu simulieren. Hierbei geht es nicht darum, alle möglichen Edgecases abzudecken, sondern nur darum erstmal eine Basis zu haben.

Als nächstes würde ich mir dann anschauen, welche Testabdeckung ich damit schon erreicht habe, dabei ist das Ziel nicht, mit dieser Art von Tests eine hohe Testabdeckung zu haben, sondern zu schauen, welche Teile der Applikation noch gar nicht abgedeckt sind. Dabei können 2 verschiedene Szenarien vorliegen.

a) Der Code wurde nicht durchlaufen, weil man dazu eine komplexe Umgebung mit den richtigen Daten und richtigen Antworten von anderen Systemen simulieren muss. Hier ist der Ansatz dann, dass man Componenten-Tests baut, und deren Abhängigkeiten mit Mockito weg mockt. An der Stelle wird es auch passieren, dass man den Code umbauen muss um überhaupt richtig testen zu können, aber zum Glück hat man ja schon eine gewisse Art von Sicherheitsnetz, dass zumindest grob sicherstellt, dass man es merkt wenn was kaputt geht.

b) Code wurde nicht durchlaufen, weil es sich dabei um toten Code handelt. Es kommt immer wieder mal vor, dass durch Änderungen, Code gar nicht mehr gebraucht wird, aber trotzdem in der Codebase bleibt. Eventuell mit einem if drum, und wenn man die Bedingung weiter verfolgt, nie true ergibt. Dieser Code kann dann einfach weg.

So würde ich mich dann von Ebene zu Ebene vorarbeiten.

Betrachte die Testabdeckung als Werkzeug, das dir hilft eine wartbare Applikation zu bauen und nicht als Metrik, die erreicht werden muss.
Grundsätzlich stimme ich zu. Ich würde nur eine Ergänzung zu den Unit-Tests machen.
Das beschriebene Vorgehen halte ich für mit am besten, wenn es darum geht eine bisher nicht/unzureichend getestete Anwendung zu testen. Da ist der Fokus auf "Teste die Use-Cases" optimal, weil es den schnellsten Erkenntnisgewinn bietet und die größte Sicherheit bietet, dass die Anwendung "richtig" funktioniert. Kleinteilig Unit-Tests zu ergänzen bringt wenig - insbesondere weil man in so einer Situation meist nur das Ist-Verhalten der Methoden testet und nicht das Soll-Verhalten (weil es in solchen Konstellationen selten ein Soll-Verhalten auf Methoden gibt, dass man nachprüfen kann). Und selbst wenn sich eine Methode "seltsam" oder "unintuitiv" verhält, weiß man nicht, ob andere Teile der Anwendung auf diesem Verhalten aufbauen. Sprich Unit-Tests finden hier eher selten Fehler. Sie sind eine Investition in die Zukunft und nicht schlecht - aber der direkte Impact ist geringer.

Unit-Tests - und das ist meine Ergänzung - sind sinnvoll bei neuem Code, weil sie damit auch direkt eine Dokumentation des Soll-Verhaltens sind. Sprich, wenn man so eine Anwendung weiterentwickelt, dann sollte man den Fokus darauf legen, Unit-Tests für neuen Code oder geänderten Code schreiben. Das ist einfacher - weil man eh gerade am Code dran ist - und zum anderen ist das Soll-Verhalten der Methoden/Unit klar. Das heißt, hier finde ich auch echte Fehler, wenn das Soll-Verhalten abweicht.
Das ist auch etwas, was Tools sonarqube in den teuren Lizenzen unterstützt, dass man Quality-Gates nur auf neuen/geänderten Code festlegen kann. Und so sukzessive zu einer besseren Anwendung kommt.
 
Ich stimme @LimDul voll zu. Ich würde es aber etwas weiter abstrahieren - auch wenn es unter dem Strich auf das Gleiche hinaus läuft:
Bei den Tests geht es immer darum, dass Anforderungen an den Code (Egal auf welcher Ebene) getestet werden. Die Anforderungen müssen also im Detail bekannt sein. Überall, so dies bekannt ist, können also entsprechende Tests geschrieben werden.

Bei bestehendem Code hat man leider sehr oft, dass Anforderungen nicht oder nicht ausreichend dokumentiert sind und da kommt es dann tatsächlich dazu, dass man zu oft keine Tests auf Methodenebene schreiben kann.

Ich sehe aber noch einen weiteren wichtigen Punkt: Legacy Code ist oft auch schlicht ohne Refactorings nicht Unit Testbar! Eine Unit greift halt auf zig andere Units zu und die will man nicht testen. Diese sind aber auch nicht einfach so zu mocken. Das ist aus meiner Sicht in der Praxis ein weiteres Problem.

Wenn man dann mit Legacy Code arbeiten muss, dann bleibt einem nur ein Vorgehen ähnlich:
  • Als erster Schritt kann man nur generell Feature Tests einführen. Unit Tests sind oft sinnvoll nicht möglich.
  • Wenn man die Features getestet hat, dann kann man Refactorings machen. Die Tests geben einem hoffentlich genug Sicherheit (Erfahrungen zeigen aber, dass man Features nicht komplett dokumentiert und damit nicht getestet hat.)
  • Und dann bekommt man auch Unit Tests. Diese sind in meinen Augen sinnvoll, denn diese dienen nicht nur dem Test sondern auch der Dokumentation. Die Features sind dort dokumentiert.

Wenn es also möglich ist, Unit Tests für Methoden zu schreiben, dann würde ich dies tun - einfach um damit die Verhaltensweise zu dokumentieren. Aber man sollte sich überlegen, diese Tests etwas zu kennzeichnen, denn es sind keine offiziellen Features die dokumentiert werden sondern nur Verhaltensweisen, die der Code aktuell zu haben scheint.

Und da würde mich direkt interessieren: Bin ich da mit meiner Sichtweise, dass Unit Tests auch als Dokumentation dienen sollten, aus eurer Sicht eher zu extrem? JavaDoc auf (Unit-) Tests? (Ich habe halt bisher eher begrenzte Erfahrung bezüglich Teams / Projekte, da ich in meinem beruflichen Werdegang wenig gewechselt habe und so eher weniger Entwickler-Teams erleben durfte ... Daher würden mich eure beruflichen Erfahrungen / eure Praxis interessieren als Erfahrungsaustausch)
 
Das stimmt, danke für die Ergäzung. Es gibt übrigens auch für Maven und Gradle Plugins, die die Testabdeckung für neuen Code messen können.
Cool, das maven Plugin kannte ich noch nicht. Hast du da zufällig noch einen Link dazu, wie man an das diff in einem maven/jenkins Build kommt? Reine Neugierde, sonst suche ich heute abend mal selber.

Ich denke da einen Jenkins Build, der einen diff auf origin/main macht und dass da nutzt.
 
Und da würde mich direkt interessieren: Bin ich da mit meiner Sichtweise, dass Unit Tests auch als Dokumentation dienen sollten, aus eurer Sicht eher zu extrem? JavaDoc auf (Unit-) Tests? (Ich habe halt bisher eher begrenzte Erfahrung bezüglich Teams / Projekte, da ich in meinem beruflichen Werdegang wenig gewechselt habe und so eher weniger Entwickler-Teams erleben durfte ... Daher würden mich eure beruflichen Erfahrungen / eure Praxis interessieren als Erfahrungsaustausch)
Teils teils, wir hatten schon Projekte, wo wir (mit extra Annotationen) Unit-Tests dokumentiert haben für Test-Reports. Ist dann aber mangels Nützlichkeit eingeschlafen.

Für mich sind Unit-Tests immer eine Dokumentation des Soll-Verhaltens. Deswegen halte ich es auch für wichtig, dass Unit-Tests lesbar & klein sind - so dass man das Soll-Verhalten auch ablesen kann. Ich schreibe tatsächlich manchmal - eher selten - Javadoc an Unit-Tests, wenn ich zwar eine Methode teste, aber dahinter eine echte, komplexe Fachlichkeit steht, wo der Setup-Code im Test umfangreich ist und nicht mehr direkt verständlich.
 
Cool, das maven Plugin kannte ich noch nicht. Hast du da zufällig noch einen Link dazu, wie man an das diff in einem maven/jenkins Build kommt? Reine Neugierde, sonst suche ich heute abend mal selber.
Das weiß ich leider nicht, wir nutzten sonar dafür, haben uns aber mal das als Alternative für die Zukunft angeschaut.

Und da würde mich direkt interessieren: Bin ich da mit meiner Sichtweise, dass Unit Tests auch als Dokumentation dienen sollten, aus eurer Sicht eher zu extrem? JavaDoc auf (Unit-) Tests?
(Unit-)Test sind eine wichtige Quelle für Informationen über ein System. Ich schau mir die auch meist als erstes an um zu verstehen, was das System macht. JavaDoc halte ich aber für das falsche Werkzeug um sie zu dokumentieren, denn JavaDoc hast du nicht im Testrunner oder Build-Report stehen. Was wir meistens machen, ist die Test-Methoden sprechenden zu benennen. Das sieht dann so aus:

Java:
@Test
void should_skip_null_values() {
    var result = MyStringJoiner.join("A", null, "B");
    assertThat(result).isEqualTo("A,B");
}

Wenn man dann doch ein bisschen mehr schreiben will, dann nutzen wir DisplayName

Java:
@Test
@DisplayName("""
             This test should test, that null values are skippt,
             and the rest of the parts are joined with ","
             """)
void should_skip_null_values() {
    var result = MyStringJoiner.join("A", null, "B");
    assertThat(result).isEqualTo("A,B");
}

Diese Info taucht dann gleich auf, falls ein Test dann rot wird, und man weiß genau was Sache ist, ohne erstmal den Code anzuschauen. Auch kann das recht nützlich sein, um mit der Fachabteilung zu reden, weil diese Info ist auch für die lesbar.

Wenn einem es wichtig ist, dass die Fachabteilung auch damit arbeiten kann, dann kann man über Dinge wie Cucumber oder Fitness nachdenken. Dort schreibt man die Tests in einer Sprache die alle Beteiligten verstehen und als Entwickler baut man Glue-Code, der dafür sorgt, dass die Tests auch ausgeführt werden. Aber das ist dann ein ganz anderes Thema und ich will hier nicht zu weit abschweifen.
 
Das weiß ich leider nicht, wir nutzten sonar dafür, haben uns aber mal das als Alternative für die Zukunft angeschaut.
Mir geht es ähnlich - das wäre eine Alternative ohne Lizenzkosten für Sonar, die man mit deutlich weniger organisatorischen Aufwand einführen kann. Wo ich nicht erst ein DevOps / Infrastruktur Team beauftragen muss Dinge einzurichten und zu beschaffen 🙂

(Unit-)Test sind eine wichtige Quelle für Informationen über ein System. Ich schau mir die auch meist als erstes an um zu verstehen, was das System macht. JavaDoc halte ich aber für das falsche Werkzeug um sie zu dokumentieren, denn JavaDoc hast du nicht im Testrunner oder Build-Report stehen. Was wir meistens machen, ist die Test-Methoden sprechenden zu benennen. Das sieht dann so aus:
Klar, das ist für mich selbstverständlich. Es gibt halt Konstellationen wo der Platz nicht mehr reicht. Die sollten selten sein - aber dann um so wichtiger ist es die sauber zu dokumentieren.

Wenn man dann doch ein bisschen mehr schreiben will, dann nutzen wir DisplayName
Und mal wieder was neues gelernt 🙂
Es ist erstaunlich, wie sehr man oft auf den gewohnten Pfaden unterwegs und gar nicht weiß, was es Alternativen gibt. Ich wusste bisher gar nicht, dass es DisplayName gibt. Damit hätten wir uns das ganze Geraffel mit der eigenen Annotation im Projekt sparen können.
 
Nochmal hier kurz eingehakt, für alle, die sich schon mal die zyklomatische Komplexität haben ermitteln lassen. Normalerweise misst ja diese nur wieviele Pfade es innerhalb von Methoden gibt und besagt, dass pro Pfad ein Testfall geschrieben werden muss.

Gibt es Tool oder kann zum Beispiel SonarQube folgendes:
Die zyklomatische Komplexität aller Methoden einer Klasse aufsummieren, sodass übersichtlich angezeigt wird, welche zyklomatische Komplexität jede Klasse hat?

Trotzdem erscheint auch diese Metrik problematisch.
Z.b. hätte eine Methode mit einem switch-case, das 10 oder mehr Fälle hat, eine hohe zyklomatische Komplexität, auch wenn die inhaltliche Komplexität einfach ist und Testfälle schnell geschrieben sind.
Dagegen gibt es sicherlich auch Methoden, die inhaltlich sehr komplex sind und das Entwickeln von Unit-Tests einen hohen Zeitaufwand bedeutet, obwohl sie z.B. nur einen Pfad besitzen und von dieser Metrik als wenig komplex eingestuft werden würden. Sehe ich das richtig?
 
Die zyklomatische Komplexität aller Methoden einer Klasse aufsummieren, sodass übersichtlich angezeigt wird, welche zyklomatische Komplexität jede Klasse hat?
Hinsichtlich der Testabdeckung wird dir das nicht so viel bringen wie du dir erhoffst. Die zyklomatische Komplexität sagt nicht sehr viel über den Aufwand was zu testen ist, aus.

Java:
public static String getGermanWeekday(Weekdays weekday) {
        switch (weekday) {
            case MONDAY:
                return "Montag";
            case TUESDAY:
                return "Dienstag";
            case WEDNESDAY:
                return "Mittwoch";
            case THURSDAY:
                return "Donnerstag";
            case FRIDAY:
                return "Freitag";
            case SATURDAY:
                return "Samstag";
            case SUNDAY:
                return "Sonntag";
        }
        return null;
    }

Der Code hat eine zykolmatische Komplexität von 7, genauso wie:

Java:
  public String getWeekdayInUserLanguage(Weekdays weekday) {
        var authentication = SecurityContextHolder.getContext().getAuthentication();
        String currentUserName = null;
        if (!(authentication instanceof AnonymousAuthenticationToken)) {
            currentUserName = authentication.getName();          
        } else {
            currentUserName = "anonymous";
        }
        var lang = "en";
       
        if(!currentUserName.equals("anonymous")) {
            var user = userService.getUserByName(currentUserName);
           
            if(user != null) {
                var langFromUser = user.getLanguage();
                if(langFromUser != null) {
                    lang = langFromUser;
                }
            }
        }
       
        if(lang.equals("de")) {
            return getGermanWeekday(weekday);
        } else {
            return getEnglishWeekday(weekday);
        }
    }

Wobei der obere Code sehr trivial zu testen ist, der untere nicht. Der untere hat eine höhere kognitive Komplexität aber das alleine ist nicht nur der Grund für den höheren Testaufwand. Im unteren Fall musst du zum Beispiel für den UserService einen Mock erstellen, und sowas wie SecurityContextHolder.getContext().getAuthentication() ist nur mit Powermock zu mocken.

Ich vermute, dass du abschätzen willst, wie hoch der Testaufwand ist, ich glaube aber, dass alleine die zykolmatische Komplexität dafür nicht genug ist. Mir fällt ehrlich gesagt auch kein guter Ansatz dazu ein, daher wäre ich sehr interessiert, wenn du deine finalen Ergebnisse teilen würdest.
 
Hi!

Eine Sache möchte ich noch bzgl. dem Verständnis klären, um sicherzugehen, dass ich die Semantik richtig verstehe - es geht um folgenden Abschnitt, der die Unit-Testabdeckungsziele definiert:

1.) ... mit dem langfristigen Ziel, eine Gesamtabdeckung von 80 % zu erreichen.

2.) Hierbei ist speziell vorgesehen, dass alle Unit-Tests eine Anweisungsabdeckung von 100 % und eine Zweigabdeckung von mindestens 80 % erzielen müssen.

@ 1.) Das bedeutet einfach, dass mindestens 80% der Codebasis durch Unit-Tests abgedeckt sein sollen, oder?
@ 2,) Verstehe ich es richtig, dass das NICHT bedeutet, dass
a.) 100% der Codebasis durch Unit-Tests abgedeckt sein sollen, sodass Anweisungsabdeckung gegeben ist und mind. 80% der Codebasis
durch Unit-Tests abgedeckt sein müssen, sodass Zweigabdeckung besteht - also das nicht, SONDERN
b.) , dass diese Vorgabe sich auf die Unit-Tests selber bezieht. Also dass für alle Unit-Tests gelten soll, eine Anweisungsabdeckung von 100% zu
erzielen sowie eine Zweigabdeckung von mind. 80%.

Also angenommen eine Software hätte fiktiv in der Codebasis nur 100 Methoden.
Dann wäre 1 erfüllt, wenn es für mindestens 80 Methoden einen Unit-Test gibt.

Wäre 2 auch erfüllt, wenn es nur 10 Unit-Tests gibt (also 10% Gesamtabdeckung), aber diese 10 Unit-Tests haben bzgl. ihren getesteten Methoden 100% Anweisungsabdeckung und 80% Zweigabdeckung?

Ich hoffe, dass mein Verständnisproblem klar ist 🙂

Kann jemand für Aufklärung sorgen?
 
Bzgl. Punkt 1 ist halt die Frage, was man als Maßstab nimmt: Code-Zeilen? Anweisungen? Methoden? Anforderungen?

Ich würde mal annehmen, dass hier gemeint ist, dass 80 % aller Anweisungen in der Codebasis durch Unit-Tests abgedeckt sein sollen. D. h. nach Ausführung aller Unit-Tests wurden 80 % aller Anweisungen, die sich in der Codebasis befinden, mindestens einmal durchlaufen.

Das ist nicht gleichbedeutend mit 80 % aller Methoden: wenn Du 8 Methoden mit insgesamt 80 Anweisungen hast und zwei Methoden mit insgesamt ebensfalls 80 Anweisungen, dann kannst Du zwar 8 Unit-Tests für die 8 der 10 Methoden schreiben und damit 80 % der Methoden testen, es werden jedoch nur 80 der 160 Anweisungen und damit 50 % aller Anweisungen abgedeckt.

Punkt 2 verstehe ich derart, dass für jeden Unit-Test gilt, dass nach seiner Ausführung 100 % der Anweisungen und 80 % aller Pfade des Prüflings wenigstens einmal durchlaufen worden sind.
 
Bzgl. Punkt 1 ist halt die Frage, was man als Maßstab nimmt: Code-Zeilen? Anweisungen? Methoden? Anforderungen?

Ich würde mal annehmen, dass hier gemeint ist, dass 80 % aller Anweisungen in der Codebasis durch Unit-Tests abgedeckt sein sollen. D. h. nach Ausführung aller Unit-Tests wurden 80 % aller Anweisungen, die sich in der Codebasis befinden, mindestens einmal durchlaufen.

Das ist nicht gleichbedeutend mit 80 % aller Methoden: wenn Du 8 Methoden mit insgesamt 80 Anweisungen hast und zwei Methoden mit insgesamt ebensfalls 80 Anweisungen, dann kannst Du zwar 8 Unit-Tests für die 8 der 10 Methoden schreiben und damit 80 % der Methoden testen, es werden jedoch nur 80 der 160 Anweisungen und damit 50 % aller Anweisungen abgedeckt.

Punkt 2 verstehe ich derart, dass für jeden Unit-Test gilt, dass nach seiner Ausführung 100 % der Anweisungen und 80 % aller Pfade des Prüflings wenigstens einmal durchlaufen worden sind.
Ja, denke genau so ist es - danke!

Also 1: Bezieht sich darauf, dass mind. 80% aller Anweisungen der Codebasis durchlaufen wird.
-> Ich glaube die TestCoverage im SonarQube-Dashboard (sofern man in seinem Maven-Projekt z.B. jacoco verwendet, weil das erkennt SonarQube bzw. dessen Analyse-Ergebnisse) sagt genau das aus.

Und 2 hätte ich auch so verstanden wie du - der Prüfling ist dann die getestete Klasse oder getestete Methode.
 

Zurück
Oben