Discussion:
Unit-Tests von Einheiten ohne öffentliche Leseschnittstelle
(zu alt für eine Antwort)
Christian H. Kuhn
2016-07-07 19:19:45 UTC
Permalink
Hallo Gemeinde,

Bevor hier der Löschantrag wegen Inaktivität kommt, mach ich doch lieber
nochmal Traffic :-)

Auch mein aktuelles Projekt hat mal wieder Lerneffekte, wo ich sie nicht
erwartet habe. Ziel war, Gradle kennenzulernen und in Jenkins zu
integrieren. Dazu habe ich mir ein gemischtes Java-/Android-Thema
herausgesucht, da kann ich dann auch Android Studio kennenlernen. Mit
Android 2.3.3 hatte ich im Studium mal zu tun, mal schauen, was sich
geändert hat.

Ich schreibe eine Schachuhr. Die, die es gibt, sind mehrheitlich von
geringer Kenntnis des internationalen Turnierschachs und der FIDE-Regeln
geprägt, es gibt also sogar mögliche Anwender. Grobstruktur: drei
Pakete. Ein Paket common mit der Funktionalität, ein Paket java mit der
Java-Swing-GUI, ein Paket android mit der Android-GUI. Letzteres kommt
später und soll erstmal nicht interessieren.

Die Anwendung soll eine elektronische Schachuhr darstellen. Eine
Schachuhr hat zwei Uhren, die von einer vorgegebenen Zeit abwärts laufen
und bei Erreichen der Null ein optisches Zeichen („Fallblättchen“)
geben; zwei Knöpfe, die die Uhr auf der Seite anhalten, auf der der
Knopf gedrückt wird, und die Uhr auf der anderen Seite in Gang setzen,
so dass immer nur eine Uhr läuft; einen Knopf zum Stoppen; einen Knopf
zum Zurücksetzen. Verschiedene Funktionen zum Einstellen der Uhr sind im
Menü enthalten. Zur Zeit ist nur eine Bedenkzeitperiode möglich. Erlaubt
sind klassische (Zeit pro Partie) und Fischer-Bedenkzeiten (Zeit pro
Partie, kumulativer Zeitzuschlag pro Zug). Künftige Versionen sollen
auch mehrere Bedenkzeitperioden (mit und ohne Zugzähler) und
Bronstein-Bedenkzeiten (nicht-kumulativer Zeitzuschlag pro Zug)
enthalten. Eine Light-Version erlaubt nur voreingestellte Bedenkzeiten,
eine Pro-Version erlaubt freie Wahl der Bedenkzeit und ermöglicht
Sonderfunktionen für den Schiedsrichter.

Das Paket common enthält zwei Klassen, zwei Interfaces und einen
Aufzählungstyp. QChessClock ist die Klasse für die Gesamtfunktion. Sie
implementiert das Interface QChessClockObservable (QChessClockObserver
wird von den GUI implementiert). Der Aufzählungstyp QChessClockState
wird in einer privaten Variablen benutzt, um den Zustand (läuft,
angehalten, nicht gestartet) festzuhalten. Zwei weitere private
Variablen sind vom Typ QChessTimer. Die öffentliche Schnittstelle
besteht aus Methoden, die das Drücken der Knöpfe und die Menübefehle
abbilden, und den Methoden zum Registrieren und Benachrichtigen der
Observer. Außer notifyObservers() gibt es keine öffentliche Funktion zum
Abfragen des Zustands von privaten Variablen.

QChessTimer stellt die einzelne Uhr dar. Die „öffentliche“ Schnittstelle
ist nur paketsichtbar, da ein direkter Zugriff auf die einzelnen Uhren
nur über die Hauptklasse QChessClock erfolgen soll. Verschiedene
Konstruktoren erlauben das initiale Setzen der Bedenkzeit. start() und
stop() dienen zum Starten und Anhalten der Uhr. isRunning() gibt den
Status (läuft, angehalten) an, isFlagFallen() zeigt ein gefallenes
Blättchen an. unsetFlagFallen() setzt das Fallblättchen zurück. Und
schließlich gibt getRemainingSeconds() die verbleibende Zeit in Sekunden an.

Wer den Quellcode sehen will: https://www.qno.de/gitweb/

Das Coden war jetzt tatsächlich das kleinere Problem, und Gradle ist
zwar anders als Maven, aber gut bedienbar. Probleme tauchen beim Testen
auf. Ich will ja TDD anwenden. Also erst die Tests und dann den Code
schreiben. Hat mit JUnit4 bei QChessTimer auch ganz gut funktioniert.

Bei QChessClock habe ich nicht den Schimmer einer Vorstellung, wie ich
die Klasse testen soll. Es gibt keine Getter-Funktionen, mit denen sich
was überprüfen ließe. Ich kann also testen, dass das Testobjekt nach
Konstruktor ungleich null ist, sonst nichts. Eine Änderung der
Schnittstelle nur zu Testzwecken kommt selbstverständlich nicht in
Frage. Bleibt eigentlich nur, dass die Testklasse QChessClockObserver
implementiert und auf die Benachrichtigungen des Observable wartet. Der
Weg ist gangbar, ich habe ihn aber noch in keinem Buch gefunden. Daher
vermute ich, dass es da eleganteres gibt?

In noch extremerem Umfang gilt das für die Java-GUI. Die GUI erzeugt ein
QChessClock-Objekt und registriert sich dort als Observer. Sie stellt
die erwähnten Knöpfe und Menüs bereit, die auf die öffentlichen Methoden
der Schachuhr zugreifen. Öffentliche Methoden sind außer dem Konstruktor
und actionPerformed() noch die verschiedenen update()-Funktionen, über
die das Observable seine Zustandsänderungen mitteilt. Auch hier habe ich
praktisch nichts, was ich mit JUnit testen könnte, und andere Tests der
Klasse kenne ich nicht. Auch für den Integrationstest des gesamten
Systems scheint JUnit das falsche Mittel zu sein.

In der Folge ist entsprechend die Testabdeckung, die von JaCoCo
ermittelt wird, entsprechend niedrig. Wenn andere Tests eingesetzt
werden, werden die von JaCoCo erkannt? Oder muss man Klassen dann aus
der Testabdeckungsanalyse exkludieren?

Und schließlich die alles entscheidende Frage: Kann mir das jemand hier
beantworten? Oedr denke ich zu theoretisch?

TIA
QNo
Peter
2016-07-07 20:37:05 UTC
Permalink
Post by Christian H. Kuhn
Hallo Gemeinde,
[viel Text]

Dein Vorgehen ist falsch.
Du mußt ein Projekt so aufsetzen, daß es von Anfang an testbar ist.
Die ganz hart gesottenen schreiben erst die Testklassen, und danach die
Implementierung.
Nicht ganz so radikal aber wirksam:
Die Implementierung und die Testklassen gemeinsam entwerfen.
Der Sinn dieses Vorgehens besteht genau darin, daß das Projekt von
Anfang an testbar ist, und nicht im nachhinein festgestellt wird, daß
man das Design nicht vernünftig testen kann.

Package-private Schnittstellen kann man testen, indem du das
Test-Package mit der Implementierung identisch benennst;
Du hast also im Projekt zwei Ordner mit Sourcen:
src/myPack/MyClass
und
test/myPack/TestMyClass

Und besorg dir ein Werkzeug, welches die Testabdeckung mißt; so kannst
du leicht feststellen, wo noch Tests fehlen.
Michael Paap
2016-07-07 21:21:40 UTC
Permalink
Post by Peter
Dein Vorgehen ist falsch.
Du mußt ein Projekt so aufsetzen, daß es von Anfang an testbar ist.
Die ganz hart gesottenen schreiben erst die Testklassen, und danach die
Implementierung.
Ich lese: "Probleme tauchen beim Testen auf. Ich will ja TDD anwenden.
Also erst die Tests und dann den Code schreiben."
Post by Peter
Und besorg dir ein Werkzeug, welches die Testabdeckung mißt; so kannst
du leicht feststellen, wo noch Tests fehlen.
Ich lese: "In der Folge ist entsprechend die Testabdeckung, die von
JaCoCo ermittelt wird, entsprechend niedrig."

