# Für was schreibt man Unit-Tests?



## Marco13 (4. Sep 2012)

Hi

Der Titel ist bewußt zweideutig  Es geht natürlich nicht um den _Zweck_ von Unit-Tests, sondern um die Frage, für _welche_ Klassen und Methoden und Konstellationen man _wie_ Unit-Tests erstellt. 

Die TDD-Verfechter werden jetzt sagen: Man schreibt Tests für alles, und das noch bevor man es entwickelt hat. Andere werden sagen: Man schreibt Tests für alles was wichtig ist  . Ich frage mich, wie man in der Praxis wirklich nützliche und sinnvolle systematische Unit-Tests erstellen kann. Bei Websuchen findet man immer diese Trivialbeispiele, die mit der Realität nichts zu tun haben (ich kann das Money-Beispiel nicht mehr sehen :noe: ). 

Mal angenommen man wollte für so etwas vermeintlich triviales (!) wie das Java Collections Framework Unit-Tests schreiben. Eine handvoll recht überschaubarer Interfaces. Dann überlegt man und sucht vielleicht einen Ansatz, und schaut mal nach, ob es sowas nicht vielleicht schon gibt (ist ja nicht so abwegig, oder?) und findet dann z.B. in Guava eine abstrakte Basisklasse für den Test eher 'Map': MapInterfaceTest.java - google-collections - Google Collections Library - Google Project Hosting - ~650 (!) Zeilen stupider, repetitiver Trivialcode :noe: (Nur zum Vergleich: Die _Implementierung_ in Form von HashMap hat (einschließlich ihrer abstrakten Basisklasse "AbstractMap"!) nur ca. 400 Zeilen...) 

Damit das nicht in der ersten Antwort steht: Mir sind die Vorteile bewußt. Wenn man vorhat, mehrere verschiedene Map-Implementierungen zu schreiben ist man froh, wenn man so einen Test über die neue Implementierung drüberlaufen lassen kann, und sofort eventuelle Fehler findet. Und wenn man Änderungen macht, sieht man sofort, ob man vielleicht etwas kaputt gemacht hat. Es geht wie gesagt nicht um den eigentlichen _Zweck_ von Unit-Tests. Der sollte jedem einleuchten.

Es geht eher darum, wie man das Ziel erreichen kann, _systematisch_ Tests zu schreiben, wenn das Schreiben und Testen (!) solcher systematischer Tests so schnell so absurd aufwändig wird, wie schon in dem einfachen Map-Beispiel (und dann noch List, Set & Co dazu... Ich will mir gar nicht überlegen, was man noch in Test packen müßte, damit er seinen Namen wirklich verdient, wenn man z.B. spezifisches Verhalten von CopyOnWriteArrayList überprüfen wollte (multi-Threaded Unit-Tests?! :autsch: )). Duch die verschiedenen möglichen Aufrufreihenfolgen entsteht m.E. eine kombinatorische Explosion...

Vielleicht mal ganz konkret: Wer würde sich trauen, eine definitive Aussage darüber zu machen, ob der oben verlinkte Test eine 1:1-Entsprechung zu gültigen Map-Implementierungen darstellt? D.h. gibt es eine gültige Implemetierung, die den Test nicht passiert, oder eine ungültige Implementierung, die von dem Test nicht erkannt wird? :bahnhof: Wie soll man das herausfinden?

Oder sind wir vielleicht gerade im "Tal der Tränen", wo Software nicht mehr so einfach ist, dass man sie "per Hand" formal verifizieren kann, aber die Tools und Sprachfeatures zur formalen Defintion von Vor- und Nachbedingungen und Invarianten und dem automatischen Testen noch nicht praktisch einsetzbar sind, und man deswegen die Fleißarbeit auf sich nehmen muss, tausende Zeilen Testcode zu schreiben, der einen ein bißchen beruhigt, von dem man aber DOCH nie weiß, ob er seinen Zweck wirklich im strengsten Sinn erfüllt?

Bin gespannt auf die Antworten


----------



## nillehammer (4. Sep 2012)

Hallo Marco,

ich schreibe es so wie ICH es mache ohne Anspruch auf Allgemeingültigkeit. Ich betrachte jetzt mal nur Unit-Tests im engeren Sinne. Unit-Tests im engeren Sinne sind Whitebox-Tests. Sprich ich kenne den Code, den ich teste. Von besonderer Bedeutung sind hier die Verzweigungen. Mein Ziel ist eine möglichst vollständige Zweigüberdeckung meiner Tests. Da liegt es in der Natur der Sache, dass selbst bei nur sehr wenig Zweigen die Länge meiner Unit-Tests die Länge des Produktivcodes um ein Vielfaches übersteigt.

