Unit-Tests bei Methoden ohne jeden Rückgabewert

Hallo,
ich arbeite mich gerade in JUnit und Mockito ein. Was mir Kopfschmerzen bereitet ist, wie bekomme ich einen Test für eine Methode hin die keine Parameter und auch keinen Rückgabewert hat hin.

public static void printUsage(){
        PARSER.printUsage(System.out);
    }
...```
Zur Erläuterung, durch printUsage() werden die Konsolen-Parameterbeschreibungen für args4J ausgegeben. Ich habe allerdings auch bei vielen Methoden wo mir für private Methoden keine Getter zur Verfügung stehen und dies auch gewünscht ist,
Kennt hier Jemand eine Lösung? Eventuell mit Mocking? Darin habe ich mich noch nicht sonderlich tief eingearbeitet.

Hängt in dem konkreten Fall davon ab, was parser macht und wo parser herkommt.

Falls Parser eine rein statische Klasse ist, könntest du mit PowerMockito die statische Klasse mocken und den printUsage-Aufruf verifizieren.
Alternativ könntest du für den Test System.out auf einen dummy-Printstream setzen und verifizieren, dass bei dem Methodenaufruf irgendwas darauf aufgerufen wird.

Methoden haben, wenn sie nichts zurück geben, andere Seiteneffekte, auf die man prüfen kann. In dem genannten Beispiel wird die Methode printUsage ja irgendwie mit dem OutputStream/PrintStream interagieren (writte-Aufruf(e), flush, close). Bei bekannter Ausgangslage sind diese Aufrufe vorhersehbar. Zur Überprüfung, ob die Aufrufe wie erwartet passiert sind, kann ein gemockter OutputStream/PrintStream genutzt werden. So wie Deine Methode jetzt definiert ist, kannst Du ihn aber nicht übergeben. Du hast hier also ein sehr gutes Beispiel dafür, warum Methoden, die sich ihre Abhängigkeiten (hier System.out) selbst besorgen, schwer testbar sind. Überlege Dir, wie Du die Methode oder die Klasse so ändern kannst, dass der tatsächlich zu nutzende PrintStream austauschbar wird.

Und noch ein Nachtrag zu privaten Methoden/Feldern:
Da gibt es zwei mögliche Auswege. Entweder Du setzt die Sichbarkeit auf default hoch (sprich kein modifizierer) oder Du veränderst sie innerhalb Deines Tests mittels Reflection. Ich bevorzuge den ersten Weg. Man könnte sagen: “Für mich ist default das neue private.”

[quote=nillehammer]Und noch ein Nachtrag zu privaten Methoden/Feldern:
Da gibt es zwei mögliche Auswege.[/quote]
Und andere wiederum schlagen einen dritten Weg vor: “Teste keine privaten Methoden!”
Private Methoden werden naturgemäß nur innerhalb der eigenen Klasse aufgerufen und werden bei korrektem Unittesting daher automatisch mitgetestet, wenn man die Methoden höherer Sichtbarkeit vollständig testet.

*** Edit ***

Prinzipiell sollte man nicht die Methoden testen, sondern Funktionalitäten.

Hab ich anfangs auch so gemacht. Ich denke aber, dass der Whitebox-Gedanke hinter Unittests es rechtfertigt, dediziert alles einzeln zu testen. Bei Deinem Ansatz habe ich in dem Whitebox-Test ja dann doch wieder Blackbox Anteile (eben die privaten Methoden). Das mochte ich nicht. Darüber hinaus war es auch manchmal ein Gefrickel, Testaufrufe so zu bauen, dass auch in den privaten Methoden, alle möglichen Zweige durchlaufen wurden.

Ich weiß nicht… Ich sehe das mit Whitebox-Tests anders. Vielleicht habe ich auch ein falsches Verständnis vom Unittesting, aber ich halte es so, dass ich möglichst versuche Blackbox-Tests zu schreiben. Manchmal werden die zu Greyboxes, aber ich versuche die Tests mit möglichst wenig Wissen von der Implementierung zu erstellen.
Wenn Interaktion getestet wird, dann kennt man natürlich das API. Aber genau das soll ja auch getestet werden. Wenn der Zustand getestet wird, dann kann der Test komplett “schwarz” bleiben.

Ja, ich glaube, da sind unsere Ansätze verschieden. Wenn ich einen Unittest schreibe, dann explizit mit Bezug auf den Quellcode der zu testenden Klasse. D.h. ich weiß und beachte, dass es an dieser Stelle eine Verzweigung gibt, dass ich an jener Stelle eine non-null Referenz brauche etc. Dann schreibe ich Tests, die das ganz genau austesten. Alles mit dem Ziel, in Unittests eine Code- und Zweigüberdeckung von 100% hinzubekommen. Das Prinzip gilt für mich aber auch nur da. Ansonsten halte ich es ähnlich wie Du.

Isolierte Unit Tests sind eigentlich immer White Box Tests IMHO, auch sind Mocks ein Anzeichen fuer White Box :wink:
Bei Integrationstests kann das anders sein…

Das Problem des TE ist, dass der Code schlecht zu testen ist (static!!) weil es spezielle Mocking- Frameworks/Techniken verlangt.

Koennte man stattdessen eine Instanz von PARSER per Konstruktor „Injecten“ waere das trivial zu loesen, egal mit welchem Mocking Framework.

[quote=nillehammer]. Alles mit dem Ziel, in Unittests eine Code- und Zweigüberdeckung von 100% hinzubekommen.[/quote]Das Ziel erreicht man viel besser und ohne White-Box wenn man die Funktionalität testet und nach den TDD-Regeln vor geht.
Alles andere führt ehr früher als später zu Problemen…

Das Problem vom TO ist genau so ein Resultat. Wäre er nach TDD vorgegangen wäre klar, gewesen, dass das gewünschte Verhalten das Schreiben in den stdout ist und dass man dies am simpelsten dadurch testet, dass man für die Dauer der Ausführung des Produktivcodes System.out durch ein Mock ersetzt, bei dem man dann prüft, ob der erwartete Text ausgegeben wurde.
Ob dieses Mock eine eigene Implementierung ist oder von einem Mocking-Framework erstellt wird ist dann schon nicht mehr soooo wichtig…

bye
TT

Ein wichtiges Argument für Blackbox- / Funktionalitätstests und gegen Whitebox-Tests fehlt hier noch:
Bei Whitebox-Tests treibt man Refactoring schlimmstenfalls ad absurdum, weil die Tests viel zu stark an die Implementierung gekoppelt sind. Ein Refactoring des Codes hat dadurch automatisch ein Refactoring der Tests zur Folge. Bei Blackbox-Tests braucht man diese bei einem Refactoring nicht anzupassen und kann somit selbstbewusst refaktorisieren.

[quote=Timothy_Truckle]Das Ziel erreicht man viel besser und ohne White-Box wenn man die Funktionalität testet und nach den TDD-Regeln vor geht.
Alles andere führt ehr früher als später zu Problemen…[/quote]
Das stimmt schlicht nicht. Die Metriken beschreiben ja gerade eine Überdeckung im Verhältnis zu Code. Es ist geradezu widersinnig, Überdeckung von Code zu fordern, ihn aber nicht hinzuzuziehen und nur die public API bei den Tests aufzurufen. Im Gegenteil lässt sich das Ziel viel schneller erreichen, wenn man den zu testenden Teil “möglichst direkt” aufruft.

In den anderen von Dir vorgebrachten Punkten mag ich keinen Widerspruch zu dem von mir Geschriebenen sehen. Auch ich habe geschrieben, dass die Methode printUsage() schlecht testbar ist, weil eben nicht nach TDD vorgegangen wurde. (Die Nennung des Begriffs hab ich mir gespart). Und auch der Ansatz zur Änderung sowie das Mocken ist bereits genannt worden.

[quote=cmrudolph;100732]Ein wichtiges Argument für Blackbox- / Funktionalitätstests und gegen Whitebox-Tests fehlt hier noch:
…Ein Refactoring des Codes hat dadurch automatisch ein Refactoring der Tests zur Folge. Bei Blackbox-Tests braucht man diese bei einem Refactoring nicht anzupassen und kann somit selbstbewusst refaktorisieren.[/quote]
Der Einwand deckt sich mit meinen Erfahrungen. Ein Refactoring meiner Klassen hat praktisch immer ein Refactoring der Tests zur Folge. Dadurch, dass die Tests alle sehr feingranular sind und ich auch Doppeltests vermeide, hält sich der Aufwand aber in Grenzen. Im übrigen ist man auch bei Blackbox-Tests nicht ganz vor Refactorings gefeit.

Warum ich Whitebox-Tests auf der aller ersten Ebene der Tests (und nur hier) befürworte:

  • Die Tests testen den eigenen von einem selbst implementierten Code. Wer, wenn nicht man selbst, sollte sie denn sonst machen? (“Niemand” ist keine befriedigende Antwort :))
  • Man mag über den Sinn von Metriken diskutieren. Aber ich finde es schon sinnvoll, dass es Tests gibt, die sicher stellen, dass jede Zeile und jeder Zweig im Code mit allen wichtigen möglichen Testdaten durchlaufen wurden. Denn jede Zeile/Zweig ist eine potenzielle Fehlerquelle.
  • Fehler lassen sich leichter lokalisieren.

TDD sagt doch deutlich dass die Unit Tests White Box sind.
Ein Mock ist ein eindeutiges Anzeichen fuer White Box!

Ich empfinde das Anpassen eines PROD-Codes immer als falsch, wenn der einzige Grund der Test ist.

Durch TDD komme ich auch selten an Stellen, an denen ich private Methoden testen möchte. Entweder solche Methoden sind reine Klassenmethoden oder es ist sinnvoll, diese in eine eigene Klasse zu heben. Aber bei dem Gedanken, den private-Modifier zu entfernen, sträuben sich meine Nackenhaare.

Wenn der Test für die verschiedenen Ein- und Ausgänge meiner Methode zu groß wird, ist diese Methode vielleicht in ihrer Gesamtheit zu groß.

Das sehe ich nicht so. Mit einem Mock lässt sich Interaktion testen. Aber nur weil ich weiß, dass z. B. ein Observable bei einem bestimmten Ereignis ein Event-Objekt verschicken muss (oder auch nur eine notify()-Methode aufrufen muss), heißt das noch nicht, dass der Test dadurch White-Box wird.
MMn testet man mit einem Mock, ob ein API eingehalten wird.

Wieso? Weil die Notwendigkeit einer Codezeile sich ausschließlich in einem Test begründet?
Der Test entsteht, weil eine Funktionalität gefordert ist. Es wird also ein was und nicht ein wie gefordert.
Ich stimme allerdings zu, dass eine Anforderung sich, sofern sie denn feingranular/spezifisch genug ist, auf nicht allzu viele verschiedene Weisen umsetzen lässt, was eine gewisse Kopplung mit dem Code impliziert.

Das oben beschriebene Szenario ist eine klare Verletzung des SRP (Wikipedia tut so, als ob es nur auf Klassen anwendbar wäre, dabei ist es sowohl auf höherer Ebene - wie Packages, Module, Bibliotheken - als auch auf niedriger Ebene - wie Methoden - nützlich).

Es gibt in deinem Beispiel zwei Gründe, warum sich deine printUsage-Methode ändern könnte: Wenn sich das Ausgabeformat ändern, oder der Ort, an den es geschrieben wird. Letzteres ist gar nicht einmal so unwahrscheinlich (man denke an eine Log-Datei oder Swing-Console). Was ist, wenn du ein zusätzliches Ausgabeformat unterstützen willst (etwa ausführlicher zur Debug-Unterstützung)? Was ist, wenn du woandershin schreiben willst? Was ist, wenn du dieselbe Ausgabe sowohl auf der Konsole wie auch in der Log-Datei haben willst? Das heißt, deine Methode ist unflexibel und zu stark auf den aktuellen Fall spezialisiert.

Eine offensichtliche Verbesserung wäre, den Stream für die Ausgabe nicht hart festzuschreiben:

public static void printUsage(OutputStream os){
        PARSER.printUsage(os);
}

Die Methode wird nun nicht nur flexibler, sondern ist automatisch auch testbar - einfach einen ByteArrayOutputStream mitgeben und auslesen. Wenn der Aufruf zu unbequem ist, kann man immer noch eine nicht zu testende (weil triviale) Bequemlichkeitsmethode schreiben:

public static void printUsage(){
      printUsage(System.out);
}

Hier wahrscheinlich weniger sinnvoll (aber dazu müsste man die Struktur besser kennen), aber bei anderen Seiteneffekt-Methoden eventuell hilfreich ist, die eigentliche Berechnung von der Anwendung des Seiteneffekts zu trennen. Dann kann man die eventuell komplizierte Berechnung problemlos testen, und der Seiteneffekt ist dann oft so trivial, dass man sich einen Test schenken kann, oder nur eine grobe Prüfung wie “ja, es ist etwas passiert, und es ist dabei kein Fehler aufgetreten” notwendig ist:

public static String getUsage() {
   return PARSER.getUsage();
}

public static void printUsage(String output){
      System.out.println(output);
}

[quote=nillehammer]Zitat Zitat von Timothy_Truckle Beitrag anzeigen
Das Ziel erreicht man viel besser und ohne White-Box wenn man die Funktionalität testet und nach den TDD-Regeln vor geht.
Alles andere führt ehr früher als später zu Problemen…
Das stimmt schlicht nicht. Die Metriken beschreiben ja gerade eine Überdeckung im Verhältnis zu Code.[/quote][KleinkindMode]Doch![/KleinkindMode]

Was sind denn die Regeln des TDD?[ol]
[li] Schreibe einen Test für ein Verhalten, das der Produktiv-Code noch nicht hat.[/li][li]Implementiere dieses Verhalten.[/li][li]Refactor[/li][/ol]
Schritte 1 und zwei sind dabei enger miteinander verzahnt: [ol]
[li]Schreibe genau so viel Test für die neue Funktionalität, bis der Test fehl schlägt (dabei gilt nicht kompilieren ist fehlschlagen), keines Falls mehr![/li][li]Schreibe genau so viel Produktiv-Code bis der Test grün ist, keines Falls mehr![/li][li]Wenn der Test die Funktionalität noch nicht testet erweitere ihn, Falls doch schreibe einen neuen Test für ein weiteres Feature (und/oder refactor).[/li][/ol]
Wenn man das so mach hat man hinterher genau 100% Abdeckung des Produktivcodes. Das lässt sich gar nicht vermeiden.

Die Kunst ist, das Verhalten des Produktiv-Codes klein genug aufzudröseln. Das muss man aber sowieso machen, weil man ja sonst etwas Falsches implementiert.

[quote=nillehammer;100765]Ziel viel schneller erreichen, wenn man den zu testenden Teil “möglichst direkt” aufruft.[/quote]Richtig, gewöhnlich ist dass aber ein Zeichen dafür, dass man gegen das “Single Responsibility Pattern” verstoßen hat und die zu testende Funktionalität in ein eigenes Objekt mit öffentlichen Schnittstellen gehört hätte…

[quote=maki;100769]Ein Mock ist ein eindeutiges Anzeichen fuer White Box![/quote]Das sehe ich anders. Das Mock ist eine Abhänigkeit des zu Testenden Codes, und sagt an sich nichts über den zu testenden Code aus, außer das wir erwarten, dass auf dem Mock eine bestimmte Methode aufgerufen wird.
Oder musst Du wissen, wie Swing implementiert ist, um eingen Listener-Objekte zu registrieren?

[quote=Sym;100771]Aber bei dem Gedanken, den private-Modifier zu entfernen, sträuben sich meine Nackenhaare.[/quote]Mir auch.

bye
TT

[quote=Timothy_Truckle;100785][KleinkindMode]Doch![/KleinkindMode][/quote]:smiley:

Der Einwand ist allerdings berechtigt. Auch das deckt sich mit meinen Erfahrungen. Viele Methoden, die zunächst als (Hilfs-)Methoden in einer Klasse steckten, wandern oft in eigene Klassen. Am Ende bleiben eigentlich immer nur einige wenige übrig, die direkt auf Membern der Klasse arbeiten und deswegen dort verbleiben.

@Landei :
Deine erste Methode halte ich hier für die gangbarste. Sieht man ja auch oft genug in der Java-Api selber, z.B. bei Exceptions (“printStackTrace()”). Allerdings wird dort statt OutputStream tatsächlich ein PrintStream übergeben, was, wie ich finde auch gar nicht so verkehrt ist.

Zum Thema:
Warum muss bei TDD eigentlich immer gemockt werden? Das klingt bei mir immer danach, was bei bygones in der Signatur steht: “…you probably not doing it right.”

[quote=Spacerat]Warum muss bei TDD eigentlich immer gemockt werden? Das klingt bei mir immer danach, was bei bygones in der Signatur steht: “…you probably not doing it right.”[/quote] @Landei hatte zum Thema “ist TDD tot” zwei Videos gepostet. In einem davon hat Kent Beck sich auch zum Mocking geäußert. Er versucht mocking weitestgehend zu vermeiden:
21:10 vom Video