Auch wenn's schwerfällt: Es ist durchaus sinnvoll, zu lesen, worauf man
antwortet.

Gruß,
Michael
Christian H. Kuhn
2016-07-08 12:09:10 UTC
Permalink
Post by Michael Paap
Auch wenn's schwerfällt: Es ist durchaus sinnvoll, zu lesen, worauf man
antwortet.
Danke.

mfg
QNo
Wanja Gayk
2016-07-08 20:10:13 UTC
Permalink
Post by Peter
[viel Text]
Dein Vorgehen ist falsch.
Diese Aussage ist in ihrer Pauschalität falsch.
Post by Peter
Du mußt ein Projekt so aufsetzen, daß es von Anfang an testbar ist.
Die ganz hart gesottenen schreiben erst die Testklassen, und danach die
Implementierung.
Die Implementierung und die Testklassen gemeinsam entwerfen.
Geht nicht immer, will man nicht immer, ist oft auch zu aufwändig.
Manchmal programmiert man ohne klares Ziel, weil das noch nicht in Sicht
ist und man sich langsam voran tastet, bzw. baut man wärend der
Implementation sehr oft noch sehr viel um, refactored eine Menge und man
will dafür nicht immer die Tests komplett umbauen, weil sich die API
ändert (weil man auf halbem Wege merkt, dass sie doch nicht so elegant
war, wie man dachte und einem was besseres einfällt).

Es ist in so einem Fall pragmatischer und schneller sich erstmal ins
Getümmel zu werfen und erst dann mit den Tests zu beginnen, wenn sich
die Lage beruhigt. Bis dahin sollte man nach meiner Erfahrung einige
Prinzipien beachten, wie z.B. Singletons und Monostates zu vermeiden,
Dependencies zu injizieren (z.B. im Kontruktor), bzw. direkt ein DI-
Framework zu nutzen (CDI, Guice, Dagger2, you name it), um sich den Weg
in die Testbarkeit nicht zu verbauen.

Gruß,
-Wanja-
--
..Alesi's problem was that the back of the car was jumping up and down
dangerously - and I can assure you from having been teammate to
Jean Alesi and knowing what kind of cars that he can pull up with,
when Jean Alesi says that a car is dangerous - it is. [Jonathan Palmer]
Patrick Roemer
2016-07-08 22:17:16 UTC
Permalink
Post by Wanja Gayk
Manchmal programmiert man ohne klares Ziel, weil das noch nicht in Sicht
ist und man sich langsam voran tastet, bzw. baut man wärend der
Implementation sehr oft noch sehr viel um, refactored eine Menge und man
will dafür nicht immer die Tests komplett umbauen, weil sich die API
ändert (weil man auf halbem Wege merkt, dass sie doch nicht so elegant
war, wie man dachte und einem was besseres einfällt).
Aber wie merkt man das denn ohne Tests (oder sonstigen Code der mit dem
API arbeitet)? :)

Natürlich lege ich auch erst mal explorativ los, und schreibe ganz
bestimmt nicht erst mal für jede Zeile "Produktivcode" einen Test, der
diese erzwingt. Aber ich will relativ bald ein Gefühl dafür bekommen,
wie sich so ein API anfühlt - und den explorativen Code dafür schreibe
ich doch lieber gleich als Testcases. Wenn das API sich zu stark ändert,
schmeisse ich die entsprechenden Tests lieber weg anstatt zu
refaktorieren und schreibe neue. Und wenn es sich stabilisiert, habe ich
schon eine Basis, aus der man eine "ordentliche" Testsuite wachsen
lassen kann.

Viele Grüße,
Patrick
Wanja Gayk
2016-07-17 22:33:33 UTC
Permalink
Post by Patrick Roemer
Post by Wanja Gayk
Manchmal programmiert man ohne klares Ziel, weil das noch nicht in Sicht
ist und man sich langsam voran tastet, bzw. baut man wärend der
Implementation sehr oft noch sehr viel um, refactored eine Menge und man
will dafür nicht immer die Tests komplett umbauen, weil sich die API
ändert (weil man auf halbem Wege merkt, dass sie doch nicht so elegant
war, wie man dachte und einem was besseres einfällt).
Aber wie merkt man das denn ohne Tests (oder sonstigen Code der mit dem
API arbeitet)? :)
Nicht jede Methode ist Teil einer öffentlichen API, sondern das meiste
Zeug ist private (bzw. package private) oder Teil einer groben Idee.

Beispiel: Ich will ne Liste von Objekten zusammen stellen, die sich aus
irgendeinem Zustand ergibt (Zeilen aus einer tabelle, Artikel, bestimmte
Objekte aus einer größeren Masse von Objekten, egal).
Du machst also ne einfache Methode, die sieht so aus:

List<Something> getSomethings(){...}

Schön, sieht erstmal richtig aus, sieht auch nicht so schwer aus, tut
wahrscheinlich was es soll, man freut sich. Werde da bei Gelegenheit
einen Test für schreiben, um sicher zu gehen, aber jetzt geht's erstmal
weiter mit dem Code, der das dann benutzt,vielleicht fällt dann ja was
auf.

Dann merke ich: Nah, also eine Liste zu liefern war irgendwie blöd, weil
ein Set da klarer wäre, obwohl: wenn ich die Duplikate sehen will, wäre
es sinnvoll eine Liste einem Set gegenüber zu stellen.. will ich
duplikate loggen oder nicht? Will ich vielleicht ne LinkedList haben,
und billig vorne was einfügen können? Weiß ich noch nicht so genau, aber
es erscheint mir ne gute Idee zu sein, wenn ich erstmal eine Collection
im Parameter mitgebe, dann kann ich mir später als Aufrufer aussuchen,
was für eine Collection ich befüllen will.
Also wird daraus:

<T extends Collection<? super Something>> T getSomethings(T target){...}

Schön, sieht richtig aus, mache ich dann einen Test für, wenn es mir
passt, erstmal weiter gehen..

Dann merke ich: Okay, hier benutze ich das Stück nochmal, aber das hier:

Set<Something> someThings = getSomethings(new HashSet<Something>());
Set<OtherThings> otherThings = new HashSet<>();
for(Something s : someThings){
otherThings.add(toOtherThing(s));
}

...ist irgendwie blöd jetzt, ich packe alles in meine Collection und
muss dann nochmal drüber iterieren, um es auf was anderes zu mappen,
besser ich gebe der Methode nicht nur Zielcollection mit, sondern
zusätzlich eine Mapping-Funkction dann muss ich nur einmal drüber
iterieren und der Aufrufer, der das schon benutzt, der übergibt einfach
ne Identity-Funktion und ist fein raus, oder ich mache ihm eine neue
Methode mit der alten Signatur, die einfach weiter delegiert und eine
Identity-Funktion als Parameter übergibt, damit wird der Aufrufer
lesbarer:
Also wird daraus:

<T extends Collection<? super Something>> T getSomethings(T target){
return getSomethings(target, Function.identity());
}
<U, T extends Collection<? super U>> T getSomethingsFunction<? super
Something, U> mapper, T target){...}

Und der Code oben wird zu:
Set<OtherThings> otherThings = getSomethings(myClass::toOtherThing, new
HashSet<OtherThings>());

Hätte ich diese Entwicklung "test first" geschrieben, also mit allen
Corner cases, hätte ich jeden der Tests für diese Methode (und das kann
ein ganzer Batzen sein) zweimal umschreiben müssen. So sehe bin ich bei
etwas angekommen, wo ich denke: okay, das sollte reichen.
Entweder schreibe ich jetzt einen Test, oder wenn mich noch das
"höhere" Ziel quält, wegen dem ich diese Methode überhaupt geschrieben
habe, kann es sein, dass ich das erst zuende machen muss, um den Faden
nicht zu verlieren.
Also mache ich mit meiner Idee erstmal weiter, bis ich zu dem Punkt
komme, wo ich denke dass es jetzt "soweit funktionieren sollte, dass ich
es testen kann", weil ich keine größeren Änderungen mehr erwarte und
dann schreibe ich Tests. Zunächst für das grobe Zeug, dann für das
feinere Zeug. Ich präzisiere dann meine Erwartungen immer genauer und
die Tests sagen mir, ob ich richtig liege, oder einen Hund drin habe.