Aber selbst mit 100% Zweigüberdeckung ist man noch nicht failsafe. Hinzu kommen evtl. noch verzwickte Implementierungsdetails. Häufig macht man mit seinem Code nämlich Zusicherungen an seine Clients, die einem auf den ersten Blick garnicht auffallen. Dazu folgendes Beispiel: Wir schreiben ein API, dass Arrays in Listen umwandelt (ich weiß gibt's schon, nur als Beispiel). Zunächst das Interface:

```
public interface ArrayUtil {

  public List<E> asList(E[] array);
}
```
Implementierung1:

```
public ArrayUtilImpl1 implements ArrayUtil {

  public List<E> asList(E[] array) {

    if(array == null || array.length == 0) {
       return new ArrayList();
    }
    ...
```
Implementierung2

```
public ArrayUtilImpl2 implements ArrayUtil {

  public List<E> asList(E[] array) {

    if(array == null || array.length == 0) {
       return Collections.emptyList();
    }
    ...
```
Du siehst, beide Implementierungen erfüllen den Kontrakt des Interfaces und behandeln null-Values und leere Arrays gleich. Aaaaber selbst mit 100% Zweigüberdeckung hat man nicht alles getestet. Im ersten Fall hat man nämlich die implizit die Zusicherung an seine Clients gemacht, dass sie die Liste ändern können, im zweiten nicht. Also noch ein Testfall mehr, der Modifikationsoperationen auf der zurückgegebenen leeren Liste aufruft.

Langer Rede kurzer Sinn:
Unit-Test ist Whitebox. Du bist der Entwickler. Du weißt, was Dein Code macht. Teste alle Zweige. Mache Dir über implizite Zusicherungen Deines Codes Gedanken und teste auch die. Also: Teste alles!


----------



## Andgalf (4. Sep 2012)

Also ich tue mich jetzt schwer nicht pauschal zu sagen man schreibt Tests für alles, da ich ein TDD verfechter bin 

So spass beiseite, ich teste auch nicht 100% alles. (z.B. Getter, Setter etc.)Aber dass der Umfang des Testcodes größer ist als der produktive Code himself, halte ich für ziemlich normal.
Klar bedeuted das Mehrarbeit, da dieser Code ja auch gepflegt werden muss. Das ist er meiner Meinung nach aber auch Wert, den neben der Regressionsicherheit stellen Tests auch eine ziemlich gute Dokumentation des Quellcodes dar.

Das man 100% failsave sein kann halte ich für eine Illusion, egal wie gut / umfangreich man tested. Allerdings bringen einen die Tests ein gutes Stück näher ran.


----------



## Marco13 (4. Sep 2012)

OK, beim Vokabular bin ich mir jetzt an einigen Stellen nicht sicher, da gibt es bestimmt ausgefeilte und streng definierte Begriffe...

@Andgalf: Gut, als Programmi^C^C^C^Cessimist geht man natürlich davon aus, dass GENAU die Funktion, die NICHT von den Tests abgedeckt wird, die Probleme verursachen wird  aber... ab einem bestimmten Punkt finde ich (auch), dass die Wahrscheinlichkeit, dass der _Test_ fehlerhaft ist, höher ist, als die Wahrscheinlichkeit, dass die _Implementierung_ fehlerhaft ist...

@nillehammer: Das mit den Verzweigungen war mir einerseits bewußt, andererseits hatte ich es jetzt überhaupt nicht im Kopf: Meine Frage bezog sich primär oder ausschließlich auf den Test von Dingen, die durch ein _Interface_ vorgegeben sind. (Auch wenn man dabei "möglichst alle Fälle" abdecken muss, und damit zumindest indirekt (und ohne dass man es selbst merkt) auch versucht, möglichst alle Verzweigungen abzudecken)

Das Beispiel mit der List ist eines von vielen - man könnte schon darüber lange philosophieren und sich dran aufhängen, aber nur als Beispiel: Wenn in der Interface-Definition steht 

```
/**
 * Returns an unmodifiable List of Foo. It may be empty but is never <code>null</code>.
 * 
 * @return The list
 */ 
public List<Foo> getFoo() { ... }
```
Dann müßte man testen, ob dort "null" zurückgegeben wird, und ggf. failen - klar. 

Dann steht da aber noch "unmodifiable"... 

(BTW: Normalerweise bedeutet das ja nur, dass man nicht davon ausgehen kann, dass die Liste modifiable ist. Ich finde, es wäre nicht unbedingt als Bruch der API anzusehen, wenn man dort eine modifiable zurückgibt - die Spezifikation soll ja eigentlich nur klarmachen, dass der Client davon ausgehen muss, dass sie unmodifiable ist, und wenn sich jemand (trotz der Doku!) darauf verläßt, dass sie modifiable ist, nur weil er einmal eine modifiable bekommen hat, ist er eigentlich selbst schuld... aber natürlich würde man der Hygiene halber eine unmodifiable zurückgeben).

... also konkret: Sollte man (auf so einen Kommentar hin) tatsächlich einen Test schreiben, der prüft, ob die zurückgegebene Liste bei ALLEN (!) modifizierenden Methoden (und bei einem Iterator#remove ihres Iterators) eine "UnsupportedOperationException" wirft? Müßte man ja ... eigentlich, wenn man es _richtig_ machen wollte ... ???:L (Das fühlt sich nur so schrecklich irrelevant an...  )


----------



## Ark (4. Sep 2012)

Ich achte eher darauf, die Schnittstellen möglichst sauber in (meist imaginären ) Javadoc-Kommentaren zu definieren. Was da nicht zugesichert wird, gibt's nicht, fertig.  Entsprechend nutze ich Schnittstellen nur so, wie sie dokumentiert sind. Kurzum: Ich teste nicht primär, sondern versuche stattdessen schon beim Schreiben von Code jede Einzelheit zu bedenken, die einen Einfluss darauf haben könnte, ob ich meine Zusicherungen erfüllen werde oder nicht. Nach vielen so geschriebenen Zeilen kommt es dann irgendwann tatsächlich zum ersten Start des Programms - und zur ersten NPE.  Wenn ich da also Fehler mache, dann eher so Fehler bei der Initialisierung von irgendetwas, die dafür sorgen, dass der Prozess mit Pauken und Trompeten vom Betriebssystem einkassiert wird. ^^ Diese Fehler räume ich dann zügig aus, darauf explizit testen würde ich wohl aber eher nicht.

Grobe API-Design-Fehler halte ich für gefährlicher als schlechte Implementierungen, denn Letztere zeugen von Ersterem und erschweren Fehlerkorrekturen - so meine bisherige Erfahrung. Und da helfen auch keine Unit-Tests.

Gründlichere Tests mache ich eher nur bei Dingen, bei denen Fehler z.B. einen schlecht behebbaren Datenverlust nach sich ziehen könnten, und - im Unterschied zum ersten Absatz - bei Stellen, bei denen schon einmal schwerwiegende Fehler aufgetreten sind. Zum Test, ob ein solcher Fehler tatsächlich durch eine Änderung behoben wird, schreibe ich dann mal einen Unit-Test. Falls ich später wieder mit dem Fail-Kandidaten zu tun habe, kann ich mit dem Test gleich testen, ob ich's wieder vergeigt habe. ^^

Also: NPE etc. ? sofort korrigieren, kein eigener Test : schwerer Fehler aufgetreten ? Unit-Test machen, Fehler korrigeren (bis Unit-Test das Okay gibt) : noch keinen Unit-Test machen

So ungefähr mache ich das meistens.

[OT]Gibt es Testframeworks, mit denen man Concurrency-Problemen auf die Schliche kommen kann? So von wegen: Probiere alle möglichen Wettlaufsituationen aus, die entstehen können, wenn n Threads an jeweils denundden Stellen hinreichend lange warten.[/OT]
Ark


----------



## bygones (4. Sep 2012)

Die Frage nach dem was man nun so alles testen muss ist ein Problem von Testafter. Entwickelt man Testgetrieben, so hat man schon die Tests fuer seine Funktionalitaet so wie sie ist.

Eine generelle Antwort noch auf deine Frage:

Test sind fuer mich use-cases, sie bestimmen meine Implementierung, meine Schnitstellen. Sie sind Dokumentation fuer den Entwickler. Die Verantwortung und Funktionalitaet einer Klasse sind durch Tests intuitiver als in einer externen Doku.

den Fehler den viele machen ist die Tests als die Wahrheit zu sehen. Tests stellen nur das momentanen Wissen eines Systems da, sie sind fehlerhaft und nicht vollstaendig - wie jeder andere Javacode. Aber auch hier ist wieder Testfirst ein immenser Vorteil.

Deine Methode getFoo ist etwas ungluecklich in meinen Augen. Testafter ist sie unsinnig - es heisst sie ist nie null - soll man nun alle moeglichen Szenarien erstellen um das zu zeigen? unsinn. 
Testfirst ist diese Methode aufgrund einer Notwendigkeit entstanden und genau diese hat auch den Testcase gegeben. Sagt dieser dass die Liste nicht null sein darf, so kann das ein testcase sein ja.

Aber wie schon gesagt, Tests sind fuer mich eine gewisse Sicherheit, Use-case beschreibung und Dokumentation.

genug erstmal... kind quakt


----------



## Andgalf (4. Sep 2012)

Marco13 hat gesagt.:


> aber... ab einem bestimmten Punkt finde ich (auch), dass die Wahrscheinlichkeit, dass der _Test_ fehlerhaft ist, höher ist, als die Wahrscheinlichkeit, dass die _Implementierung_ fehlerhaft ist...



Der Argumentation kann ich so nicht folgen. Mit dem Test dokumentiert der Entwickler ja wie sich der produktive Code verhalten soll oder wie er sich seiner Meinung nach verhält, wie kann der Test da falsch / fehlerhaft sein?

Wenn ein Test nun doch fehlerhaft ist, war entweder die ursprüngliche Annahme des Enticklers über die funktionsweise des Codes falsch oder die Spezifikation hat sich geändert.

Im ersten Fall, finde ich es gut zu wissen/erkennen, dass die Annahme falsch war. Meist findet man in dem Zusammenhang andere/weitere Bugs.
Im zweiten Fall ist es ja nur natürlich, das "Wartungsaufwand" entsteht.


----------



## Andgalf (4. Sep 2012)

Ark hat gesagt.:


> Ich achte eher darauf, die Schnittstellen möglichst sauber in (meist imaginären ) Javadoc-Kommentaren zu definieren. Was da nicht zugesichert wird, gibt's nicht, fertig.



Hier nimmst Du mit dem Schlüsselwort "imaginären" eigentlich die Antwort schon vorweg.
Zusätzlich ist es jedoch so, das Javadoc Kommentare zum "verfaulen" neigen. Wenn die Funktionsweise des Codes angepasst wird, wird oft vergessen den Kommentar entsprechend anzupassen und einen falschen Kommentar finde ich sogar noch schlimmer als einen nicht vorhandenen.

Bei Tests kann dir das im Prinzip nicht passieren, denn wenn sich die Funktionsweise des Codes ändert schlägt daraufhin der entsprechende Test fehl und muss angepasst werden. (Continuous Integration setze ich vorraus)


----------



## Ark (4. Sep 2012)

Andgalf hat gesagt.:


> Wenn ein Test nun doch fehlerhaft ist, war entweder die ursprüngliche Annahme des Enticklers über die funktionsweise des Codes falsch oder die Spezifikation hat sich geändert.


Es könnte aber auch ganz einfach sein, dass man in den Test einen Fehler eingebaut hat. Dieser Fall ist trivial, denn wenn man keinen Fehler in einen Test einbauen könnte, dann auch nicht in irgendeinen Quelltext, wenn man diesen nur als Test bezeichnet. 

Oder anders ausgedrückt: Unit-Tests sind auch nur Codestücke, und diese können (wie jedes andere Codestück auch) Fehler enthalten.

Man könnte natürlich auch noch einen Test schreiben, der den Test testet. Und dieser Test-Test kann dann natürlich auch getestet werden … :reflect:

Ark


----------



## Andgalf (4. Sep 2012)

Ark hat gesagt.:


> Es könnte aber auch ganz einfach sein, dass man in den Test einen Fehler eingebaut hat.



OMG, über welche Art Fehler reden wir denn hier? Wohl kaum über Compile-Fehler. Insofern ist jeder Fehler auf irgendeine Art eine fehlerhafte Annahme ob nun im Test oder sonstwo. Und mit den Tests dokumentiert man halt die über den produktiven Code gemachten Annahmen.


----------



## JohannisderKaeufer (4. Sep 2012)

Andgalf hat gesagt.:


> OMG, über welche Art Fehler reden wir denn hier? Wohl kaum über Compile-Fehler. Insofern ist jeder Fehler auf irgendeine Art eine fehlerhafte Annahme ob nun im Test oder sonstwo. Und mit den Tests dokumentiert man halt die über den produktiven Code gemachten Annahmen.



Zu Compile Fehlern: Java wird kompiliert, wenn du allerdings eine Skriptsprache hast, wie beispielsweise Ruby, dann hast du diese Gewissheit des Compilers nicht mehr und erhältst Laufzeitfehler die dir in einer anderen Sprache schon beim Compilieren aufgefallen wären.


Bei Stackoverflow gibt es auch eine Interessante Aussage von Kent Beck

tdd - How deep are your unit tests? - Stack Overflow


Kurzum: Testen bis das Budget aufgebraucht oder die Deadline erreicht ist.


----------



## Andgalf (4. Sep 2012)

JohannisderKaeufer hat gesagt.:


> Zu Compile Fehlern: Java wird kompiliert, wenn du allerdings eine Skriptsprache hast, wie beispielsweise Ruby, dann hast du diese Gewissheit des Compilers nicht mehr und erhältst Laufzeitfehler die dir in einer anderen Sprache schon beim Compilieren aufgefallen wären.


Stimmt über interpretierte Sprachen habe ich da nicht nachgedacht



JohannisderKaeufer hat gesagt.:


> Bei Stackoverflow gibt es auch eine Interessante Aussage von Kent Beck


Interessant, so eine Aussage ausgerechnet von Kent Beck zu lesen  Sind aber auch ein paar spannende Kommentare drunter .... "because the code a developer produces is not his own"


----------



## Ark (4. Sep 2012)

Andgalf hat gesagt.:


> Zusätzlich ist es jedoch so, das Javadoc Kommentare zum "verfaulen" neigen. Wenn die Funktionsweise des Codes angepasst wird, wird oft vergessen den Kommentar entsprechend anzupassen und einen falschen Kommentar finde ich sogar noch schlimmer als einen nicht vorhandenen.


Kein Javadoc → keine Zusicherungen. Zumindest handhabe ich das so. Insofern sehe ich das genauso, dass kein Javadoc besser ist als ein falscher.

Was das "Verfaulen" angeht, sehe ich das etwas anders: Ich schreibe in Javadoc nicht, was eine Methode macht, sondern was sie machen _sollte_. Javadoc-Kommentare sind für mich sozusagen Spezifikation und Dokumentation in einem. Beim Schreiben von Code verlasse ich mich auf diese Kommentare. Wie die Implementierung aussieht, ist mir da egal. (Das mögen andere anders sehen. ^^) Dahingehend kann ein Javadoc-Kommentar also nicht der tatsächlichen Implementierung hinterherhinken, weil der Kommentar gar nicht von der Implementierung abhängt.

Wenn ich merke, dass da etwas nicht funktioniert, muss (sollte ) der Fehler an der richtigen Stelle behoben werden. Und wenn ich eben merke, dass der Fehler darin besteht, dass ich die falsche Methode aufrufe, muss ich eben eine geeignetere Methode aufrufen (oder erst implementieren). Aber ich werde keine Methode entgegen ihrer Spezifikation ändern, nur damit mein Code funktioniert oder irgendein Unit-Test durchläuft.

Kurzum: anstatt im Nachhinein lange in einer Debugging-Testing-Schleife zu hängen, versuche ich, Fehler gar nicht erst entstehen zu lassen. Und dies meine ich vor allem damit zu erreichen, Schnittstellen sauber festzulegen. Unit-Tests beschleunigen die Fehlersuche, falls man durch Ändern des Codes etwas kaputtgemacht haben sollte. Aber sie allein sagen mir nicht, wo die Ursache des zu behebenden Fehlers liegt. Da kann nur eine Spezifikation weiterhelfen. Und dadurch, dass ich mich nur auf diese Spezifikation verlasse, kann ich auch sichergehen, dass sich der Fehler auf diese Stelle beschränkt.



Andgalf hat gesagt.:


> OMG, über welche Art Fehler reden wir denn hier?


Tippfehler.

Ark


----------



## Marco13 (4. Sep 2012)

@Ark: Teilweise stimme ich den genannten Zielen zu. Oder andersrum, etwas plakativ: Eine besch****ene API wird nicht dadurch besser, dass man sie gründlich Unit-Testet  Die implizit angedeutete Strategie ""nur die Fehler zu beheben, die auch auftreten"" funktioniert für vieles, aber für manche Sachen ist es IMHO nicht ausreichend, wenn der "Testbericht" lautet: "Ich hab's gestartet, und es ging" 

[OT]


> Gibt es Testframeworks, mit denen man Concurrency-Problemen auf die Schliche kommen kann? So von wegen: Probiere alle möglichen Wettlaufsituationen aus, die entstehen können, wenn n Threads an jeweils denundden Stellen hinreichend lange warten.



Tatsächlich gibt es Entwicklungen dazu (wird ja auch immer wichtiger!). Auf der parallel 2012 - Softwarekonferenz für Parallel Programming, Concurrency und Multicore-Systeme - Karlsruhe, 23.-24. Mai 2012 :: Agenda / Programm wurden da einige vorgestellt. Es gab mehrere Vorträge dazu, sowas wie parallel 2012 - Softwarekonferenz für Parallel Programming, Concurrency und Multicore-Systeme - Karlsruhe, 23.-24. Mai 2012 :: Testwerkzeuge für nebenläufige Anwendungen hatte ich mir mal angehört, und das klang schon ganz interessant (diese Tools haben natürlich Grenzen, aber gehen schon recht weit)
[/OT]

@bygones: _"Die Frage nach dem was man nun so alles testen muss ist ein Problem von Testafter. Entwickelt man Testgetrieben, so hat man schon die Tests fuer seine Funktionalitaet so wie sie ist."_
Hm... vielleicht liegt ein Mißverständnis vor, aber ... so sehe ich das erstmal nicht. Selbst bei TDD muss man seinen Test ja "gegen" etwas schreiben - irgendein gemocktes Interface (sooo kenn' ich mich damit nicht aus  aber soweit ich das bisher verstanden hatte). Sowas wie ein null-Test ist ja nun der einfachste denkbare Fall. Wie würde es aussehen, wenn man z.B. ein "Collection"-Interface definieren wollte? Ja, [c]add,remove,size,contains...[/c] und bei List soll es sich anders verhalten als bei Set, und vielleicht soll es manchmal noch unmodifiable sein, und wenn man die Methoden in unterschiedlichen Reihenfolgen aufruft um ihr _Verhalten_ (mit möglichst großer Abdeckung) zu testen ... wie man an dem im ersten Thread verlinkten Versuch eines gewissenhaften Tests sieht, explodiert der Aufwand für den Test (soweit ich das verstehe: auch bei TDD) da ja recht schnell. (Und BTW: Sieht man einem Test an, ob er durch TDD oder durch 'Testafter' entstanden ist? Eigentlich doch nicht...!?)

@Andgalf: _"Mit dem Test dokumentiert der Entwickler ja wie sich der produktive Code verhalten soll oder wie er sich seiner Meinung nach verhält, wie kann der Test da falsch / fehlerhaft sein?"_
Das klingt ein bißchen wie das, wobei ich mich auch schon ertappt habe: Ich schreibe eine Klasse, definiere _nachher_ den Test, lasse den Test laufen - und er failt - und dann passe ich den Test an, weil die Implementierung ja richtig ist, was ich schon vorher mit System.out's überprüft habe  (So sehr das TDD-Fans im ersten Moment vielleicht weh tut, es ist IMHO schon sehr sinnvoll - wiederum weil ab einem bestimmten Punkt ja egal sein kann, wie ein Test entstanden ist). Ansonsten muss ich Ark zustimmen: Wenn man in einer Implementierung mit 100 Zeilen einen Fehler macht, wäre es schon fast naiv zu glauben, dass man in einem dazugehörigen Test mit 400 Zeilen keinen Fehler macht. Insbesondere bezieht sich "Fehler" hier nicht auf eine NPE oder irgendeinen offensichtlich falschen Test der dann failt, sondern, wie du auch selbst angedeutet hast, auf genau die Fehler, die man nicht bemerkt: Unzulänglichkeiten und unbewußt gemachte Annahmen, die eben eine andere (aber immernoch "richtige") Implementierung failen lassen, oder schlimmer, eine "falsche" Implementierung durchlassen, weil gerade der fehlerhafte Punkt schlicht nicht getestet wird. Zuletzt klang das jetzt schon fast als wolltest du propagieren, dass ein nicht der Spezifikation entsprechendes Verhalten dadurch "zur Spezifikation erhoben" wird, weil anfangs mal ein Unit-Test geschrieben wurde, der genau dieses unspezifizierte Verhalten erwartete ???:L aber das meintest du wohl nicht  

Nochmal @Ark: Ich versuche auch so weit wie möglich diesem an "Design by Contract" angelehnten Prinzip zu folgen, und ein Teilaspekt davon ist zumindest das Bemühen um eine gewissenhafte JavaDoc-Dokumentation. Trotzdem gehen Unit-Tests (und der Nutzen, der damit verbunden sein sollte) ja deutlich darüber hinaus. Man kann, wenn man so eine Klasse wie "ArrayList" schreibt, und dort die Methode "subList" anbietet noch so genau schreiben, was die Methode macht: Ob sich das zurückgegebene Objekt wirklich konform zur Spezifikation von "List" verhält, kann man mit letzter Gewissheit nur mit einem erschöpfenden Unit-Test sagen - und ich behaupte: Selbst das nur theoretisch, solange es kein formales Modell gibt, mit dem man mathematisch streng beweisen kann, dass alle (bisher leider bestenfalls als Prosa in den JavaDoc stehenden) Vor- und Nachbedingungen erfüllt sind. Insofern bringen Unit-Tests nur "etwas mehr" Sicherheit, die bei "pragmatischer Gewissenhaftigkeit" eben damit bezahlt wird, dass man SEHR viel Zeit investiert und SEHR viel Code schreibt, dessen mittelbarer oder unmittelbarer Nutzen für mich kaum im Verhältnis zum Aufwand steht. 

Die Idee von Unit-Tests ist schön, und der Gedanke, damit Debugging-Zeit zu sparen und sauberere, verlässlichere Software zu schreiben und sie leichter warten zu können ist verlockend, aber wie man Unit-Tests gestalten sollte, um wirklich diesen Effekt zu haben, ohne dass man etliche Tausend Zeilen stupide runterschreiben muss (und am Ende doch nicht zu wissen, ob sie diesen Zweck überhaupt erfüllen) hat sich mir noch nicht erschlossen. Die Vorgehensweise: "Ja, ich teste die Klassen, die ich für wichtig halte, so, wie ich es für richtig halte, und teste dabei das, wovon ich glaube, dass es schiefgehen könnte" wirkt irgendwie nicht so überzeugend, und setzt einen der Gefahr aus, dass man sich von anderen (oder sich selbst) vorwerfen lassen muss, dass die vielen grünen Balken nur ein Potemkinsches Dorf sind, mit dem man sein Gewissen beruhigen und im oberflächlichen Sinn seine Selbstzweifel beseitigen will. (Das klingt jetzt vielleicht als wollte ich Unit-Tests diskreditieren - genau das will ich aber NICHT - ich will nur wissen, wie man sie "richtig" oder "vernünftig" macht, ohne dass man den größten Teil seiner Zeit mit Testcodeschreiben verbringt :rtfm: )


----------



## Spacerat (5. Sep 2012)

Ich würde glatt mal nach fragen, wer diese Unit-Tests letztendlich überhaupt macht. Wenn man so wie Ark vorgeht, entwickelt man seine Methoden doch eigentlich ohnehin spezifikationskonform, zumindest nimmt man das selbst an und sagt, die Methode arbeitet korrekt. Ich würde solche Unit-Tests eher als Exploits betrachten, denn solange einer failt, ist diese Stelle angreifbar. Evtl. kommt diese Idee ja auch aus dieser Szene (Hacker, Cracker usw.), weil Serverausfälle durch Hackangriffe viel teurer werden, als wenn man vorher selber testet bzw. testen lässt, welch' Schindluder mit den eigenen Entwicklungen getrieben werden kann. Unit-Tests sind aus meiner Sicht demnach erst dann erforderlich, wenn eine Implementation mit sensitiven Daten umgehen muss. Unit-Tests sollten sich deswegen auch nicht unbedingt strikt an irgendwelche Spezifikationen halten, sondern eher ein "Ich krieg' dich schon klein!" anstreben.
Kurzum: Untit-Tests sollten auch bzw. vorzugsweise die Dinge testen, die nicht in der Spezifikation stehen aber genau dann failen. Es gibt also keine fehlerhaften Unit-Tests.
Entwickler wären demzufolge auch selbst gar nicht in der Lage, sinnvolle Unit-Tests für ihre eigenen Methoden zu schreiben.
[EDIT]BTW.: @Marco13: Ich weis gar nicht, wieso du mit nicht bygones Meinung jedoch mit Arks Vorgehen konform gehst. Ist Arks Vorgehen etwa nicht das, was man hinlänglich als "Testdriven" bezeichnet?[/EDIT]


----------



## Guest2 (5. Sep 2012)

Moin,



Marco13 hat gesagt.:


> Ich schreibe eine Klasse, definiere _nachher_ den Test, lasse den Test laufen - und er failt - und dann passe ich den Test an, weil die Implementierung ja richtig ist, was ich schon vorher mit System.out's überprüft habe



einer meiner Profs meinte mal, wenn man sich selbst dabei ertappt ein System.out einzutippen es allerhöchste Zeit für einen passenden Unit-Test wird, sonnst fängt man schlicht an Zeit zu verschwenden. Und letztendlich ist es auch so, das schreiben eines Unit-Tests dauert nicht viel länger als das schreiben eines System.out. Gleichzeitig bleibt der Unit-Test für "immer" während das System.out kurzzeitiger "Unsinn" ist. Wenn das schreiben des Unit-Tests länger dauert als das System.out, liegt imho die Vermutung nahe das man unter umständen gerade einen verkappten IT-Test schreibt.

Persönlich strebe ich bei Unit-Tests auch nicht unbedingt eine besonders hohe Testabdeckung an. Lieber sind mit IT-Tests, welche die komplette Anwendung oder Bibliothek mit einem Soll-Verhalten vergleichen. Solange die ohne Fehler laufen, gehe ich davon aus, dass der Code im Grunde in Ordnung ist. Wenn nicht, lokalisiere ich den Fehler durch das einstreuen weiter IT- oder Unit-Tests, bis ich den Fehler lokalisiert habe. Und eben nicht durch das einstreuen von System.outs (zumindest nehme ich mir das immer vor ).

Außerdem sind die IT-Tests perfekte Beispiele für die Dokumentation. Letztendlich sind die meisten meiner IT-Tests nämlich eben genau einfache Beispiele, nur das sie automatisiert ausgeführt werden können und es ein Soll-Verhalten gibt.

Viele Grüße,
Fancy


----------



## FArt (5. Sep 2012)

Ein Entwickler entwickelt in der Regel mit der Zeit ein Gefühl dafür, welche Stellen wie intensiv zu testen sind, genau wie auch bei Logging, welche Informationen wann und wo in welchem Loglevel interessant sind. Es verhält sich also wie Code allgemein: mit der Zeit schreibt man (hoffentlich) immer besseren Code und Tests.

Ich schreibe Tests für die öffnentliche Schnittstelle vor der Implementierung, um die Anforderungen noch mal klarzustellen und evtl. Lücklen aufzudecken, die dann im Vorfeld noch geklärt werden müssen. Das ist schon mal eine Art Schnittstellendokumentation und muss sich mit allen funktionalen und nichtfunktionalen Anforderungen decken (auch wenn man diese u.U. nicht alle abprüfen kann). Für andere, kleiner Funktionalitäten schreibe ich dann (oft während der Implementierung der Funktionalität) abgeschlossene Tests.

Tests schreiben ist ein iterativer Prozess, genau wie entwicklen an sich, d.h. im Laufe der Zeit werden immer mehr Tests hinzukommen, manche fallen u.U. weg. In der Regel versuche ich jeden Bug mit einem Test zu reproduzieren und dann zu fixen. Das steigert die Codequalität sehr, und man ist sich bei Refaktorierungen eingigermaßen sicher, dass man nichts wesentliches kaputt gemacht hat.

Tests sind aber nur so gut, wie sie praktikabel sind und auch berücksichtigt werden. Wer z.B. fehlschlagende Tests über längere Zeit (z.B. im Jenkins) ignoriert, braucht keine Tests zu schreiben. Wenn die Tests zu lange laufen, wird sie der Entwickler nicht regelmäßig ausführen. Somit sind sie auch nicht mehr sehr wertvoll.
Bei uns kann z.B. keine Release gebaut werden, wenn nicht alle Tests laufen. Eine sinnvolle "Testabdeckung" wird gleichzeitig über Metriken und Reviews sichergestellt.


----------



## Spacerat (5. Sep 2012)

:idea: Oi... jetzt steige ich anscheinend auch langsam dahinter. Unit-Tests sollen einem im Vorfeld sagen, was eine Methode können muss und was sie nicht können darf. Das wäre dann auf jeden Fall was ganz anderes als "Schadensbegrenzung" im Fehlerfall. Klar... in seiner Verzweiflung hilft einem dann evtl. doch noch ein "System.out", wenn man dass, was der Unit-Test abverlangt nicht gedeichselt bekommt. Die "Unit-Tests", die nach dem Implementieren der Methode entstehen (maw.: wie ich sie bisher sah), können dann wohl auch Sache der Hacker bleiben.


----------



## Andgalf (5. Sep 2012)

Spacerat hat gesagt.:


> :idea: Oi... jetzt steige ich anscheinend auch langsam dahinter. Unit-Tests sollen einem im Vorfeld sagen, was eine Methode können muss und was sie nicht können darf. Das wäre dann auf jeden Fall was ganz anderes als "Schadensbegrenzung" im Fehlerfall.



Korrekt, das ist das Prinzip hinter TDD. Wenn man TDD korrekt anwendet, sorgt dies auch für ein schlankes Design, da man ja immer nur genau so viel Code produziert bis alle Tests "grün" sind. Danach wird halt der nächste Test geschrieben, der ein weitere Eigenschaft/Funktionalität absichert die der Code einhalten soll. Naturgemäß ist dieser Test erstmal rot, da man ja Test first vorgeht.

.... mom was mach ich hier eigentlich ich wollte doch gar nicht TDD erklären, dafür gibbet Bücher 

Auf jeden Fall muss ich FArt hier zustimmen:



> Das steigert die Codequalität sehr, und man ist sich bei Refaktorierungen eingigermaßen sicher, dass man nichts wesentliches kaputt gemacht hat.



Seit ich bzw. wir Testdriven arbeiten, gibt es keine "Angst" mehr vor Refactorings ... Da wir mit den Tests überprüfen können, ob das ursprüngliche Verhalten des Codes sich nicht verändert hat.


----------



## bygones (5. Sep 2012)

Marco13 hat gesagt.:


> [WALL OF TEXT] ich will nur wissen, wie man sie "richtig" oder "vernünftig" macht, ohne dass man den größten Teil seiner Zeit mit Testcodeschreiben verbringt :rtfm: )


interessanter weise wird testcodeschreiben immer als das Boese betrachtet, waehrend code schreiben das Gute ist. Beides ist code. TDD ist nun mal eben dass man einen grossteil seiner Zeit mit Testschreiben verbringt, aber ist das nun das Problem oder nicht ? Ich denke deine Frage zieht er auf das sinnvolle schreiben bzw. effiziente Schreiben von Tests ab. Und da ist es wie bei jedem code - learning by doing, akzeptieren, dass man eine Schwachstelle hat und daran arbeiten. Je mehr man es macht, desto schneller und besser wird man. Anfangs bremst einen das aus, und das verteufeln dann die meisten, da der Langzeiteffekt missachtet wird.

sorry dass ich nicht auf den ganzen text antworte, ist a) zu viel und b) halte ich solche Diskussion fuer ermuedend. Jede Seite hat Bsp wie und warum es klappt/nicht klappt. Theoretisch etwas zu sagen ist immer leicht. Am sinnvollsten waere es sich zu 2. vor einem Rechner zu hocken und einige Pair-Programming-TDD stunden zu verbringen. Alles andere ist fragwuerdig imo.

