Path: csiph.com!aioe.org!.POSTED!not-for-mail From: Wanja Gayk Newsgroups: de.comp.lang.java Subject: Re: Unit-Tests von Einheiten ohne öffentliche Leseschnittstelle Date: Mon, 18 Jul 2016 00:33:33 +0200 Organization: Aioe.org NNTP Server Lines: 164 Message-ID: References: <1467923825.579226@alpaka.in-berlin.de> NNTP-Posting-Host: jNeIdM6YciQhZN9ktthCYA.user.gioia.aioe.org Mime-Version: 1.0 Content-Type: text/plain; charset="iso-8859-15" Content-Transfer-Encoding: 8bit X-Complaints-To: abuse@aioe.org User-Agent: MicroPlanet-Gravity/3.0.4 X-Notice: Filtered by postfilter v. 0.8.2 Xref: csiph.com de.comp.lang.java:13013 In article , Patrick Roemer (sangamon@netcologne.de) says... > > Responding to 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 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 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 someThings = getSomethings(new HashSet()); Set 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 getSomethings(T target){ return getSomethings(target, Function.identity()); } > T getSomethingsFunction mapper, T target){...} Und der Code oben wird zu: Set otherThings = getSomethings(myClass::toOtherThing, new HashSet()); 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. > 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. > 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: https://www.youtube.com/watch?v=USc-yLHXNUg 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]