Meine Entwicklung sieht in der Regel so aus:
1.Hack
2.Refine
do{
3.Write Test
4.Fix Bugs
}while (bugs detected || tests incomplete)

Üblicherweise beschränkt auf ein System von 1 bis 4 Klassen, bzw. auf
ein bis drei Milestones auf meiner Todo-Liste.

Ein Test zurrt mir die API einer Methode fest, macht Änderungen teurer,
schränkt mich ein. Derart Entscheidungen verschiebe ich gerne auf
später.
Ein Reihe Tests, die ich nach einem halben Tag wieder löschen kann, weil
ich merke, dass ich auf dem Weg eine bessere Lösung für ein Problem
gefunden habe ("oh der Workaround ist eigentlich ziemlich cool, da
brauche ich diese 5 Methoden eigentlich gar nicht mehr.."), ist
verschwendete Zeit, die verbringe ich lieber am Problem oder in der
Teeküche, um den Kopf wieder frei zu kriegen.
Post by Patrick Roemer
Natürlich lege ich auch erst mal explorativ los, und schreibe ganz
bestimmt nicht erst mal für jede Zeile "Produktivcode" einen Test, der
diese erzwingt. Aber ich will relativ bald ein Gefühl dafür bekommen,
wie sich so ein API anfühlt - und den explorativen Code dafür schreibe
ich doch lieber gleich als Testcases.
Letzteres ist bei mir high level. Beispiel: Ich brauche einen REST Web
Service, der mir irgendwelche Kunden zurück liefert. Der Test ist
einfach: HTTP connection aufmachen, Kundennummer rein geben, kommt der
Kunde zurück? Stimmen die Fehlermeldeungen? Das will ich ja nicht jedes
mal immer neu ausprobieren und mit dem Browser oder CURL rumlutschen, da
soll einmal Knöpfchen drücken genügen. Also Test schreiben, dann
implementieren. Da weiß ich, was ich will, im voraus.

Und dann hast du Aufgaben, wie: Ich suche alle Files in einem Directory,
extrahiere daraus irgendwelche Daten, cache die, ggf. muss ich per
Timestamps prüen, ob sich was in einem File geändert hat, damit ich den
neu einlese, etc. Da ergeben sich Fragen, wie: Wie halte ich die Daten,
wie die Relation der Daten zu den Files und den Timestamps, bzw. brauche
ich read-write-locks, was passiert, wenn ein File gelöscht wird, oder
whatever.. und da weiß man ggf. noch nicht ganz genau, was man will und
was das Sinnvollste ist. Mit anderen Worten wird erstmal explorativ
drauflos gehackt und geschaut, wie sich das verhält und ob man das so
nehmen kann, oder ob was anderes besser wäre, etc. und der einzige
"Test" bis dahin hat die Qualität einer hingerotzen Main-Methode, nur um
das Zeug auf dem höchsten Level auszulösen. Keine Corner cases, etc.
Und wenn ich dann ein Gefühl dafür habe, wohin die Reise geht, was ich
will und dass die Idee, die ich da ausformuliert habe gut ist, sodass
jetzt der Feinschliff kommen kann, dann fange ich an top down die Tests
zu schreiben, um a) die API zu fixieren, b) vergessene corner cases zu
entdecken und c) sicher zu gehen, dass ein späterer Bugfix kein anderes,
erwartetes Verhalten bricht.
Post by Patrick Roemer
Wenn das API sich zu stark ändert,
schmeisse ich die entsprechenden Tests lieber weg anstatt zu
refaktorieren und schreibe neue.
Ich fand heute diesen Talk hier relativ interessant (okay, ich bin
schuldig mir sowas auf nem Sonntag anzuschauen, weil es interessanter
ist das das TV-Programm). Zufällig drüber gestolpert. Ich war
überrascht, dass der Abschnitt von Minute 15 bis Minute 28 "Spike and
Stabilize" meiner Art Software zu entwickeln sehr nahe kommt:


Gruß,
-Wanja-
--
..Alesi's problem was that the back of the car was jumping up and down
dangerously - and I can assure you from having been teammate to
Jean Alesi and knowing what kind of cars that he can pull up with,
when Jean Alesi says that a car is dangerous - it is. [Jonathan Palmer]
Christian H. Kuhn
2016-07-17 23:01:49 UTC
Permalink
"Spike and Stabilize"
Kann man machen. Hat man jahrelang gemacht. Wenn man alleine arbeitet
oder in Teams mit sehr klar verteilten Aufgaben.

Ich würde nie behaupten, dass TDD irgendeiner anderen Methode überlegen
ist. Seit ich es in meinen Projekten mehr oder (meistens) weniger
konsequent anwende, hat sich die Qualität meines Codes erhoht, aber da
war von Anfang an viel Luft nach oben. Wer mehr Praxis hat, hat auch
weniger Optimierungspotential (allerdings nie keines), dafür mehr
eingefahrene Gewohnheiten, wird also für die Umstellung auf TDD mehr
Energie für weniger Ertrag aufwenden müssen.

Zwei Gründe für TDD konnte ich inzwischen bestätigen: Wenn ich erst die
Tests schreibe, mache ich mir vorher programmnähere und weniger
abstrakte Gedanken. Oft sind dann die später auftretenden Änderungen
soviel kleiner, dass trotz Änderung der Tests insgesamt weniger Arbeit
aufzuwenden war. Gefühlt, nicht gemessen. Und zweitens: In agilen
Methoden, wo im Prinzip jeder an allem arbeiten kann, egal ob pair
programming, Scrum oder sonstwas, führt TDD dazu, dass die Arbeit an
einer Stelle problemloser von jemand anderem übernommen werden kann.
Wenn der Test vor dem Code entsteht, übernimmt man immer grün, also
lauffähig, und muss nicht erst die Fehler zusammensuchen.

lg
QNo
Patrick Roemer
2016-07-19 08:58:13 UTC
Permalink
Post by Wanja Gayk
Post by Patrick Roemer
Post by Wanja Gayk
Manchmal programmiert man ohne klares Ziel, weil das noch nicht in Sicht
ist und man sich langsam voran tastet, bzw. baut man wärend der
Implementation sehr oft noch sehr viel um, refactored eine Menge und man
will dafür nicht immer die Tests komplett umbauen, weil sich die API
ändert (weil man auf halbem Wege merkt, dass sie doch nicht so elegant
war, wie man dachte und einem was besseres einfällt).
Aber wie merkt man das denn ohne Tests (oder sonstigen Code der mit dem
API arbeitet)? :)
Nicht jede Methode ist Teil einer öffentlichen API, sondern das meiste
Zeug ist private (bzw. package private) oder Teil einer groben Idee.
Package private ist ja schon wieder API für andere Klassen im selben
Package.
Post by Wanja Gayk
List<Something> getSomethings(){...}
[...]
Post by Wanja Gayk
<T extends Collection<? super Something>> T getSomethings(T target){...}
List<Something> getSomethings() {
return getSomethings(new ArrayList<>());
}