Nur noch zu deiner Frage
_"(Und BTW: Sieht man einem Test an, ob er durch TDD oder durch 'Testafter' entstanden ist? Eigentlich doch nicht...!?)"_
ich behaupte ja (die ausnahmen bestaetigen das). TDD test code ist intuitiver, kuerzer und verstaendlicher.



Spacerat hat gesagt.:


> :idea: Oi... jetzt steige ich anscheinend auch langsam dahinter. Unit-Tests sollen einem im Vorfeld sagen, was eine Methode können muss und was sie nicht können darf. Das wäre dann auf jeden Fall was ganz anderes als "Schadensbegrenzung" im Fehlerfall. Klar... in seiner Verzweiflung hilft einem dann evtl. doch noch ein "System.out", wenn man dass, was der Unit-Test abverlangt nicht gedeichselt bekommt. Die "Unit-Tests", die nach dem Implementieren der Methode entstehen (maw.: wie ich sie bisher sah), können dann wohl auch Sache der Hacker bleiben.


jo - TDD erstellt dir Tests die zeigen, was du machen wolltest, Testafter zeigt dir was du tust. Klingt aehnlich, aber ist im Detail womoeglich ein welten unterschied.


----------



## Andgalf (5. Sep 2012)

full Ack @bygones


----------



## Marco13 (5. Sep 2012)

@Spacerat: Vielleicht gibt es da subtile Unterschiede in den Begrifflichkeiten. Aber das von Ark beschriebene klang für mich (Ahnungslosen  ) eher nach "Design by Contract" als nach dem von bygones angesrochenen TDD. Wenn ich das richtig verstanden habe, kamen bei ersterem ja keine echten Unit-Tests drin vor, sondern "nur" eine detaillierte Beschreibung und Spezifikation und deren gewissenhafte Implementierung (wobei sich beides natürlich nicht gegenseitig ausschließt).  