Inlinen, und alle bestehenden Aufrufer sind zufrieden.
Post by Wanja Gayk
<T extends Collection<? super Something>> T getSomethings(T target){
return getSomethings(target, Function.identity());
}
<U, T extends Collection<? super U>> T getSomethingsFunction<? super
Something, U> mapper, T target){...}
Und hier ändert sich dank des Overloads für bestehende Aufrufer eh nichts.
Post by Wanja Gayk
Set<OtherThings> otherThings = getSomethings(myClass::toOtherThing, new
HashSet<OtherThings>());
Hätte ich diese Entwicklung "test first" geschrieben, also mit allen
Corner cases, hätte ich jeden der Tests für diese Methode (und das kann
ein ganzer Batzen sein) zweimal umschreiben müssen.
Von test-first mit allen corner cases war ja eh nicht die Rede. Aber
wenn ich irgendwann Testcases hierfür geschrieben habe, dann, weil ich
mir aus irgendeinem Grund speziell über diese Methode Gedanken gemacht
habe. Ganz so trivial wird die dann nicht sein; dann schadet es nichts,
wenn es weiterhin Aufrufer gibt, die die neue Version durchexerzieren,
wenn auch nicht in allen möglichen Variationen. Tests dafür kann ich
hinzufügen, wenn und wann ich es für nötig halte, oder eben nicht. Das
Abschreckungspotential des "Umschreibens" halte ich in diesem konkreten
Beispiel jedenfalls für überschaubar.
Post by Wanja Gayk
1.Hack
2.Refine
do{
3.Write Test
4.Fix Bugs
}while (bugs detected || tests incomplete)
Ich finde, dass das Schreiben von Tests beim "Refine" ungemein hilfreich
sein kann.
Post by Wanja Gayk
Mit anderen Worten wird erstmal explorativ
drauflos gehackt und geschaut, wie sich das verhält und ob man das so
nehmen kann, oder ob was anderes besser wäre, etc. und der einzige
"Test" bis dahin hat die Qualität einer hingerotzen Main-Methode, nur um
das Zeug auf dem höchsten Level auszulösen. Keine Corner cases, etc.
Und wenn ich dann ein Gefühl dafür habe, wohin die Reise geht, was ich
will und dass die Idee, die ich da ausformuliert habe gut ist, sodass
jetzt der Feinschliff kommen kann, dann fange ich an top down die Tests
zu schreiben, um a) die API zu fixieren, b) vergessene corner cases zu
entdecken und c) sicher zu gehen, dass ein späterer Bugfix kein anderes,
erwartetes Verhalten bricht.
Das impliziert irgendwie, dass Corner Cases keinen Einfluss auf das
Design haben. Und wenn ich Code dafür schreibe, will ich den auch
irgendwie triggern.

Wenn ich mit einer main anfange, sammelt sich da peu a peu aller
möglicher auskommentierter und toter Code von Variationen dieses
High-Level-Szenarios an. Da spendiere ich lieber noch ein paar
Assertions und packe die Varianten in Testcases.
Post by Wanja Gayk
Ich war
überrascht, dass der Abschnitt von Minute 15 bis Minute 28 "Spike and
http://youtu.be/USc-yLHXNUg
Have you written lots and lots of TDD code? ;)

Was er im Prinzip sagt, ist doch: Wenn (und nur wenn!) man hinreichend
viel konsequent mit agilen Prozessen gearbeitet hat, weiss man ja,
worauf es ankommt. Dann kann man die Regeln ruhig Regeln sein lassen
(das wichtige davon hat man ja eh internalisiert), den Code hinballern
und hinterher aufräumen. Das mag für ihn und für Dich funktionieren,
aber das als Entwicklungsprozess zu verticken, finde ich schon mutig.
Und mir fehlt da eben der Aspekt des Test-Driven *Design*.

Viele Grüße,
Patrick
Wanja Gayk
2016-07-20 22:07:29 UTC
Permalink
Post by Patrick Roemer
Post by Wanja Gayk
Nicht jede Methode ist Teil einer öffentlichen API, sondern das meiste
Zeug ist private (bzw. package private) oder Teil einer groben Idee.
Package private ist ja schon wieder API für andere Klassen im selben
Package.
Prinzipiell schon, aber in der Regel hat man den Source gut genug unter
Kontrolle, um hier aufzupassen (ich schreib mir auch übölicherweise
einen Kommentar dran, der sagt: Packaga private nur für unit tests.
nicht benutzen!).
Wer es dennoch tut ist des Todes (bzw. selbst schuld). :-)
Post by Patrick Roemer
Post by Wanja Gayk
1.Hack
2.Refine
do{
3.Write Test
4.Fix Bugs
}while (bugs detected || tests incomplete)
Ich finde, dass das Schreiben von Tests beim "Refine" ungemein hilfreich
sein kann.
Kommt auf die Sitation an. Bei trivialen Methoden spare ich es mir. Sind
die Methoden zu kompliziert, gehören sie meist eh aufgespalten.
Post by Patrick Roemer
Post by Wanja Gayk
sodass
jetzt der Feinschliff kommen kann, dann fange ich an top down die Tests
zu schreiben, um a) die API zu fixieren, b) vergessene corner cases zu
entdecken und c) sicher zu gehen, dass ein späterer Bugfix kein anderes,
erwartetes Verhalten bricht.
Das impliziert irgendwie, dass Corner Cases keinen Einfluss auf das
Design haben.
Was den üblichsten Corner case angeht: Wie haben uns in meiner Gruppe
geeinigt, dass Methoden in der Regel davon ausgehen, dass Parameter
nicht null sind, falls sie null sein dürfen, muss es dokumentiert sein.
Bei vielen Methoden ist diese Eigenschaft aber eher "egal", weil es
keinen Sinn ergibt sie so zu benutzen und man sie auch nirgendwo
benutzt, sprich: ich kümmere mich manchmal zunächst nicht drum, sondern
erst, wenn ich den Test. Ich enscheide dann, ob ich Toleranz will, oder
einen strikten check (den ich dann einbaue) und das wird dann durch den
Test fixiert. Ich habe lediglich die Entscheidung über das Verhalten in
dem Corner Case verzögert, weil der Gedanke in der "Hack/Refine"-Phase
noch nicht wichtig war. Das gehört dann mehr zur Stabilisierung.
Manchmal lasse ich mir eine Möglichkeit länger offen, das hat auch
Vorteile. Ist natürlich Fallabhängig.
Post by Patrick Roemer
Und wenn ich Code dafür schreibe, will ich den auch
irgendwie triggern.
Oft genug schreibe ich gar keinen Code für Corner Cases. Das Verhalten
einer Methode für irgendein internes Zeug mit einem oder zwei Aufrufern
ergibt sich, bzgl. Corner Cases, eher automatisch.
Eine Methode, beispielsweise, die prüfen soll, ob zwei Maps bzgl. ihrer
Keys identisch sind, um den Unterschied zu loggen, wird bei mir nahezu
automatisch ne NPE werfen, wenn eine der Maps null ist. Da muss man
nicht vorher nen Test zu schreiben. Vor allem nicht, wenn der einzige
Aufrufer garantiert zwei Maps liefern muss, weil er sonst selbst nicht
funktionieren würde.
So einen Test kann man nachliefern, ohne sich ins Bein zu schießen, das
muss man nicht vorher tun.
Post by Patrick Roemer
Post by Wanja Gayk
Ich war
überrascht, dass der Abschnitt von Minute 15 bis Minute 28 "Spike and
http://youtu.be/USc-yLHXNUg
Have you written lots and lots of TDD code? ;)
Jap, genug jedenfalls, um mittlerweile bei Tests nicht mehr ganz so
streng zu sein. Ich denke mein Code sieht dennoch halbwegs passabel aus.
Das liegt aber schon an der Abneigung gegen lange Methoden.
Post by Patrick Roemer
Was er im Prinzip sagt, ist doch: Wenn (und nur wenn!) man hinreichend
viel konsequent mit agilen Prozessen gearbeitet hat, weiss man ja,
worauf es ankommt.
Da bin ich mit etwa 15 Jahren Java-Erfahrung, glaube ich, mittlerweile
relativ firm.