Die Beschränkung von Unit-Tests auf sensitive Bereiche sehe ich aber auch nicht so. Ich finde (vielleicht weil es bei mir konkret um etwas ähnliches geht) die Collections ein gutes Beispiel: Man definiert das "Map"-Interface, und implementiert dann die HashMap. Und dann implementiert man die TreeMap, und will schnell sicherstellen, dass sie der Spezifikation genügt (und zwar im strengeren Sinne als: "Ich hab bei der Implementierung ganz doll aufgepasst"). Und das gleiche dann für LinkedHashMap und alle anderen Implementierungen die es gibt und noch geben wird. Und wenn man irgendwo was ändert, läßt man nur noch alle Tests durchlaufen, und sieht, dass man nichts kaputt gemacht hat. 

Vielleicht liegt eine der Ursachen für die unterschiedlichen Ansichten in genau diesen konkreten Zielen: Es ging mir weniger um Unit-Tests von irgendwelchen package-privaten, finalen Klassen, sondern eher (aber nicht ausschließlich!) um eine Art Konformitätstest verschiedener Implementierungen eines öffentlichen Interfaces. Und das kann auch derjenige entwickeln, der das Interface defniert, indem er ""alle"" Verhaltensweisen überprüft, die in Interface festgelegt sind. 


@Guest2: Diese Aussage mit "Unit-Test statt System.out" hatte ich bei meinen ersten Websuchen dazu auch irgendwo gelesen. Üblicherweise würde ich bei sowas sagen: "Ich finde das unrealistisch". Hier gehe ich so weit zu sagen: "Das IST unrealistisch" (aber vielleicht bewußt, übertrieben, um dem entgegenzuwirken, dass jemand den Aufwand, den man für einen Unit-Test betreiben könnte, stattdessen in einen Haufen System.out-Tests steckt). 
Was genau du mit IT-Tests meinst, ist mir nicht ganz klar. Für mich war es schon /bevor ich wirklich an Unit-Tests dachte) selbstverständlich, dass ich zu "komplizierten" Klassen/Methoden eine main erstelle, die diese Funktionalität ganz isoliert (mit System.outs und ggf. dummy-Daten) prüft. Das ist (und wäre auch bei ausführlichen Unit-Tests) einfach Teil meiner Strategie: Wenn man von Anfang an die API benutzt, die man da gerade entwickelt, sieht man, wo es hakt, und das was rauskommt sind gleich praktische Codesnippets. Aber Unit-Tests hätten eben eine höhere Verläßlichkeit (wenn sie systematisch erstellt werden, und viel abdecken) und Wiederholbarkeit, dadurch dass sie automatisiert ausgeführt werden können. 

@FArt: _Ich schreibe Tests für die öffnentliche Schnittstelle vor der Implementierung, um die Anforderungen noch mal klarzustellen und evtl. Lücklen aufzudecken, die dann im Vorfeld noch geklärt werden müssen. Das ist schon mal eine Art Schnittstellendokumentation und muss sich mit allen funktionalen und nichtfunktionalen Anforderungen decken (auch wenn man diese u.U. nicht alle abprüfen kann). ... Tests schreiben ist ein iterativer Prozess, ..._

Das klingt realistisch und praktikabel - aber (sorry) eben auch ein bißchen "hilflos". Natürlich kann einem kein Kochbuch das Denken abnehmen - jeder Entwickler hat da eine Verantwortung, die er wahrnehmen muss. Aber es gibt bei dieser Beschreibung einige Freiheitsgrade, die im schlimmsten Fall ausgenutzt werden können, und die Unit-Tests ad absurdum führen. 

```
/** ...Returns an unmodifiable List of even numbers, never null... */
List<Integer> getEvenNumbers()  { ... }

void testEvenNumbers()
{
    assertNotNull(someObject.getEvenNumbers());
}
```
Wenn so ein Test durchläuft, sagt das ja nichts aus. Man müßte noch prüfen, ob die Zahlen even sind

```
void testEvenNumbers()
{
    assertNotNull(someObject.getEvenNumbers());
    for (Integer i : someObject.getEventNumbers()) assertIsEven(i);
}
```
Auch das sagt nichts aus. (Mal abgesehen davon, dass man nicht weiß, ob beim ersten Aufruf dieSELBE Liste zurückgegeben wurde, wie beim zweiten :autsch: ) Aber testen wir noch die nicht-modifizierbarkeit

```
void testEvenNumbers()
{
    List<Integer> list = someObject.getEventNumbers()
    assertNotNull(list);
    for (Integer i : list) assertIsEven(i);
    assertThrowsException(list.add(2));
    assertThrowsException(list.remove(0));
    assertThrowsException(list.removeAll(...));
    ...
}
```
Und den Iterator nicht vergessen:

```
void testEvenNumbers()
{
    List<Integer> list = someObject.getEventNumbers()
    assertNotNull(list);
    for (Integer i : list) assertIsEven(i);
    assertThrowsException(list.add(2));
    assertThrowsException(list.remove(0));
    assertThrowsException(list.removeAll(...));
    ...
    Iterator<Integer> iterator = list.iterator();
    assertThrowsException(iterator.remove());
}
```
Gut, den _ersten_ Eintrag kann man mit dem Iterator schonmal nicht entfernen. Vielleicht aber den zweiten? Ja, ich weiß, _spätestens_ jetzt wird's völlig absurd :autsch: ... aber wäre nicht NUR so etwas ein wirklich systematischer und gewissenhafter Test?

Nochmal anschauen wie die Methode standardmäßig implementiert ist:

```
List<Integer> getEvenNumbers()  
{
    return Arrays.asList(2,4,6); 
}
```
Tja, das lohnt sich dann ja nicht... Außer wenn jemand irgendwann die Methode mal implementiert als

```
List<Integer> getEvenNumbers()  
{
    return new SpecialListWhereTheLastElementMayBeRemovedWithAnIterator(2,4,6);
}
```
oder (naheliegender) als

```
List<Integer> getEvenNumbers()  
{
    return Arrays.asList(2,null,6); // Darüber sagt die JavaDoc nichts...
}
```
und das dann zu Problemen führt. 

Bevor da zu sehr drauf rumgehackt wird: Das Beispiel dient nur der Verdeutlichung - im Sinne der Frage im von JohannisderKaeufer verlinkten Stack: Wie _tief_ sind Unit-Tests? .... Und es scheint: Im Zweifelsfall wohl sehr oberflächlich, weil alles andere nicht praktikabel wäre. Ähnlich wie der Kommentar von Kent Beck dort, der ein Zeichen dafür ist, dass wir wirklich in dem im ersten Beitrag erwähnten "Tal der Tränen" stecken.

Jedenfalls erscheint mir das als einziger praktikabler Mittelweg zwischen "gar keine Tests" und "Uber-Tests (wie die angedeuteten)" : Man schreibt oberflächliche Tests für die öffentliche API, und geht nur dort mit Tests (iterativ) weiter in die Tiefe, wo Probleme auftreten. Ich finde aber, dass das den theoretischen Nutzen und die durch solche Tests eigentlich angestrebte Verläßlichkeit praktisch auf null reduziert, und sie "nur" zu reinen Regressionstests werden... :bahnhof:


EDIT: Die neue "Wall of Text" hat sich mit einigen Antworten überschnitten. 

@bygones: _"Und da ist es wie bei jedem code - learning by doing, akzeptieren, dass man eine Schwachstelle hat und daran arbeiten."_

Ja, so hatte ich das jetzt auch mitgenommen. Nicht wirklich zufriefenstellend, aber ... damit wird man sich wohl (bisher noch) abfinden müssen.


----------



## bygones (5. Sep 2012)

dein Bsp ist ein bsp fuer einen unsinnigen test.

deine Methode sagt aus [c]testEvenNumbers()[/c], d.h. sie soll testen, dass deine liste gerade zahlen beinhaltet. Mehr nicht. 

Es gibt eine Faustregel fuer tests: es gibt ein assert. mehrere Asserts bedeuten meist dass die Methode zu viel macht. Auch testmethoden folgen dem SingleResponsibility Prinzip. Wenn du meinst alle die anderen Funktionalitaeten auch testen zu wollen/muessen, so gehoeren sie in ihre eigene Methode.

weiterhin ist der test fragwuerdig, da du nicht deinen Code testest sondern die java collection, ergo fremden code. (mag natuerlich nur dem bsp geschuldet sein).

Ich seh allgemein die Vorangehensweise anders. Eine Schnittstelle evolviert aus einer Implementierung. Die Tests bestimmen meine Implementierung und diese zeigt dann die Schnittstelle auf. Fuer mich ist der andere Weg zu vorausdenkend und voller Annahme, was wohl eine Schnittstelle wie braucht. Das weiss man aber erst, wenn man use-cases umgesetzt hat. Ergo. Tests bestimmen die Implementierung, die mir dann meine Schnittstelle aufzeigen.

Fuer viele ist diese Weise anscheinend zu revolutionaer und entgegen die (jahre)lange Arbeitsweise, als dass sie das nicht akzeptieren, probieren wollen. Dann braucht man sich nicht wundern, wenn man in Tests und TDD keinen Vorteil sieht. Dann ist es aber ebenso unsinnig sich darauf zu versteifen.



Marco13 hat gesagt.:


> @bygones: _"Und da ist es wie bei jedem code - learning by doing, akzeptieren, dass man eine Schwachstelle hat und daran arbeiten."_
> 
> Ja, so hatte ich das jetzt auch mitgenommen. Nicht wirklich zufriefenstellend, aber ... damit wird man sich wohl (bisher noch) abfinden müssen.


ernsthaft - was hast du erwartet ?


----------



## Marco13 (5. Sep 2012)

_deine Methode sagt aus testEvenNumbers() , d.h. sie soll testen, dass deine liste gerade zahlen beinhaltet. ... Es gibt eine Faustregel fuer tests: es gibt ein assert. mehrere Asserts bedeuten meist dass die Methode zu viel macht. ... weiterhin ist der test fragwuerdig, da du nicht deinen Code testest sondern die java collection_

Das Beispiel war natürlich suggestiv und unsinnig, aber sollte verdeutlichen, dass ich mir über die "Reichweite" bzw. eben _Tiefe_ der Tests nicht im Klaren bin. Natürlich waren die meisten Teile dieses Tests _eigentlich_ Tests für eine UnmodifiableList. Aber wenn man sagt, dass man dort eine UnmodifiableList zurückgibt, müßte man eigentlich testen, ob das wirklich der Fall ist ... :bahnhof: 

(Vielleicht ist das mit dem "Unmodifiable" dort als Beispiel auch nicht so geeignet. Man könnte versuchen den Punkt zu verallgemeinern zu "irgendeine Zusicherung, die in der Ppaxis sehr schwer zu überprüfen ist", aber das würde klingen, als würde dort beim Design noch anderes im Argen liegen...)


_Ich seh allgemein die Vorangehensweise anders. Eine Schnittstelle evolviert aus einer Implementierung. ... _

Hm... das driftet vielleicht jetzt ab, aber ... ich dachte immer, dass ein Teilaspekt von TDD das Testen eines Interfaces ist - nicht notwendigerweise (aber naheliegenerweise) eines Java-Interfaces, sondern wirklich der Schnittstelle. Ich sehe es aber auch so, dass man dieses nur definieren kann, wenn man sich über dessen Verwendung im Klaren ist. Und ob man sich darüber durch das Vorab-Erstellen von Unit-Tests klar werden kann, oder vielleicht doch nur durch das praktische Erstellen von "KSKBs" ist eigentlich ein anderes Thema. 


_ernsthaft - was hast du erwartet ?_

Einen Thread, der so ähnlich aussieht, wie dieser hier  Dass es nicht eine 1-Zeilen-Antwort gibt, die alle Fragen klärt und jeden perfekt zufrieden stellt, ist klar. Aber die allgemeine Frage, wie detailliert (oder "tief") Unit-Tests sein sollten fand und finde ich berechtigt, und es ist gut, verschiedene Standpunkte dazu zu hören. 

Um das nochmal zu erklären: Ich will eine Bibliothek entwickeln (bzw. habe das schon teilweise), die gewisse Parallelen zu den Collections hat: Einige überschaubare Interfaces, mit teilweise recht spezischen Zusicherungen in den JavaDocs, und einige verschiedene Implementierungen dazu. Ich würde gerne testen, ob alle Implementierungen sich konform zur Spezifikation verhalten. Wenn man dann aber (an dem 'MapInterfaceTest' aus Guava) sieht, wie exorbitant aufwändig schon ein Test für ein einziges relativ einfaches(!) Interface ist, wenn man es _gewissenhaft_ machen will, fällt es mir schwer, mir vorzustellen, wie man das für komplexere Interfaces in der Praxis (und ohne einen 200-Milliarden-Dollar-Konzern im Rücken ) tatsächlich machen sollte. Und dieser Thread hat mich jetzt der Überzeugung näher gebracht, dass das schlicht nicht geht, und dass man einige Freiheiten hat, und sich nicht unbedingt Vorwürfe anhören werden muss, wenn der Test nicht "perfekt und allumfassend" ist, solange er "gut" und "zweckmäßig" ist.


----------



## maki (5. Sep 2012)

> Hm... das driftet vielleicht jetzt ab, aber ... ich dachte immer, dass ein Teilaspekt von TDD das Testen eines Interfaces ist - nicht notwendigerweise (aber naheliegenerweise) eines Java-Interfaces, sondern wirklich der Schnittstelle. Ich sehe es aber auch so, dass man dieses nur definieren kann, wenn man sich über dessen Verwendung im Klaren ist. Und ob man sich darüber durch das Vorab-Erstellen von Unit-Tests klar werden kann, oder vielleicht doch nur durch das praktische Erstellen von "KSKBs" ist eigentlich ein anderes Thema.


umgekehrt wird ein Schuh draus 

Bei TDD werden keine Schittstellen gestestet, sondern Implementierungen, also White Box Tests.
Man "mockt" Abhängigkeiten "weg" indem eben Mock-Implementieruengen für externe Abhängigkeiten verwendet werden, die meist als Interfaces bestehen(oder eben gerade durch TDD "entdeckt" wurden - "Interface Discovery", da wird TDD zum Designtool), aber die testet man eben nicht, sondern man verwendet/entdeckt sie im test.

Das witzige bei TDD ist u.a., dass man sich über die Verwendung externer Abhängigkeiten erst während des schreibens klar wird, man definiert diese Dinge nicht vorher als feste Schnittstelle etc.


----------



## TP@Urlaub (5. Sep 2012)

Marco13 hat gesagt.:


> @Spacerat: Vielleicht gibt es da subtile Unterschiede in den Begrifflichkeiten. Aber das von Ark beschriebene klang für mich (Ahnungslosen  ) eher nach "Design by Contract" als nach dem von bygones angesrochenen TDD. Wenn ich das richtig verstanden habe, kamen bei ersterem ja keine echten Unit-Tests drin vor, sondern "nur" eine detaillierte Beschreibung und Spezifikation und deren gewissenhafte Implementierung (wobei sich beides natürlich nicht gegenseitig ausschließt).
> 
> [...]



Design by Contract ist übrigens ein geschützer Begriff und es gehört mehr dazu als eine "detaillierte Beschreibung" sowie einer "gewissenhaften Implementierung". Um das besser zu verstehen müsste man sich Eiffel ansehen (die Sprache von Bertrand Meyer) in die Sprache sind die Methoden derer sich DbC bedient integriert, d.h. es ist problemlos möglich Vor-/Nachbedingungen, Varianten, Invarianten *im Code* zu formulieren welche dann zur Laufzeit immer ausgewertet werden (es sei denn man sagt dem Compiler er soll sie explizit nicht übernehmen). Damit fallen schon mal eine ganze Menge Unit-Tests weg, IMHO sind Unit-Tests in Verbindung mit DbC mehr Integrationstests. Jedenfalls sind die gebotenen Möglichkeiten der Sprache ein ganz anderes Kaliber als nur eine "gewissenhafte Implementierung". Leider bietet Java nichts Großartiges in dieser Richtung ausser vll. das assert-Statement, welches aber nur einen minimalen Teil abdeckt. Man kann die DbC-Geschichte in Java aber bis zu einem gewissen Grad simulieren, z.B. eigene Methoden die die Invarianten abprüfen etc. Das gelbe vom Ei ist das nicht, es hilft aber.


----------



## Marco13 (5. Sep 2012)

@maki: Vermutlich merkt man nur, was es bedeutet, wenn man mal versucht, es selbst anzuwenden ... vielleicht habe ich mich aber auch unklar ausgedrückt. Man KANN ein Interface in diesem Sinn ja nicht "testen", es muss immer eine Implementierung sein  

@TP@Urlaub: Ich weiß, dass Eiffel (und vermutlich andere Sprachen) die Idee des Design by Contract stärker in die Sprache selbst integrieren. Ich fand das schon überzeugend, so wie ich es damals im Studium mitbekommen hatte, und versuche, zumindest die Grundideen davon mitzunehmen: Auch wenn man die Conditions nicht formal und automatisch zusichern kann, sehe ich die JavaDoc doch als eine Art "Vertrag", auf den man sich verlassen können sollte. Was darüber hinausgeht (auch in dem Sinne, wie es in Eiffel schon existiert) geht dann in Richtung dessen, was ich mit der Tool-Unterstützung im ersten Beitrag andeuten wollte. Es gibt da auch Forschungsarbeiten dazu, aber ... eben kein Eclipse-Plugin  (Wobei ... wenn man danach suchen würde, würde man wahrscheinlich eins finden.... ich glaube es gibt sogar Eclipse-Plugins mit denen man MineSweeper spielen kann  )


----------



## Guest2 (5. Sep 2012)

Marco13 hat gesagt.:


> Was genau du mit IT-Tests meinst, ist mir nicht ganz klar.



IT-Test steht für Integrationstest. Ein Unit-Test sollte immer nur eine möglichst kleine in sich abgeschlossene Einheit testen und dabei unabhängig von anderen Einheiten bleiben. Der IT-Test hingegen testet die Einheiten in ihrem Zusammenspiel oder das Ganze als Gesamtheit.

Beispiel: Angenommen Du hättest diesen Code:


```
public class Business {

    private final Database db = new Database();


    public void doit(final int id) {

        db.doit(id);

    }

}
```

Und Du willst Business.doit() testen. Jetzt könntest Du vermutlich tausende Unit-Tests schreiben, um alle Fälle abzudecken. Das wäre aber Unsinn, schließlich ist Database.doit() bereits mit seinen eigenen Unit-Tests getestet, Du kannst also beim testen von Business.doit() davon ausgehen das Database.doit() korrekt ist. Das maximale was Dich also interessiert ist das Database.doit() aufgerufen wird. Entsprechend sieht der Unit-Test aus (mit jmockit):


```
public class TestBusiness {

    @Tested
    Business business;

    @Capturing
    Database db;

    @Test
    public final void testGet() {

        final int id = 42;

        business.doit(id);

        new VerificationsInOrder() {  {

                db.doit(id);

        } };

    }

}
```

In der jmockit Einführung gibt es ein Beispiel, das komplexer ist und damit wohl näher an der Wirklichkeit.

Bei einem einzigen Fehler im Code sollte im Idealfall auch nur ein einziger Unit-Test fehlschlagen. Wohingegen vermutlich gleich alle IT-Tests fehlschlagen würden.

Als Beispiel für einen IT-Test kannst Du z.B. auch Dein JOCLSample.java nehmen. Dort must Du nur ~3 Zeilen ändern, um es zu einem IT-Test zu machen. Gleichzeitig deckt der IT-Test dann aber 18,3% deines Java Codes ab (ja, ich habs getestet )!  

Viele Grüße,
Fancy


----------



## maki (5. Sep 2012)

Ach ja, zum Thema Testabdeckung:
Bitte nicht allzu ernst nehmen 

These: 
Man kann 100% Testabdeckung haben, ohne ein einziges Assert... Abdeckung heisst ja nur, dass da mal ein Thread durchgelaufen ist, nicht nicht dass da auch was getestet/überprüft wurde.....


----------



## Marco13 (5. Sep 2012)

Ach, ein *I*ntegra*T*ions-Test  Sicher braucht man sowas am Ende auch noch - mit verschiedenen Schattierungen, je nachdem, ob es um eine Bibliothek oder Anwendung geht und so. Aber gerade für die öffentlichen Teile einer API (mit Interfaces, für die potentiell auch andere Implementierungen schreiben können) wären allumfassende Unit-Tests schon nicht schlecht. (Ich hatte das vor längerer Zeit schonmal gedacht: Da wollte ich verschiedene Arten von Listen implementieren (SchuffledList etc) und da wären verbindliche Unit-Tests, die die Collection- bzw. List-Kontrakte prüfen, nicht schlecht gewesen). 

Inwieweit sowas wie JMockit für eine Bibliothek in diesem Sinne geeignet ist, muss ich mir näher ansehen. Aber die grundsätzliche Möglichkeit, auch Unit-Tests zu "schachteln", kam mir auch in den Sinn

```
void testEvenNumbers() {
    assertAllEven(thing.getEvenNumbers());
    ...
    ExistingCollectionTests.assertUnmodifiableList(thing.getEvenNumbers()); 
}
```
aber ich könnte mir vorstellen, dass man Gründe nennen kann, warum das "nicht gut" ist...

[ot]
Das JOCLSample erreicht schon 18.7%? Das macht doch gar nichts... (Und ... der interessante Teil ist sowieso der JNI-Teil - und natürlich das Verhalten, das in diesem Sample NICHT vorkommt  )
[/ot]


----------



## Guest2 (5. Sep 2012)

[ot]





Marco13 hat gesagt.:


> Das JOCLSample erreicht schon 18.7%? Das macht doch gar nichts... (Und ... der interessante Teil ist sowieso der JNI-Teil - und natürlich das Verhalten, das in diesem Sample NICHT vorkommt  )



Ja genau, obwohl da nicht soviel passiert, wird trotzdem schon ein beachtlicher Teil getestet. Beim IT-Test wird der JNI-Teil ja auch mitgetestet nur weis ich nicht, wie man da die Testabdeckung messen kann .

Gerade das JOCLSample ist schön, da Du darin das CPU-Ergebnis mit dem GPU-Ergebnis vergleichst. Dadurch hast Du dann auch nicht das von Maki angesprochene Problem, sondern stellst sicher das beide zum selben Ergebnis kommen (was dann auch vermutlich stimmt).

Imho sind bei Wrappern IT-Tests auch die einzig sinnvolle Möglichkeit zu testen, da man sonnst den Treiber Mocken müsste und das könnte schnell aufwendiger werden als der Wrapper selbst.[/ot]

Deshalb mag ich auch IT-Tests, man erreicht sehr einfach und sehr schnell eine Testabdeckung, bei der man davon ausgehen kann, dass das im Grunde schon so passt. Gleichzeitig ist das, was in einem IT-Test passiert, normalerweise das Szenario wie der Nutzer die Software auch einsetzt. 

Der Nachtteil von einem IT-Test ist allerdings auch das bei einem Test der ~20% des Codes abdeckt im Fehlerfall auch der Fehler in (mindestens) 20% des Codes gesucht werden muss. Beim Unit-Test hingegen sind es im vielleicht nur 0,000001%.   

(Neben dem Nachteil das IT-Tests in der Ausführung länger dauern und seltener ausgeführt werden, aber dafür gibt es ja Continuous-Integration )

Viele Grüße,
Fancy


----------



## Marco13 (5. Sep 2012)

[ot]


Guest2 hat gesagt.:


> Imho sind bei Wrappern IT-Tests auch die einzig sinnvolle Möglichkeit zu testen, da man sonnst den Treiber Mocken müsste und das könnte schnell aufwendiger werden als der Wrapper selbst.



Hmja, der einzige Zusammenhang in dem mir "Mocken" bisher "hautnah" untergekommen ist, war genau das, in jcuda testing - Byte-Welt Forum , aber wirklich intensiv damit beschäftigt habe ich mich nicht. Gerade bei JOCL/JCuda sind wegen der "Hardwarenähe" die klassischen Unit-Tests auch nur schwer anwendbar: Man kann nicht testen, ob Speicher wirklich auf die Grafikkarte kopiert wurde - außer wenn man ihn wieder zurückkopiert und vergleicht - joa, kann sinnvoll sein, wirkt aber ein bißchen gestelzt...

Da macht sowas wie das JOCLSample IMHO auch mehr Sinn, weil ich finde, dass bei solchen Samples der Vorteil einerseits darin besteht, dass man Testen kann, und das auch bei "komplexeren" Dingen, aber vor allem weil die Samples die Ausgangsbasis für andere Entwicklungen sind. Ein 100-Zeilen-HelloWorld-Sample sagt (gerade bei sowas wie OpenCL) vieeel mehr, als die etlichen 100 KB (!) JavaDoc aus der CL-Klasse  
[/ot]



_Der Nachtteil von einem IT-Test ist allerdings auch das bei einem Test der ~20% des Codes abdeckt im Fehlerfall auch der Fehler in (mindestens) 20% des Codes gesucht werden muss. _

Das fände ich gar nicht soo schlimm. Fehler sind ja nicht die Regel (*duck*) und speziell bei Java sieht man ja ggf. recht leicht, "in welcher Zeile die Exception fliegt" und so. Was ich (im Hinblick auf den ursprünglichen Anlass für diesen Thread) kritischer finde, ist, dass durch einen "IT-Test" zwar vielleicht ein großer Teil abgedeckt wird, aber im Gegensatz zum Unit-Test eben nur auf _genau eine_ Weise - und zwar die gutmütige, von der man weiß, dass sie funktioniert. Ich hab' mal den Spruch aufgeschnappt: Software testen heißt, zu versuchen, einen Fehler zu provozieren. Was passiert, wenn man bei einer Methode "null" übergibt, oder wenn man die Methode "useFoo" VOR "initializeFoo" aufruft? Und passt das, was da passiert, zur Spezifikation? Eine API in diesem Sinne auf "Robustheit" oder einfach "Spezifikationskonformität" zu prüfen ist nur mit Unit-Tests möglich - aber eben so aufwendig, dass man es in der Praxis offenbar kaum machen kann..