Ich freue mich allerdings nach den Projekten, in denen ich schon war,
mittlerweile mehr darüber, wenn es überhaupt Tests gibt - und erst
recht, wenn die automatisch laufen und der CI-Server böse Mails
verschickt. Ich habe da Kollegen gehabt, das geht auf keine Kuhhaut
mehr, die handelten so nach der Devise: Test gebrochen - Test gelöscht.
Post by Patrick Roemer
Dann kann man die Regeln ruhig Regeln sein lassen
(das wichtige davon hat man ja eh internalisiert), den Code hinballern
und hinterher aufräumen. Das mag für ihn und für Dich funktionieren,
aber das als Entwicklungsprozess zu verticken, finde ich schon mutig.
Finde ich nicht. Ich halte es für einen pragmatischen Ansatz. Die
Disziplin, sich nicht um die Tests zu drücken ist das einzig schwere
dabei. Und schlecht zu testender Code wird einem spätestens dann so
unangenehm auffallen, dass man sich beim nächsten Mal etwas mehr
anstrengt. :-)
Post by Patrick Roemer
Und mir fehlt da eben der Aspekt des Test-Driven *Design*.
Big Design upfront war noch nie mein Ideal. Klar braucht man ein Ziel,
ein grobes Konzept (kommt auch auf die Domäne an, bei einer UI will ich
gerne vorab ein möglichst präzises Design) und etwas Vorausschau, damit
man sich nicht auf dem Weg einmauert, das meiste, in der Regel internes
Zeug, ist Freistil. Und da halte ich es eher mit kleinen Iterationen.

Der Unterschied ist wohl: Test Driven Design setzt darauf, dass man sich
vorher mehr Gedanken macht, Spike and Stabilize darauf, dass man sich
seine Optionen länger offen hält. Beides kann durchaus erfolgreich sein.
Ich lebe mit letzterem einfach besser. Meinem Spaß finde ich vor allem
darin etwas zum Funktionieren zu bringen. Erst die Kür, dann die
Pflicht: Erst bring ich es zum laufen, dann mache ich es schön. Manche
brauchen es andersrum. :-)

Gruß,
-Wanja-
--
..Alesi's problem was that the back of the car was jumping up and down
dangerously - and I can assure you from having been teammate to
Jean Alesi and knowing what kind of cars that he can pull up with,
when Jean Alesi says that a car is dangerous - it is. [Jonathan Palmer]
Christian H. Kuhn
2016-07-19 12:09:46 UTC
Permalink
Post by Wanja Gayk
Nicht jede Methode ist Teil einer öffentlichen API, sondern das meiste
Zeug ist private (bzw. package private) oder Teil einer groben Idee.
Ach ja, noch einen zu 100% code coverage: Wenn privater Code bei den
Testdurchläufen nicht getestet wird, sollte man sich dringend anschauen,
ob der überhaupt je benutzt wird. Es gibt Fälle, in denen die Antwort
„ja“ lautet (bestimmte Exceptions, synchronized), weil die Bedingungen
einfach nicht gescheit von außen herzustellen sind. Die meisten Fälle
sind die anderen :-)

lg
QNo
Patrick Roemer
2016-07-07 22:28:32 UTC
Permalink
Post by Christian H. Kuhn
Bei QChessClock habe ich nicht den Schimmer einer Vorstellung, wie ich
die Klasse testen soll. Es gibt keine Getter-Funktionen, mit denen sich
was überprüfen ließe. Ich kann also testen, dass das Testobjekt nach
Konstruktor ungleich null ist, sonst nichts. Eine Änderung der
Schnittstelle nur zu Testzwecken kommt selbstverständlich nicht in
Frage. Bleibt eigentlich nur, dass die Testklasse QChessClockObserver
implementiert und auf die Benachrichtigungen des Observable wartet.
Variante: Der Test *erzeugt* ein Exemplar des Observer-Interface, das
entweder Assertions beinhaltet, oder eingehende Events speichert und
Methoden bietet, über die der Test diese später abfragen kann, um
Assertions darauf loszulassen.
Post by Christian H. Kuhn
Der
Weg ist gangbar, ich habe ihn aber noch in keinem Buch gefunden.
Genau dieser Ansatz sollte eigentlich in jedem TDD-Buch abgehandelt
werden. Die Nomenklatur ist allerdings herzlich inkonsistent: Das läuft
unter "Mock", "Shunt", "Stub", "Fake",... Bei Beck[1] ist das etwa ein
"(Self) Shunt" (und verwendet genau Dein Beispiel: Ein Test, der selber
ein Listener-Interface implementiert), während ein "Mock" eher ein nicht
verifizierender Drop-In für eine komplexe Ressource ist. Bei Meszaros[2]
heisst es hingegen "Test Spy" bzw. eben "Mock" (und Becks "Mock" wäre
ein "Fake Object").
Post by Christian H. Kuhn
In noch extremerem Umfang gilt das für die Java-GUI. Die GUI erzeugt ein
QChessClock-Objekt und registriert sich dort als Observer. Sie stellt
die erwähnten Knöpfe und Menüs bereit, die auf die öffentlichen Methoden
der Schachuhr zugreifen. Öffentliche Methoden sind außer dem Konstruktor
und actionPerformed() noch die verschiedenen update()-Funktionen, über
die das Observable seine Zustandsänderungen mitteilt. Auch hier habe ich
praktisch nichts, was ich mit JUnit testen könnte, und andere Tests der
Klasse kenne ich nicht.
Ich kann kaum glauben, dass eine Websuche mit "swing junit" nichts
zutage fördert...

GUI-Testing ist mühsam und IMHO recht spaßfrei. Es empfiehlt sich
deshalb (und nicht nur deshalb), möglichst viel Präsentationslogik in
eigenständig testbaren Klassen zu halten, die nicht nur innerhalb der
GUI-Eventloop lauffähig sind. Ein solcher Ansatz wäre z.B. Presentation
Model[3].

Um die korrekte Verkabelung des Modells mit den Widgets zu testen, muss
man entsprechende APIs des GUI-Frameworks verwenden, mit denen man
Widgets auffinden, ihre Zustände abfragen und Events auslösen kann.
Swing bietet sowas, und darauf basierend gibt es diverse auf JUnit
aufsetzende GUI-Test-Frameworks.

Bis zu einem gewissen Grad kann man sicher auch mit diesen Mitteln noch
testen, ob die GUI halbwegs so aussieht, wie erwartet (Positionierung,
Pixelfarben,...). So extrem bin ich aber selber noch nie geworden. :)

Viele Grüße,
Patrick

[1] Kent Beck, "Test-Driven Development by Example"
[2] Gerard Meszaros, "xUnit Test Patterns"
[3] http://www.jgoodies.com/download/presentations/patterns-and-binding.pdf
Christian H. Kuhn
2016-07-08 12:13:15 UTC
Permalink
Post by Patrick Roemer
Genau dieser Ansatz sollte eigentlich in jedem TDD-Buch abgehandelt
werden. Die Nomenklatur ist allerdings herzlich inkonsistent: Das läuft
unter "Mock", "Shunt", "Stub", "Fake",...
Ah. Bei mir ist Mocking in der anderen Richtung angekommen: Wenn ich in
meinem Beispiel die Schachuhr vor den Einzeluhren entwickelt hätte,
hätte ich für die Uhren Mocking-Objekte benutzt, die auf definierte
Aktionen definierte Ergebnisse liefern. Solange, bis ich dann eine
getestete Klasse für die echte Uhr habe. Die Transferleistung, dass das
Mocking-Objekt auch auf höherer Stufe in der Klassenhierarchie als die
zu testende Klasse ansiedeln kann, war dann zu schwer für mich.
Post by Patrick Roemer
Ich kann kaum glauben, dass eine Websuche mit "swing junit" nichts
zutage fördert...
[1] Kent Beck, "Test-Driven Development by Example"
[2] Gerard Meszaros, "xUnit Test Patterns"
[3] http://www.jgoodies.com/download/presentations/patterns-and-binding.pdf
Ich melde mich wieder, wenn ich fertig mit Lesen bin :-)

Vielen Dank!

mfg
QNo
Christian H. Kuhn
2016-07-08 14:05:46 UTC
Permalink
Zwischenspiel.
Post by Christian H. Kuhn
In der Folge ist entsprechend die Testabdeckung, die von JaCoCo
ermittelt wird, entsprechend niedrig.
JaCoCo war das Mittel der Wahl, weil es von EclEmma benutzt wird und
somit in Eclipse zur Verfügung steht. Es scheint nicht das beste Mittel
zu sein:

public class Rumspiel {

private enum Possibilities {
A, B;
}

private Possibilities poss;

public Rumspiel() {
poss = Possibilities.A;
}

public void setPoss(final boolean _poss) {
if (_poss) {
poss = Possibilities.A;
} else {
poss = Possibilities.B;
}
}

public void aMethod() {

switch (poss) {
case A:
System.out.println("Fall A");
break;
case B:
System.out.println("Fall B");
break;
}
}
}

Ein default in der Switch-Anweisung wäre unerreichbarer Code; andere
Möglichkeiten als A und B können nicht vorkommen. Insbesondere kann poss
nicht null sein, das wird durch Konstruktor und setPoss verhindert. Ohne
Default behauptet JaCoCo aber, dass nur 2 von 3 Branches abgedeckt sind.

Lösbar ist das Problem, dass einer der cases zum default deklariert
wird. Das vermindert aber zumindest die Lesbarkeit des Codes; und ich
behaupte einfach mal, dass auch die Wartbarkeit leidet, weil ich bei
einer möglichen späteren Erweiterung des enum, das vielleicht inzwischen
in ein anderes Package ausgelagert wurde, nicht mehr sofort erkennen
kann, dass default eben nicht default, sondern ein ganz konkreter case ist.

Google kennt das Problem, aber nicht die Lösung. Sourcen ändern, um bei
gleicher Funktionalität Tests zu erfüllen, ist wohl kein guter Gedanke.
Den Branch-Schwellenwert, der für einen grünen Build erreicht werden
muss, abzusenken kann auch keine Lösung sein, denn der erträgliche Wert
hängt zu sehr von der Anzahl der switch- zu sonstigen Verzweigungen ab.

Bleibt ein anderes Tool. Eines, das mit Maven, Gradle und Jenkins
zusammenarbeitet. Ich bin auf Serenity, Cobertura und das proprietäre
Clover gestoßen, JCov scheint sich mit CI-Tools nicht zu vertragen. Was
benutzt ihr? Und mit welchen Erfahrungen?

TIA
QNo
Wanja Gayk
2016-07-08 20:35:43 UTC
Permalink
Post by Christian H. Kuhn
Bleibt ein anderes Tool. Eines, das mit Maven, Gradle und Jenkins
zusammenarbeitet. Ich bin auf Serenity, Cobertura und das proprietäre
Clover gestoßen, JCov scheint sich mit CI-Tools nicht zu vertragen. Was
benutzt ihr? Und mit welchen Erfahrungen?
EclEmma, bzw. JaCoCo für den Jenkins. Nicht perfekt, aber gut genug,
ergänzt durch Kommentare, oder asserts als "zur Laufzeit testbare
Kommentare" an der einen oder anderen Stelle, die manchen Fehlgriff des
Tools erklären. Dazu noch ziemlich strikte Compiler-/Error-Einstellungen
in Eclipse, wo auch Checkstyle und FindBugs drüber gehen. Nach dem Build
auf dem CI-Server schaut auch nochmal Coverity über das Zeug, das findet
auch noch das Eine oder Andere.
Als Sahnehäubchen haben wir auch ein Peer-Review-Tool für größere, bzw.
kritische (weil zentrale) Änderungen.
Und wenn das Programm durch al diese (semi-)Automatismen durch ist,
dürfen sich QA-Mitabeiter und Performance-Tester dran versuchen, die
haben ihre eigenen automatischen Suites für die Nightly Builds und
versuchen sich explorativ an ausgewählten Builds.
Das klingt alles etwas paranoid, aber es hat uns schon den einen oder
anderen Tag gerettet, wenn nicht gar vor richtig dickem Ärger bewahrt.

Gruß,
-Wanja-
--
..Alesi's problem was that the back of the car was jumping up and down
dangerously - and I can assure you from having been teammate to
Jean Alesi and knowing what kind of cars that he can pull up with,
when Jean Alesi says that a car is dangerous - it is. [Jonathan Palmer]
Patrick Roemer
2016-07-08 22:01:00 UTC
Permalink
Post by Christian H. Kuhn
switch (poss) {
System.out.println("Fall A");
break;
System.out.println("Fall B");
break;
}
[...]
Post by Christian H. Kuhn
Ein default in der Switch-Anweisung wäre unerreichbarer Code; andere
Möglichkeiten als A und B können nicht vorkommen.
Trotzdem wäre ein zusätzlicher default-Branch legal und damit eben ein
weiterer Codepfad.
Post by Christian H. Kuhn
Insbesondere kann poss
nicht null sein, das wird durch Konstruktor und setPoss verhindert.
null ist nicht das Problem - auch, weil das eine NPE bei Auswertung des
"poss" triggern müsste, bevor es überhaupt in die Verzweigung ginge.
Post by Christian H. Kuhn
Ohne
Default behauptet JaCoCo aber, dass nur 2 von 3 Branches abgedeckt sind.
Im Bytecode stehen auch drei Branches - javac generiert einen
synthetischen default-Branch. Und der lässt sich von einem expliziten
no-op Default-Branch im Sourcecode nicht unterscheiden. (Und ggfs.
setzen andere Compiler dieses Konstrukt anders um.) Es ist also auch
etwas schwierig für JaCoCo, das "richtig" zu interpretieren.

Wahrscheinlich gibt es andere Coverage-Tools, die entsprechende
Heuristiken eingebaut haben ("bei einem switch über das #ordinal() eines
Enum, ignoriere den default branch, falls alle Werte des Enum bereits
abgedeckt sind und Saturn gerade im dritten Haus steht"), aber die
stolpern dann wahrscheinlich über die Bytecode-Umsetzung von
String-Switches oder etwas ganz anderes. Ein Tool, das unter allen
Umständen weder false positives noch false negatives liefert, wird man
wohl kaum finden - spätestens, wenn Du sowas erwartest, wie bei dem
null-Argument oben ("Es gibt aber doch gar keinen Codepfad, auf dem ein
mutable field in Zustand X versetzt werden kann!").
Post by Christian H. Kuhn
Sourcen ändern, um bei
gleicher Funktionalität Tests zu erfüllen, ist wohl kein guter Gedanke.
Unter Umständen schon, das nennt sich Refactoring und Test-Driven-Design. :)

Hier geht es aber nur darum, ein automatisiertes Tool glücklich zu
machen, das den Codefluss nicht versteht. Und Code schlechter zu machen,
damit das Tool ihn für gut hält, ist wirklich nicht so prima. (Das gilt
genauso für PMD, FindBugs, etc., nur, dass in deren Domäne Ausnahmen
besser konfigurierbar sind.)
Post by Christian H. Kuhn
Den Branch-Schwellenwert, der für einen grünen Build erreicht werden
muss, abzusenken kann auch keine Lösung sein, denn der erträgliche Wert
hängt zu sehr von der Anzahl der switch- zu sonstigen Verzweigungen ab.
Was soll denn ein solcher Schwellenwert bringen? Bei 80% Abdeckung ist
mein Code ok, bei 79.9% Mist? Boiling Frogs...

IMHO bringt Coverage-Reporting nur was, wenn man sich die Ergebnisse
wiederholt im Laufe der Zeit ansieht, um Trends zu erkennen, und jeweils
in die konkret auffälligen Stellen reindrillt, um Kategorien von
Schwachstellen zu identifizieren.
Post by Christian H. Kuhn
Bleibt ein anderes Tool.
Oder die Erkenntnis, dass die Dinger einfach nicht perfekt sein können.
Ich fand Emma/JaCoCo bisher durchaus brauchbar. Kann gut sein, dass
andere Tools relativ dazu besser sind (entsprechendes Feedback dazu hier
würde ich wohl zum Anlass nehmen, mir die auch mal anzuschauen), aber
Wunderdinge würde ich von keinem erwarten.

Viele Grüße,
Patrick
Christian H. Kuhn
2016-07-10 22:40:08 UTC
Permalink
Post by Patrick Roemer
Trotzdem wäre ein zusätzlicher default-Branch legal und damit eben ein
weiterer Codepfad.
Nachdem ich darüber meditiert habe, habe ich im gegebenen Fall eine der
legalen Varianten nicht explizit, sondern als default implementiert. Mit
Bauchweh; der Tag mag kommen, an dem ich mich für diese Entscheidung
hassen werde. Ich hab es aber im Quellcode sauber dokumentiert :-)
Post by Patrick Roemer
Post by Christian H. Kuhn
Sourcen ändern, um bei
gleicher Funktionalität Tests zu erfüllen, ist wohl kein guter Gedanke.
Unter Umständen schon, das nennt sich Refactoring und Test-Driven-Design. :)
Hier geht es aber nur darum, ein automatisiertes Tool glücklich zu
machen, das den Codefluss nicht versteht.
Ich habe mich da etwas ... suboptimal ausgedrückt. Zum Glück hast du
mich richtig verstanden :-)
Post by Patrick Roemer
Post by Christian H. Kuhn
Den Branch-Schwellenwert, der für einen grünen Build erreicht werden
muss, abzusenken kann auch keine Lösung sein, denn der erträgliche Wert
hängt zu sehr von der Anzahl der switch- zu sonstigen Verzweigungen ab.
Was soll denn ein solcher Schwellenwert bringen? Bei 80% Abdeckung ist
mein Code ok, bei 79.9% Mist? Boiling Frogs...
100% Abdeckung classes, lines und methods macht schon Sinn. Wenn es
weniger wird, hat man wohl irgendwo irgendwas übersehen. Branches und
Complexity hingegen ... ich neige dazu, die ersten drei bei Jenkins für
den Status auszuwerten, die letzten beiden dagegen nur für Gesundheit.