----------



## Spacerat (5. Sep 2012)

Marco13 hat gesagt.:


> Ich hab' mal den Spruch aufgeschnappt: Software testen heißt, zu versuchen, einen Fehler zu provozieren. Was passiert, wenn man bei einer Methode "null" übergibt, oder wenn man die Methode "useFoo" VOR "initializeFoo" aufruft? Und passt das, was da passiert, zur Spezifikation? Eine API in diesem Sinne auf "Robustheit" oder einfach "Spezifikationskonformität" zu prüfen ist nur mit Unit-Tests möglich - aber eben so aufwendig, dass man es in der Praxis offenbar kaum machen kann..


Das hat, so wie ich das inzwischen mitbekommen habe, nichts mehr mit Unit-Tests zu tun, sondern ist genau jenes Verfahren, die "Verwundbarkeit" einer Software aufzuspüren (siehe, mein Post mit den Exploits). Mein Fazit also: Unit-Tests werden vom Entwickler vor der eigentlichen Implementierung erstellt und Integrations-Tests werden während der Entwicklung einer SW nacheinander auf grün getestete Units durchgeführt. Unit- und Integrationstests beinhalten keinen Robustheitstest. Das letzte ist nun eher das, was ich mir bisher unter "Unit-Tests" vorstellte. Eine Unit arbeitet ja auch noch dann spezifikationskomform wenn sie Dinge zulässt, die in dieser Spezifikation nicht vorgesehen oder gar bedacht wurden. Das ist genau der Punkt, den Entwickler selber gar nicht testen können nur User probieren hartnäckig aus, was alles geht um Barrieren zu überwinden.


----------



## mvitz (5. Sep 2012)

Außerdem ist das was Marco vermutlich möchte - nämlich praktisch 100% Testabdeckung für ein Interface, für das es potentiell N Implementierungen gibt - ein ähnliches Problem wie mit Spezifikationen für ganze Programme/Module.

Man kann für diese unendlich viel Zeit aufwenden und anschließend sind sie leider doch nicht 100% korrekt. Dies gillt sogar für Spezifikationen die nur einen Use-Case beinhalten häufig, wen man nun also versucht im vorhinen alle Möglichkeiten zu betrachten, kann meiner Meinung nach nie etwas entstehen, was zu 100% zutrifft.

Aus diesem Grunde denke ich, dass es einfach nicht praktikable ist Unit-Tests (für ein Interface) zu schreiben, die alle Möglichkeiten abdecken und wenn man es versucht, dann kommen eben sehr große Tests heraus, die dann durchaus mehr Code haben als die ein oder andere Implementierung selber.


----------



## mvitz (5. Sep 2012)

Marco13 hat gesagt.:


> ...
> Das fände ich gar nicht soo schlimm. Fehler sind ja nicht die Regel (*duck*) und speziell bei Java sieht man ja ggf. recht leicht, "in welcher Zeile die Exception fliegt" und so. ...



Hier musst du allerdings beachten, dass das Argument natürlich nur für Exceptions gillt, wenn allerdings zum Schluss "lediglich" ein falscher Wert heraus kommt, dann musst du dich schon durch die kompletten 20% hangeln ;-)


----------



## Marco13 (5. Sep 2012)

@Spacerat: Wenn du so von "Exploits" redest klingt das immer nach Böswilligkeit - aber ich bin mir gar nicht sicher, ob du das meinst (ich meine es jedenfalls nicht  ). Es geht nur darum, dass eine Implementierung eines Interfaces, die nicht der Spezifikation entspricht schlicht _falsch_ ist, und eben nicht (wie es eigentlich sein sollte) überall dort verwendet werden könnte, wo man das interface eigentlich verwenden kann - weil sich derjeinge, der das interface verwendet, eben ggf. auf den Vertrag verläßt. 

Mal noch ein willkürlich-suggestives Beispiel: Bei einer Map muss 
V Map#put(K key, V value)
den Wert zurückgeben, der vorher unter dem gegebenen Key gespeichert war. Irgendjemand legt dort z.B. irgendwelche Streams rein, und verläßt sich auf das angegebene Verhalten:

```
Map<String, OutputStream> map = ...
...

OutputStream oldStream = map.put(id, newStream);
if (oldStream != null) oldStream.close();
```
Das funktioniert. Wenn dort nun aber eine blöde Map-Implementierung verwendet wird, die dort immer "null" zurückgibt, dann bleiben die alten Streams, die aus der Map geflogen sind, ungeschlossen, was früher oder später zu Problemen führt. 

Ein Unit-Test, den jemand für "Map" schnell aus dem Ärmel schütteln würde, würde vielleicht prüfen, ob das Element nach dem "put" enthalten ist und so, aber ... er sollte/müßte sowas eigentlich auch abprüfen:

```
Map<Key, Value> map = createMap();
Key key = new Key();
Value value0 = new Value();
Value value1 = new Value();
Value oldValue0 = map.put(key, value0);
assertNull(oldValue0);
Value oldValue1 = map.put(key, value1);
assertEquals(oldValue1, value0);
```
Aber sowas für jedes noch so kleine (aber eben vielleicht bei der Verwendung extrem wichtige!) Detail zu schreiben ist kaum machbar. 

So, und _nachdem_ ich das geschrieben habe, habe ich nochmal in die MapInterfaceTest.java - google-collections - Google Collections Library - Google Project Hosting geschaut  Dort steht es, ab Zeile 967

```
public void testPutExistingKey() {
    final Map<K, V> map;
    final K keyToPut;
    final V valueToPut;
    try {
      map = makePopulatedMap();
      valueToPut = getValueNotInPopulatedMap();
    } catch (UnsupportedOperationException e) {
      return;
    }
    keyToPut = map.keySet().iterator().next();
    if (supportsPut) {
      int initialSize = map.size();
      map.put(keyToPut, valueToPut);
      assertEquals(valueToPut, map.get(keyToPut));
      assertTrue(map.containsKey(keyToPut));
      assertTrue(map.containsValue(valueToPut));
      assertEquals(initialSize, map.size());
    } else {
      try {
        map.put(keyToPut, valueToPut);
        fail("Expected UnsupportedOperationException.");
      } catch (UnsupportedOperationException e) {
        // Expected.
      }
    }
    assertInvariants(map);
  }
```
Es IST natürlich machbar  Aber wie schon gesagt: Der Aufwand ist enorm, und ich kann mir kaum vorstellen, dass jemand mit Sicherheit sagen (geschweige denn _beweisen_) könnte, dass dort alle diese Fälle abgedeckt sind... auch wenn das obige Beispiel und Methodennamen wie [c]testValuesRetainAllNullFromEmpty[/c] schon darauf hindeuten, dass sich da jemand SEHR (SEHR!) viel Mühe gegeben hat


----------



## Spacerat (5. Sep 2012)

Ob nun böswillig oder nicht, sei mal dahingestellt. Es gibt Leute die machen's von sich aus und Leute die den Auftrag haben, den anderen zuvor zu kommen. "Ein Unit-Test bestimmt die Implementierung", deckt dabei aber nicht die Möglichkeiten des "an der Spezifikation vorbei" verwendens ab, die durch diese entstehen oder entstehen könnten. Wenn es z.B. nicht in der Spezifikation der Map steht, dass der zuvor enthaltene Wert zurückgegeben wird (eigentlich Möglichkeitsform, denn faktisch soll er ja lt. Spec zurückgegeben werden), dann muss er auch nicht zurückgegeben werden. Der Unit-Test schliesst dann grün und das, was man mit dem ungeschlossenen Stream nun so alles anstellen kann, muss der, der diese Lücke aufgetan hat, auch erst noch herausfinden. Das ist der feine Unterschied zwischen Spezifikation und (ihrer) Verwundbarkeit, also zwischen Unit-, Integrations- und Robustheits- bzw. Negativtest (frisch ergoogelt ).
Evtl. verwechselst du ja, wie ich zuvor auch, immernoch Unit- mit Negativ-Tests.


----------



## Antoras (5. Sep 2012)

Marco13 hat gesagt.:


> Aber sowas für jedes noch so kleine (aber eben vielleicht bei der Verwendung extrem wichtige!) Detail zu schreiben ist kaum machbar.


Sollte die Frage nicht eher lauten was die extrem wichtigen Details sind? Mit genügend Erfahrung weiß man doch was der eigene Code leisten können sollte. Eine kleine, übersichtliche und seiteneffektfreie Funktion muss deutlich weniger getestet werden als eine komplette Klasse, die aus nichts als Seiteneffekten besteht.
Die von dir verlinkte Testklasse testet die typischen Funktionen einer Map - ich sehe nicht was daran sonderlich aufwendig sein soll. Die Größe des Codes kommt von ständigen Überprüfungen auf Null-Werte und vom Fangen von Exceptions. Würde man das weglassen hätte man doch vollkommen übersichtlichen Testcode, der nur die einzelnen Funktionen einer Map testet?

Dass man das nicht weglassen kann liegt aber an Java und nicht am Testcode. Wäre der Java-Compiler typsicherer würde er es überhaupt nicht zulassen Nullable types in die Map einzufügen. Und das ständige Werfen von Exceptions macht die Sache auch nicht einfacher. Hätte man dann noch weniger Möglichkeiten überall Seiteneffekte zu produzieren müsste man auch weniger testen, weil die Menge der Fehlerfälle weniger wäre. 100% korrekten Code kann man vermutlich nie erreichen mit purem Code kommt man aber schon deutlich näher ran.

Schon mal daran gedacht, ob es nicht sinnvoller Wäre zu verbieten, dass eine Schnittstelle z.B. eine Exception werfen oder Null-Werte entgegennehmen darf, anstatt es zu erlauben und sich dann Gedanken machen zu müssen wie man dann darauf reagieren muss?


----------



## Spacerat (6. Sep 2012)

Antoras hat gesagt.:


> Schon mal daran gedacht, ob es nicht sinnvoller Wäre zu verbieten, dass eine Schnittstelle z.B. eine Exception werfen oder Null-Werte entgegennehmen darf, anstatt es zu erlauben und sich dann Gedanken machen zu müssen wie man dann darauf reagieren muss?


Tut man das an entsprechenden Stellen wo es erforderlich ist nicht sogar schon? Wenn man es in der Doku einer Schnittstelle spezifiziert, wird die Schnittstelle darauf hin doch schon eingeschränkt. Einschränkungen aber bedeuten doch weniger "Freiheit" bei der Implementation. Evtl. macht's mehr Sinn, so etwas in der konkreten Implementation, welche letztendlich ja auch getestet wird, zu spezifizieren ohne dabei die Spezifikation der Schnittstelle zu verletzen ("even more specified"). Wenn dann einer versucht, die Schnittstelle negativ zu testen, tut er dies ohne Kenntnis der tatsächlichen Implementation und scheitert (hoffentlich) evtl. genau deswegen. Unit-Tests hingegen sind alle weiterhin grün.


----------



## Marco13 (6. Sep 2012)

@Spacerat: _"Wenn es z.B. nicht in der Spezifikation der Map steht, dass der zuvor enthaltene Wert zurückgegeben wird ... dann muss er auch nicht zurückgegeben werden. Der Unit-Test schliesst dann grün und das, was man mit dem ungeschlossenen Stream nun so alles anstellen kann, muss der, der diese Lücke aufgetan hat, auch erst noch herausfinden."_

Ja. Es steht aber da. Aus gutem Grund. Und jeder, der das liest, denkt, dass er sich darauf verlassen kann. Und wenn jemand eine Map implementiert, die sich anderes (also falsch) verhält, kann das unabsehbare Folgen haben. Deswegen wäre es IMHO gut, wenn man es testen würde, bevor man seine Map-Implementierung auf die Menschheit losläßt  Wenn dort stünde: "This method may return arbitrary values" dann wäre klar, dass man sich da auf nichts verlassen kann, und das wäre auch in Ordnung.
...:reflect:
assertIsArbitrary(map.put(k)); 
:joke:


@Antoras: _"Schon mal daran gedacht, ob es nicht sinnvoller Wäre zu verbieten, dass eine Schnittstelle z.B. eine Exception werfen oder Null-Werte entgegennehmen darf, anstatt es zu erlauben und sich dann Gedanken machen zu müssen wie man dann darauf reagieren muss? _ 

An einigen Stellen hat man beim Design Freiheitsgrade. Es gibt einige APIs wo ganz pauschal drübersteht: "None of the arguments of any method may be 'null'", und dann braucht man sich darum auch keine weiteren Gedanken zu machen. Zugegeben, das Beispiel (bei dem ich sowas als Test auf "Robustheit" bezeichnet habe) war so gesehen etwas unpassend, bzw. hat vielleicht (auch wegen des Satzes mit dem "Versuch Fehler zu provozieren") etwas falsches suggeriert. Es ging mir nicht darum, eine API absichtlich falsch zu verwenden, und zu erwarten, dass sie auf die gutmütigst-mögliche Weise reagiert  sondern wirklich nur darum, alles zu überprüfen, was von der API zugesichert wird. So gesehen gibt es für ein (wieder mal sehr einfaches und suggestives) Beispiel drei Möglichkeiten:

```
1. : /** @param key The key. This may never be null */
2. : /** @param key The key */
3. : /** @param key The key. This may be null. */
```
Im 1. Fall muss man den Aufruf mit 'null' nicht in den Unit-Test packen, weil es nicht zugesichert wurde.
Im 2. Fall weiß man nicht, was man machen soll. Der Kommentar ist so gesehen "schlecht", aber bei sowas kann/sollte man wohl davon ausgehen, dass es eigentlich der 1. Fall ist
Im 3. Fall muss man prüfen, ob die Methode einen Aufruf mit 'null' übersteht. Wenn sie das nicht tut, ist sie schlicht falsch implementiert.

(Der Kommentar weiter oben bezog sich nur auf die angesprochenen Integrationstests, wo man so eine Methode wohl eher nicht mit "null" aufrufen würde, sondern NUR mit einem richtigen Key - auch wenn zugesichert ist, dass es auch mit "null" funktionieren sollte)


----------



## Spacerat (6. Sep 2012)

Marco13 hat gesagt.:


> Ja. Es steht aber da. Aus gutem Grund. Und jeder, der das liest, denkt, dass er sich darauf verlassen kann. Und wenn jemand eine Map implementiert, die sich anderes (also falsch) verhält, kann das unabsehbare Folgen haben. Deswegen wäre es IMHO gut, wenn man es testen würde, bevor man seine Map-Implementierung auf die Menschheit losläßt  Wenn dort stünde: "This method may return arbitrary values" dann wäre klar, dass man sich da auf nichts verlassen kann, und das wäre auch in Ordnung.


Stimmt. Es steht da und man sollte es auch testen. Den tut man es nicht, verstösst man gegen ein Spezifikation und ist damit ohnehin schon sehr viel früher als geplant dem Wohlwollen dubioser User ausgeliefert. Glücklicherweise gibt es dafür ja Unit-Tests, welche man halt schreibt, bevor man mit der Implementation beginnt. XD
Ich bin kürzlich im übrigen über eine derartige (Un)Spezifikation gestolpert.

```
// AudioInputStream (Auszug)
    /**
     * Obtains the length of the stream, expressed in sample frames rather than bytes.
     * @return the length in sample frames
     */
    public long getFrameLength() {
        return frameLength;
    }
```
Die Frage: Darf man hier 0 oder gar negative Werte zurückgeben? Für die Antwort - welche wohl "aber sicher doch" lautet, meines Erachtens aber "NEIN!" lauten müsste - könnt ihr euch ja mal z.B. JLayers MP3SPI ansehen - dort ist die frameLength stets unspezifiziert.


----------



## Marco13 (6. Sep 2012)

Falls der subtile Hauch von Ironie, den ich da zu erkennen glaubte, wirklich vorhanden war: Jaja, man wird doch wohl noch fragen dürfen  



Spacerat hat gesagt.:


> Die Frage: Darf man hier 0 oder gar negative Werte zurückgeben? Für die Antwort - welche wohl "aber sicher doch" lautet, meines Erachtens aber "NEIN!" lauten müsste - könnt ihr euch ja mal z.B. JLayers MP3SPI ansehen - dort ist die frameLength stets unspezifiziert.



Ja, genau das ist sie: AudioSystem#NOT_SPECIFIED (also -1). Wie wenn man diesen Konstruktor aufruft:


> public AudioInputStream(TargetDataLine line)
> Constructs an audio input stream that reads its data from the target data line indicated.
> The format of the stream is the same as that of the target data line, and the length is
> AudioSystem#NOT_SPECIFIED.


Aber es stimmt, dass sowas schon dabei stehen sollte....


----------



## JohannisderKaeufer (6. Sep 2012)

Ein Test der gegen ein Interface (Map) erstellt wird um damit verschiedene Implementierungen (HashMap, LinkedHashMap) zu testen ist KEIN Unit-Test im eigentlichen Sinne.

Warum?
Wenn ich das V-Model als Grundlage nehme dann habe ich in etwa dieses Vorgehen (etwas vereinfacht)

```
Anforderungsanalyse  |      Acceptance-Tests
  Systementwurf      |    Integrations-Tests
    Implementierung  |  Unit-Tests
```

Von links oben nach mitte unten und von dort nach rechts oben.

Ein Unittest soll eine Einzelne Klasse testen. Abhängigkeiten werden in der Regel gemockt.
Ein Integrationstest testet eine Komponente, also das Zusammenspiel zwischen den einzelnen Klassen. Abhängigkeiten werden nur noch in sofern gemockt, wie es nötig ist. z.B. wenn Paypal eingebunden wird, dann wird das gemockt, da ja nicht unbedingt echtes Geld fließen soll.
Ein Acceptance-Test beinhaltet alles, was der Kunde in der Anforderungsanalyse zum besten gibt, was die Software letztendlich können soll.

Alle Punkte kann man Manuell testen.  Hat man früher auch so gemacht, da hat dann ein Praktikant oder Werksstudent eine Liste bekommen, vielleicht sogar Excel, da Stand dann drin was getestet werden soll und wie die Eingaben sind und was rauskommen soll.

Alle Punkte kann man oftmals auch automatisch testen. Eine Beispielconfiguration.
Unit-Tests,  JUnit + Mocking. Testen auf Klassenebene
Integrations-Tests, JUnit, Testen auf Komponentenebene
Acceptance-Tests, Cucumber, Testen auf Systemebene, Einzelne Use-cases.

Mit Cucumber kann man auch Behavior-Driven-Development machen. Cucumber abstrahiert einerseits und kann intern z.B. auch JUnit nutzen.

Ein Beispiel für ein Cucumber Feature - Use-Case


```
Feature: Serve coffee
  In order to earn money
  Customers should be able to 
  buy coffee at all times

  Scenario: Buy last coffee
    Given there are 1 coffees left in the machine
    And I have deposited 1$
    When I press the coffee button
    Then I should be served a coffee
```

Intern wird hinterlegt was "Given there are 1 coffes left in the machine" bedeutet und dann bei der Ausführung wird dann daraus ein entsprechender JUnit-Test generiert. Man könnte allerdings auch direkt in JUnit Acceptance-Tests implementieren.

Und genau hier würde ich einen Test wie den zu Anfangs genannten (Map -> MapImpl) einordnen, zu den Acceptance-Tests. Und bei einem Acceptance-Test wird nicht überprüft ob die Implementierung korrekt ist, sondern ob die Anforderungen erfüllt sind.


Ein Wort zu Integrationstests die für hohe Testabdeckung sorgen. Wenn zwei Komponenten falsch implementiert sind, kann sich ein Fehler aufheben. Wenn zwei Komponenten fälschlicherweise ein Falsches Vorzeichen zurückgeben und eine andere Komponente diese Werte multipliziert, so kommt ein korrektes Ergebnis raus. Wenn der Fehler nun auffällt und an einer Stelle behoben wird, dann wird das Endergebnis falsch. 


Dann wurde hier oft noch die Komplexität die entsteht, wenn möglichst umfangreich getestet werden soll erwähnt.
Um die Komplexität zu reduzieren, gibt es ein paar Vorgehensweisen, sie wird aber immer in einer Größenordnung bleiben, die ein komplettes Testen unmöglich macht.
Wenn man komplett testen könnte, dann hätte man bereits eine Spezifikation auf Grund derer man ein Programm schreiben könnte, das in endlicher Zeit eine möglichst Gute Implementierung für das beschriebene Problem liefert.
(Kam so ähnlich mal auf BR-alpha, ein Prof. von der LMU? (jedenfalls München))

Man kann nur soweit Testen um zu der Entscheidung zu kommen, "*Setze ich diese Software ein oder lass ich es bleiben*" und das sind letztendlich Acceptance-Tests und nicht Unittests.


----------



## bygones (6. Sep 2012)

wie schoen zu sehen, dass dies ein tolles Bsp ist wie man vom Gedanken eines Unit-Tests auf Design Fragen, Probleme und Diskussion kommt  Was so alles ein einfacher Test bewirken kann...