Ich bin auch im Moment noch in dem Stadium, in dem mir eine
nicht-100%-Abdeckung von was auch immer ein paar interessante Einblicke
in die Funktionsweise von Java vermittelt. Kann durchaus sein, dass
Testabdeckung in nem Jahr nicht mehr die Bedeutung für mich hat, die sie
heute hat.
Post by Patrick Roemer
Post by Christian H. Kuhn
Bleibt ein anderes Tool.
Oder die Erkenntnis, dass die Dinger einfach nicht perfekt sein können.
Um das herauszufinden, spiele ich gerade mit den Dingern. Für die
statische Analyse hatte ich bislang nur Checkstyle, inzwischen habe ich
mir auch PDM und FindBugs angeschaut. PDM habe ich fast komplett in den
Build integriert, mit wenigen Anpassungen im ruleset.xml. Wenn mal was
ist (die von Eclipse generierten equals(Object)-Methoden überschreiten
fast immer den Schwellenwert irgendeines Komplexizitätsmaßes), schreibe
ich eine Annotation in den Quellcode, und wenn mal jemand (der Jemand
kann ich auch selbst mit ein paar Monaten Abstand sein) den Code sieht,
weiß er, dass das was war, und kann dann entscheiden, ob meine
Entscheidungen für ihn vernünftig sind. FindBugs hingegen ... Die
Warnungen, die da nach Compiler, Checkstyle und PDM übrigbleiben, haben
zuviele false positives, als dass ich das im automatischen Build haben
möchte. Zur händischen Kontrolle ist das aber auch ganz nützlich.

Ansonsten lese ich. Beck liest sich gut, ich hab aber wenig Zeit.
Mezsaros wird noch ein wenig länger dauern, ich habe aber den Eindruck,
dass der mir auch als Nachschlagewerk gute Dienste leisten könnte.
Representation Model ... dass ich bislang mit Swing kein „sauberes“ MVC
gemacht habe, war mir klar, es ist im View doch manches vom Controller
enthalten. Damit bin ich immerhin schon über Autonomous View
hinausgewesen :-) Die Folien machen Sinn, ich bemühe mich gerade um
weitere Literatur. Martin Fowler hat da unter dem für mich nicht
intuitiven Stichwort EAA dev einiges auf seiner Webseite.

mfg
QNo
Christian H. Kuhn
2016-07-15 21:25:21 UTC
Permalink
Post by Christian H. Kuhn
100% Abdeckung classes, lines und methods macht schon Sinn.
Hat sich dann auch erledigt. Lines und Instructions schaffen die 100%
nicht mehr, seit SystemUtilities.invokeAndWait() Exceptions werfen kann.
Die müssen mit catch abgefangen werden, und die Bereiche kann ich nicht
gezielt mit vernünftigem Aufwand erreichen. Also muss ich nicht nur bei
Branches und Complexity, sondern auch bei Lines und Instructions
Abstriche machen.

Aber bei Classes und Methods bleib ich eisern bei 100% ;-)

lg
QNo
Christian H. Kuhn
2016-07-14 22:12:14 UTC
Permalink
Post by Christian H. Kuhn
Cobertura
Eclipse-Plugin lässt sich nur installieren, wenn ich andere Plugins auf
historische Versionen downgrade. Gradle Plugin nach allen öffentlich
verfügbaren Quellen konfiguriert. Ergebnis: JUnit-Tests werfen
Exceptions. Gelöscht.

lg
QNo
Wanja Gayk
2016-07-08 19:57:31 UTC
Permalink
Post by Christian H. Kuhn
Hallo Gemeinde,
Bevor hier der Löschantrag wegen Inaktivität kommt, mach ich doch lieber
nochmal Traffic :-)
Bei QChessClock habe ich nicht den Schimmer einer Vorstellung, wie ich
die Klasse testen soll. Es gibt keine Getter-Funktionen, mit denen sich
was überprüfen ließe. Ich kann also testen, dass das Testobjekt nach
Konstruktor ungleich null ist, sonst nichts. Eine Änderung der
Schnittstelle nur zu Testzwecken kommt selbstverständlich nicht in
Frage.
Ich bin bei sowas ganz pragmatisch.
Gibt öffentliche Methoden, die ich testen kann, dann mache ich das
bevorzugt.
Bietet sich eine protected Methode zum Test an, hae ich die Möglichkeit
von der zu testenden Klasse eine Klasse abzuleiten und habe so Zugriff
auf die Methode der Ableitung - die ruft ledglich super auf, also ändere
ich das Verhalten wenig bis gar nicht, bzw. überschaubar genug, um eine
informierte Entscheidung zu treffen, inwiefern der Test was taugt.

Bei private Methoden habe ich wenig Schmerzen damit die Sichtbarkeit von
"private" auf "default" zu ändern, allerdings mit einem Javadoc-
Kommentar, dass die Sichtbarkeit nur wegen JUnit auf "default" gesetzt
wurde. Da Test und Testobjekt in einem package leben, habe ich Zugriff
auf die Methode und kann fröhlich testen. Ich erlaube mir as, weil der
bliche Code, den man so schreibt, ohnehin kein Frameworks für die
Öffentlichkeit ist, bzw. nicht den Charakter einer Bibliothek für die
breite Masse hat. Kurz: Man hat den Source der Klasse und den, der sie
verwendet, in der Regel gut genug unter Kontrolle, um sich damit kein
eigenes Bein zu stellen.

Ich benutze übrigens EclEmma (Emma- Plugin für Eclipse), um mir dabei zu
helfen heraus zu finden, ob ich gewisse Fälle vergessen habe.
Post by Christian H. Kuhn
Bleibt eigentlich nur, dass die Testklasse QChessClockObserver
implementiert und auf die Benachrichtigungen des Observable wartet. Der
Weg ist gangbar, ich habe ihn aber noch in keinem Buch gefunden. Daher
vermute ich, dass es da eleganteres gibt?
Ich finde nichts Falsches daran eine Klasse so zu testen, dass du eine
Test-Listener an die Klasse hängst, und die Events abfragst, die da raus
kommen. Schließlich sind die geworfenen Events Teil der API, bzw. eines
Methoden-Contracts und gehören deswegen getestet. Wenn du zusicherst in
einer bestimmten Situation ein Event zu werfen, sollte dein Test auch
prüfen, ob das geschieht, sofern das für das Programm wichtig ist.
Post by Christian H. Kuhn
In noch extremerem Umfang gilt das für die Java-GUI.
[..]
Post by Christian H. Kuhn
Auch für den Integrationstest des gesamten
Systems scheint JUnit das falsche Mittel zu sein.
JUnit halte ich für ein Mittel, welches gut genug dafür ist einzelne
Komponenten zu testen, auch wenn es UI-Kompoinenten sind. Hierbei musst
du aufpassen, dass du alles im EDT ausführst, Konstruktion der Panels,
setzen und abfragen der Listener, erzeugen, abfragen und setzen von
Werten, das Finden von Buttons und Dropdowns in der Komponenten-
Hierarchie. etc.. und du solltest einen Timeout für alle Aktionen haben,
der zwar nicht zu knapp bemessen ist (bei mir sind es etwa 10 Sekunden),
aber du willst, dass einzelne Tests fehlschlagen können, ohne dass die
dentest auf Ewig raus zögern, weil du einen Test verkackt hast und der
z.B. ewig auf ein Ergebnis wartet.
Solche UI-Tests kannst du auch auf einem Jenkins-Server laufen lassen,
wenn du ein virtuelles Display hast (Linux, Display-Variable), auf dem
die Komponenten gerendert werden können. Problem hierbei ist: Du musst
sicherstellen, dass Tests niemals gleichzeitig ausgeführt werden und
dass Frames vollständig geschlossen sind, bevor ein neuer Test gestartet
wird. Mitunter ist das etwas tricky, aber machbar. Leider ist es auch
langsam, aber mit dieser Methode habe ich die Test-Coverage von UI-Code
in unserer Codebase dramatisch erhöht - und natürlich sehe ich im Emma-
Plugin, wo er durch gerannt ist.
Für den Integrationstest einer komplette Applikation halte ich es nict
für das richtige Mittel, aber einzelne Komponenten (also ne Suchmaske
mit ein paar Knöpfen, etc), kann man so recht ordentlich testen und
deckt auch den einen oder anderen, überraschenden Fehler auf.
Es hilft aber ein Flag zu haben, um diese UI-tests zu überspringen, weil
du in der Zeit, in der die auf deinem Entwicklungsrechner laufen,
praktisch nicht arbeiten kannst, weil du mausklicks simulierst -
abgesehen davon, dass sie langsam sind. Du willst alle normalen Tests
oft und schnell durchführen und die lahmen UI-Tests nur an und zu, vor
der Kaffeepause oder vor dem Commit laufen lassen, bzw. regelmäßig auf
dem CI-Server.

Gruß,
-Wanja-
--
..Alesi's problem was that the back of the car was jumping up and down
dangerously - and I can assure you from having been teammate to
Jean Alesi and knowing what kind of cars that he can pull up with,
when Jean Alesi says that a car is dangerous - it is. [Jonathan Palmer]
Stefan Ram
2016-07-09 19:27:59 UTC
Permalink
Post by Christian H. Kuhn
Das Coden war jetzt tatsächlich das kleinere Problem, und Gradle ist
zwar anders als Maven, aber gut bedienbar. Probleme tauchen beim Testen
auf. Ich will ja TDD anwenden. Also erst die Tests und dann den Code
schreiben. Hat mit JUnit4 bei QChessTimer auch ganz gut funktioniert.
Bei QChessClock habe ich nicht den Schimmer einer Vorstellung, wie ich
die Klasse testen soll.
Du hast QChessClock anscheinend schon geschrieben. Aber oben
schreibst Du, Du wolltest »erst die Tests und dann den Code
schreiben«. Aber das geht doch nicht, wenn Du QChessClock schon
geschrieben hast! Deswegen mußt Du nun QChessClock erst einmal
wieder löschen, dann vergessen, dann die Tests schreiben, und
dann QChessClock.

Du schreibst Du wolltest »erst die Tests und dann den Code
schreiben«. Aber Tests sind doch auch Code!

Die beiden GUI-Pakete sind praktisch Klienten jener Klassen.
Schreibe Deinen Test als einen weiteren Klienten wie die
GUI-Pakete.

Model <--- Swing-UI
<--- Android-UI
<--- Test

Halte die Swing-UI und die Android-UI Code klein und
einfach, ohne jede eigene Intelligenz. So, daß sie praktisch
keine Fehler enthalten können. Dann brauchst Du nur das
Modell zu testen.

Eventuell ist ein Anwendungsmodell nötig, wenn mehr
UI-spezifische Logik nötig ist.

Model <--- Swing-Anwendungsmodell <--- Swing-UI
<--- Test
<--- Android-Anwendungsmodell <--- Android-UI
<--- Test
<--- Test

Dann kannst Du die Anwendungsmodelle auch testen.

Um das Programm mitsamt der UI zu testen, gibt es bei
Android unter anderem »UI/Application Exerciser Monkey«
und bei Swing »java.awt.Robot«.
Christian H. Kuhn
2016-07-10 23:13:10 UTC
Permalink
Post by Stefan Ram
Du hast QChessClock anscheinend schon geschrieben. Aber oben
schreibst Du, Du wolltest »erst die Tests und dann den Code
schreiben«. Aber das geht doch nicht, wenn Du QChessClock schon
geschrieben hast! Deswegen mußt Du nun QChessClock erst einmal
wieder löschen, dann vergessen, dann die Tests schreiben, und
dann QChessClock.
Selbst Kent Beck schreibt, dass er gelegentlich von der reinen Lehre
abweicht, dann aber die Tests baldmöglichst nachliefert. Ich habe mit
einer gewissen Zeit gerechnet, die ich brauche, um mein Wissen hier mit
eurer Hilfe zu erweitern. Die Zwischenzeit habe ich genutzt, um ... wie
war noch mal die Ausrede ... ach richtig: explorativen Code zu schreiben ;-)
Post by Stefan Ram
Model <--- Swing-UI
<--- Android-UI
<--- Test
Ja, das hat Patrick so ähnlich vorgeschlagen. Bei ihm war es noch ein
Mock-Objekt, das in der Testklasse implementiert, QChessClockObserver
implementiert und Methoden bereitstellt, mit denen über die
verschiedenen update()-Funktionen Zustände des Observables ausgelesen
werden können. Wenn man den Weg konsequent durchdenkt, ist es
tatsächlich so, dass die Testklasse selbst zum Observer wird, und damit
formal zu einer weiteren UI.
Post by Stefan Ram
Eventuell ist ein Anwendungsmodell nötig, wenn mehr
UI-spezifische Logik nötig ist.
Model <--- Swing-Anwendungsmodell <--- Swing-UI
<--- Test
<--- Android-Anwendungsmodell <--- Android-UI
<--- Test
<--- Test
Dann kannst Du die Anwendungsmodelle auch testen.
Da lese ich mich gerade ein, Stichworte Model View Presenter und
Presentation Model. Ich habe auch die JAVA-GUI mal „explorativ“
implementiert. Und NATÜRLICH funktioniert die. Es ist ja bislang, in der
Light-Funktion, auch nur wenig Funktionalität erforderlich, und die
Darstellung kann ich eigentlich nur überprüfen, indem ich einen Blick
auf die laufende GUI werfe. actionPerformed() ruft auf jeden Click und
jedes MenüItem unmittelbar die entsprechende Funktion des Models auf.
Die verschiedenen update() setzen verschiedene Eigenschaften der GUI
(Zeitanzeige, Farben, setEnabled()). JaCoCo spricht von 167 Zeilen Code,
davon ist ein Drittel Logik (und davon wieder ein Drittel die
switch-Anweisung in actionPerformed()) und zwei Drittel Präsentation.
Das geht theoretisch auch ohne Test. Aber mir geht es ja nicht in erster
Linie um funktionierende Software, sondern ums Lernen.
Post by Stefan Ram
Um das Programm mitsamt der UI zu testen, gibt es bei
Android unter anderem »UI/Application Exerciser Monkey«
und bei Swing »java.awt.Robot«.
Eine andere Möglichkeit habe ich auf
http://www.javaworld.com/article/2073056/swing-gui-programming/automate-gui-tests-for-swing-applications.html
gefunden. Das muss ich mir alles nochmal genau anschauen ...

mfg
QNo
Lesen Sie weiter auf narkive:
Loading...