@Marco
du meintest mal Fehler seien nicht die Regel und werden ja dank Java schnell aufgezeigt (also im Sinne von "wo in welcher zeile). Tests sind fuer die Sicherstellung, dass solche Fehler nicht auftreten. Es bringt dem Kunden nix wenn er von Fehler in Fehler rennt und du im sagen kannst "ach zum Glueck seh ich wo er auftritt".
Gut moeglich dass diese Aussage aus dem Kontext gerissen wurde...


----------



## FArt (6. Sep 2012)

Marco13 hat gesagt.:


> Das klingt realistisch und praktikabel - aber (sorry) eben auch ein bißchen "hilflos". Natürlich kann einem kein Kochbuch das Denken abnehmen - jeder Entwickler hat da eine Verantwortung, die er wahrnehmen muss. Aber es gibt bei dieser Beschreibung einige Freiheitsgrade, die im schlimmsten Fall ausgenutzt werden können, und die Unit-Tests ad absurdum führen.
> 
> 
> ```
> ...


----------



## Spacerat (6. Sep 2012)

FArt hat gesagt.:


> Du führst den Test mit diesem Beispiel ad absurdum...


Das gibt mir grad' viel zu Denken, was mir vorher noch gar nicht so aufgefallen ist... also nicht der Inhalt oder Korrektheit dieser Aussage sondern...
@Marco13: Willst du Unit-Tests an fremden APIs ausführen? Lass das! Das haben andere (eben die Entwickler dieser APIs) schon getan oder gelassen. Du kannst darauf nur noch Negativ-Tests loslassen und den Entwicklern evtl. zeigen, wo sie sich irrten.


----------



## maki (6. Sep 2012)

> @Marco13: Willst du Unit-Tests an fremden APIs ausführen? Lass das! Das haben andere (eben die Entwickler dieser APIs) schon getan oder gelassen. Du kannst darauf nur noch Negativ-Tests loslassen und den Entwicklern evtl. zeigen, wo sie sich irrten.


Im TDD Umfeld nennt man solche Tests "learning tests", bei einer neuen API schreibt man sowieso "tests", in diesem Falle eben Unittests.
Hat zB. den Vorteil dass man gleich Beispeile wie eine API zu verwenden bzw. was man glaubt wie eine API zu verwenden ist und wie sie sich zu verhalten hat.
Bei einem API Update können solche Tests einem vielleciht(?) auch sagen ob ein Upgrade sicher wäre..


----------



## Marco13 (6. Sep 2012)

@JohannisderKaeufer: Ein paar interessante Einordnungen und Definitionen (ich weiß, das hätte man sich auch alles selbst aneignen können, indem man 20 Bücher liest  aber... manchmal muss man Prioritäten setzen: In Anbetracht der Tatsache, dass es (in meinem Fall) keinen "Kunden" und keine "Vorgaben" und keine "Integeration" (im eigentlichen Sinn) gibt, reduziert es sich wirklich darauf, zu testen, ob ein Implementierung korrekt ist...)

Mit den zur Verfügung stehenden Infrastrukturen zu Acceptance-Tests und Cucumber kann ich im Moment nicht viel anfangen. Aber wenn man das mal auf eine für ein privates Projekt vielleicht angebrachte, pragmatische Ebene runterbricht, würde ich dazu tendieren, die Frage nach der Bennenung mal hinten an und das Ziel in den Vordergrund zu stellen - bezogen auf 

_"Man könnte allerdings auch direkt in JUnit Acceptance-Tests implementieren. ... Und genau hier würde ich einen Test wie den zu Anfangs genannten (Map -> MapImpl) einordnen, zu den Acceptance-Tests. Und bei einem Acceptance-Test wird nicht überprüft ob die Implementierung korrekt ist, sondern ob die Anforderungen erfüllt sind."_

Vielleicht etwas ... naiv formuliert: Auf rein _technischer_ Ebene (speziell da es um eine Bibliothek geht) sehe ich jetzt die einzige _Anforderung_ eben darin, dass die Implementierung korrekt ist 

[ot]
_"Wenn man komplett testen könnte, dann hätte man bereits eine Spezifikation auf Grund derer man ein Programm schreiben könnte, das in endlicher Zeit eine möglichst Gute Implementierung für das beschriebene Problem liefert."_

Da wird AFAIK auch schon dran geforscht: Aus formalen Anforderungsbereschreibungen Software zu generieren, die die Anforderungen erfüllt 
[/ot]


@bygones: _"du meintest mal Fehler seien nicht die Regel und werden ja dank Java schnell aufgezeigt"_
Da stand so ein "*duck*" dabei  Ich weiß, dass so eine Aussage leicht fehlinterpretiert werden könnte. Es ist nur i.a. so, dass in Java bestimmte Fehlerklassen (durch Exceptions) leichter zu entdecken sind, was bei anderen Sprachen u.U. nur "Corrupted Memory" zur Folge hätte. 


@FArt: "_1.) Eine (nicht statische) Methode steht nicht für sich alleine sondern lebt nur im Kontext der Klasse und somit im Lebenszyklus der Instanz. Es bringt somit schon sehr viel, wenn ich genau diese Paramter abprüfe: Liste, nicht null, unveränderbar. ... Die öffentliche Schnittstelle macht also noch viel mehr aus als nur die drei Angaben in der Beschreibung der Javadoc, sondern auch der technischen Dokumentation bzw. des technischen Konzepts."_

Dem stimme ich zu. Es bringt schon _viel_ die erste "Ebene" zu testen, aber es gibt noch _viel_, was dadurch nicht erfasst wird - das meinte ich mit den Fragen nach der angemessenen _Tiefe_ der Tests und der kombinatorischen Explosion, die durch die unterschiedlichen möglichen Aufrufreihenfolgen der Methoden entsteht - bzw. allgemein der möglichen Explosion der Größe des _Zustandsraumes_ eines Objektes, den man ja theoretisch komplett testen können wollte...

_"2.) Du gehts viel zu weit: du testest in diesem Test auch noch die Implementierung der unveränderlichen Liste._

Der Punkt wurde schon angesprochen: Um die Funktion zu überprüfen, die behauptet, sie gäbe eine unveränderliche Liste zurück, _müßte_ man eigentlich auf das zurückgegebene Objekt alle Tests anwenden, mit denen man man auch isoliert die Implementierung einer unveränderlichen Liste überprüfen würde - lapidar formuliert: Man weiß ja nicht, welche Implementierung man bekommt. 

Ganz konkret (aus einem anderen Zusammenhang, und noch SEHR in der Planung, also nicht zuuu ernst nehmen) : Ich habe den Fall, dass ich an einer Stelle eine Map zurückgeben möchte. Man kann bei dieser Map keine neuen Einträge hinzufügen. Also müßte "put" eine UnsupportedOperationException werfen. Es würde an dieser Stelle aber durchaus Sinn manchen, wenn man _existierenden_ keys in dieser Map neue Werte zuweisen könnte. Man könnte dort eine Implementierung zurückgeben, die genau das erlaubt. So eine Map würde aber bei einem gewissenhaften Unit-Test auf jeden Fall durchfallen, weil die 'put' Methode "nicht unterstützt" und gleichzeitig "nicht _nicht_ unterstützt" ist. 

@Spacerat: _"Willst du Unit-Tests an fremden APIs ausführen?"_ 
Nein. Es ist nur so, dass das, was ich entwickle, in mancher Hinsicht ähnlich zu den Collections ist, und ich auf der Suche nach "Inspiration" für geeignete Tests eben das Guava-Map-Test-Monster gefunden habe, wo jemand _genau das_ in einer Form gemacht hat, die ich für gut und richtig und eigentlich erstrebenswert, in der Praxis aber leider kaum so konsequent umsetzbar halte.


----------



## FArt (6. Sep 2012)

Marco13 hat gesagt.:


> In Anbetracht der Tatsache, dass es (in meinem Fall) keinen "Kunden" und keine "Vorgaben" und keine "Integeration" (im eigentlichen Sinn) gibt, reduziert es sich wirklich darauf, zu testen, ob ein Implementierung korrekt ist...).


Ganz und gar nicht. Du programmierst ja nicht ins Leere, sondern machst dir Gedanken über die Funktionalität und über die Anforderungen. Also im Prinzip bist du der Kunde, der erst mal definieren muss, was denn überhaupt rauskommen soll.
Ohne klares Ziel, wird kein klares Ergebnis heraus kommen, somit können auch keine klaren Tests geschrieben werden.
Das ist ok, solange man vor sich hin prototypt, aber dabei sollten dann langsam die Anforderungen entstehen, die man sofort in Tests gießt, bevor man weiter macht.



Marco13 hat gesagt.:


> Ganz konkret (aus einem anderen Zusammenhang, und noch SEHR in der Planung, also nicht zuuu ernst nehmen) : Ich habe den Fall, dass ich an einer Stelle eine Map zurückgeben möchte. Man kann bei dieser Map keine neuen Einträge hinzufügen. Also müßte "put" eine UnsupportedOperationException werfen. Es würde an dieser Stelle aber durchaus Sinn manchen, wenn man _existierenden_ keys in dieser Map neue Werte zuweisen könnte. Man könnte dort eine Implementierung zurückgeben, die genau das erlaubt. So eine Map würde aber bei einem gewissenhaften Unit-Test auf jeden Fall durchfallen, weil die 'put' Methode "nicht unterstützt" und gleichzeitig "nicht _nicht_ unterstützt" ist.


Und genau das ist gut so, wenn die Methode behauptet, die Map werde nicht modifizierbar.

Deine spezielle Map-Implementierung wird passend dokumentiert und natürlich getestet (put mit neuem Key -> Fehler; put mit altem Key -> Update). 
Eine Methode, die dieses Map zurückliefert, dokumentiert dies... und fertig.

Wenn jemand lediglich gegen Map arbeitet, kann es nunmal passieren, dass das eine oder andere nicht geht... ist aber auch beabsichtigt, denn das liegt in der Natur der Sache.


----------



## Marco13 (6. Sep 2012)

Die Anforderungen sind sehr abstrakt und allgemein. Bei den Collections hat sich ja auch keiner gedacht: "Ich will eine Lagerverwaltung implementieren", sondern "Ich definiere ein Interface zur allgemeinen Speicherung von Daten, für das es verschiedene Implementierungen geben kann (die sich aber alle genau so verhalten sollen, wie es in Interface definiert ist)". Und genau um letzteres zu prüfen, reicht es IMHO nicht, in allen Implementierungen einrach "nachzusehen"....


----------



## FArt (6. Sep 2012)

Marco13 hat gesagt.:


> Die Anforderungen sind sehr abstrakt und allgemein.


Das reicht noch nicht für eine Implementierung.



Marco13 hat gesagt.:


> Bei den Collections hat sich ja auch keiner gedacht: "Ich will eine Lagerverwaltung implementieren", sondern "Ich definiere ein Interface zur allgemeinen Speicherung von Daten, für das es verschiedene Implementierungen geben kann


Doch, ich denke schon dass es so ähnlich war. Anforderung: eindeutig Key/Value Paare speichern. Welche Methoden sind sinnvoll? Dann definiert man sinnvolle Implementierungen (HashMap, SynchronizedHashMap, ...). Dann definiert man, dass konkrete Implementierungen von diesem Verhalten abweichen können. Die Bedingungen sind am Interface erläutert. Das bedeutet, dass das Interface nicht das Verhalten wiederspiegeln muss, nur eine Schnittstelle für Zugriffe. Andere Sachen aber werden festgelegt (eben z.B. key/value Paare ohne Duplikate in den Keys).

Map (Java Platform SE 6)



Marco13 hat gesagt.:


> Und genau um letzteres zu prüfen, reicht es IMHO nicht, in allen Implementierungen einrach "nachzusehen"....


Wieso nachsehen?. Man schreibt Tests gegen konkrete Implementierungen (sprich der Entwickler der Klasse) und deren Verhalten. 

Definiert eine Methode als Rückgabewert Map und liefert eine bestimmte Implementierung, die u.U. nicht alle Funktionalitäten bietet, die man erwarten würde, dann ist das m.E. ein Fehler. Das gilt natürlich nicht, wenn das zu erwartende Verhalten dokumentiert ist.


```
Map getNotExtendableButModifyableData();
```

Wäre also ok (Außer evtl. die Namensgebung ;-) )
Da würde ich dann testen können, ob der Vertrag eingehalten wird.


```
NotExtendableButModifyableMap getTheMap();
```
Ist auch ok. Hier muss ich nicht noch testen, ob die Map das Verhalten einhält. Ich gehe davon aus, dass das passiert ist.


----------



## Marco13 (6. Sep 2012)

FArt hat gesagt.:


> Das reicht noch nicht für eine Implementierung.
> ...
> Anforderung: eindeutig Key/Value Paare speichern. Welche Methoden sind sinnvoll?



Ich meinte damit, dass die Anforderungen auf einer tieferen, technischen Ebene beschrieben sind - eigentlich "kurz davor", automatisch geprüft werden zu können: Statt einem Kommentar "[c]returns never null[/c] gibt's teilweise schon Annotations wie [c]@NeverNull[/c], die (fast ähnlich zu Eiffel Conditions) bis zu einem gewissen Grad automatisch (also noch autmatischer als Unit-Tests) überprüft werden können.

Die Möglichkeit, in der JavaDoc ggf. ein Verhalten zu beschreiben, das von dem abweicht, das man von dem zurückgegebenen Objekt _eigentlich_ erwartet werden würde, ändert nichts daran, dass man in einem "tiefen" Unit-Test dann ja eigentlich wieder _genau dieses_ Verhalten abprüfen müßte. Und das IST einfach aufwändig :bahnhof:


----------



## bygones (6. Sep 2012)

geht es nun darum ob man Zusicherungen im Code per Javadoc garantieren soll ?! Ich kenne nahezu keine JavaDoc die mit dem aktuellsten Stand der Implementierung einhergeht ;-).

Es ist wohl ein unterschied ob irgendwo etwas textlich dokumentiert ist oder ob es Systeme gibt, die eben dieses auch programmatisch ueberpruefen.

Und wenn du die Wahl hast zwischen JavaDoc oder UnitTest (da beides ja aufwaendig ist) -> immer UnitTest


----------



## FArt (6. Sep 2012)

Marco13 hat gesagt.:


> Ich meinte damit, dass die Anforderungen auf einer tieferen, technischen Ebene beschrieben sind - eigentlich "kurz davor", automatisch geprüft werden zu können: Statt einem Kommentar "[c]returns never null[/c] gibt's teilweise schon Annotations wie [c]@NeverNull[/c], die (fast ähnlich zu Eiffel Conditions) bis zu einem gewissen Grad automatisch (also noch autmatischer als Unit-Tests) überprüft werden können.


Ja, dann brauche ich natürlich keinen expliziten Test für die Prüfung auf ungleich null. Ähnlich könnte es sich auch mit Java-Asserts verhalten (sofern die VM während der Tests mit asserts enabled läuft).



Marco13 hat gesagt.:


> Die Möglichkeit, in der JavaDoc ggf. ein Verhalten zu beschreiben, das von dem abweicht, das man von dem zurückgegebenen Objekt _eigentlich_ erwartet werden würde, ändert nichts daran, dass man in einem "tiefen" Unit-Test dann ja eigentlich wieder _genau dieses_ Verhalten abprüfen müßte. Und das IST einfach aufwändig :bahnhof:


Das ist richtig und auch gut so. Denn (z.B. das Interface Map) verbirgt nicht nur die Implementierung, sondern auch das Verhalten. Somit muss es für diese Anforderung explizit überprüft werden.

Was mache ich, wenn ich das nicht möchte, aber trotzdem die Implementierung dahinter verbergen möchte? Dann sollte ich ein eigenes Interface schreiben, dass u.U. von Map ableitet und genau dieses Verhalten beschreibt. Die Implementierung kann dann dieses Interface implementieren und wird für sich getestet. Wird dieses Interface von einer Methode zurückgeliefert, brauche ich es nicht mehr testen. 

Und ja, saubere Tests schreiben ist aufwendig und langwierig. Ich verbringe sicher viel Zeit mit dem Schreiben von Tests. Mit Tests für konkrete Bugs dann evtl. sogar noch mehr. Aber ich habe auch gemerkt, dass wesentlich weniger Fehler gemeldet werden und vor allem die Wartung der Software wesentlich weniger zeitintensiv ist. Das rechnet sich schnell.


----------



## FArt (6. Sep 2012)

bygones hat gesagt.:


> Und wenn du die Wahl hast zwischen JavaDoc oder UnitTest (da beides ja aufwaendig ist) -> immer UnitTest


Die Wahl gibt es nicht... sowohl die Javadoc als auch die Testabdeckung wird im CI überprüft, und dann gibt es noch Reviews... da kommt keiner aus ;-)


----------



## Marco13 (6. Sep 2012)

@bygones: JavaDocs sind für manche Dinge eben die einzig "portable" Möglichkeit. Die Versuche, dem mit Annotations entgegen zu wirken gehen sicher in die richtige Richtung, sind aber bisher nur erste Schritte.



FArt hat gesagt.:


> Und ja, saubere Tests schreiben ist aufwendig und langwierig. Ich verbringe sicher viel Zeit mit dem Schreiben von Tests. Mit Tests für konkrete Bugs dann evtl. sogar noch mehr. Aber ich habe auch gemerkt, dass wesentlich weniger Fehler gemeldet werden und vor allem die Wartung der Software wesentlich weniger zeitintensiv ist. Das rechnet sich schnell.



Die Vorteile und das "für was" von Unit-Tests (im zum Threadtitel komplementären Sinn) sind mir schon klar. Und dass man diese Vorteile nicht umsonst bekommt auch. Aber nachdem ich in dem Guava Map-Test den Aufwand gesehen habe, der betrieben werden muss, um es _richtig_ zu machen, erscheint mir das aus den hier im Thread ausführlich besprochenen Gründen kaum praktikabel. So bleibt also nur, dass man versucht, im Pareto-Sinn die 80% (nicht Testabdeckung  ) zu erreichen...


----------

