Herzlich willkommen zur Vorlesung Programmieren, zur letzten Sitzung
in diesem Semester.
Ich werde ganz am Ende der Vorlesung natürlich noch ein paar
organisatorische Fragen beantworten, hoffentlich mit meinen Folien,
die ich mitgebracht habe.
Die habe ich schlauerweise ganz ans Ende der Vorlesung gestellt, damit
Sie auch noch alle aufmerksam zuhören in der Zeit vorher.
Aber natürlich sind auch die Themen, die wir heute noch in der letzten
Sitzung besprechen, so interessant, dass ich hoffe, dass das nicht der
einzige Grund ist, warum Sie aufmerksam zuhören werden.
Denn wir gucken uns heute noch mal etwas genauer das Finden und das
Beheben von Fehlern an.
Wir hatten ja schon eine Vorlesung, wo es ums Testen ging und ums
Definieren von Assertions.
Und heute soll es noch mal etwas allgemeiner darum gehen, wie man eben
mit welchen Strategien man Fehler finden und beheben kann.
Das erste Drittel ungefähr der heutigen Sitzung wird das Thema
Debugging und Fehlersuche einnehmen.
Debugging machen Sie sowieso immer auch schon selbst, wenn Sie ganz
kleine Programme schreiben und vielleicht einen Fehler finden, dann
suchen Sie natürlich nach der Ursache für diesen Fehler.
Aber heute sollen Sie eben kennenlernen, wie das Ganze auch mit
wissenschaftlicher Methode funktioniert, wie das auch in großen
Softwareprojekten funktioniert.
Und dazu habe ich eine Reihe von Tipps mitgebracht, die Ihnen bei der
systematischen Fehlersuche beim Programmieren helfen sollen.
Dann in den letzten beiden Dritteln der heutigen Veranstaltung wollen
wir uns dann noch mal genau ein Test-Framework für Java anschauen, was
für Sie ja interessant ist, nämlich JUnit, was Sie vielleicht auch
selbst schon verwenden.
Aber da gibt es einige schicke Features, von denen ich immer wieder
die Erfahrung mache, dass die viele Leute irgendwie leider nicht
kennen oder einfach nur nicht benutzt haben.
Deswegen möchte ich heute noch mal eine kleine Einführung geben, wie
JUnit so funktioniert, was da das Konzept dahinter ist, gerade auch
was Initialisierung und Finalisierung angeht und vor allen Dingen
parametrisierte Tests, denn damit kann man wirklich ganz coole Sachen
machen.
Dann sind dementsprechend die Lernziele heute auch, dass Sie eben
systematisch Programme analysieren können und dadurch eben Fehler in
Programmen finden können und das Ganze eben mit wissenschaftlicher
Methodik auch, die Sie auf das Debugging anwenden können.
Das ist also quasi der erste Teil und der zweite Teil ist eben, Sie
können das Test-Framework JUnit effizient verwenden, also so, dass Sie
wirklich auch mit geringem Aufwand eine große Menge von Dingen
abtesten können und dadurch auch natürlich Fehler finden und beheben
können.
Gut, dann kommen wir jetzt zum ersten Teil, wo es eben um Debugging
vor allem geht.
Dazu wiederum, wie üblich am Anfang ein Literaturhinweis.
Viele der Inhalte, die ich jetzt vorstelle im ersten Teil, stammen
eben aus diesem Buch von Steve McConnell und da aus dem Kapitel Nummer
23.
Am Anfang habe ich Ihnen nochmal dieses lustige Bild mitgebracht.
Sie haben es schonmal gesehen in einer vorherigen Vorlesung.
Ja, was sind nochmal Bugs?
Das hat eben Grace Hopper im Jahr 1945 irgendwie für die Software
eingeführt.
Den Begriff gab es schon vorher, aber so ein Bug ist eben irgendwo ein
Käfer, der da in dem Gerät umkrabbelt und deswegen dafür sorgt, dass
irgendwas nicht funktioniert.
Und verallgemeinert sind das natürlich jegliche Arten von Fehlern, bei
denen man erstmal nicht weiß, wo sie herkommen.
Deswegen schiebt man es auf die kleinen Tiere, auch wenn die
vielleicht gar nichts dafür können.
Ich habe Ihnen hier noch so ein Zitat, ein sogenanntes Zitat, denn es
hat keine richtige Quelle.
Da steht halt anonym mitgebracht.
Also wenn Debugging der Vorgang ist, die Fehler auszubauen, dann muss
Programmieren ja wohl der Vorgang sein, die Fehler einzubauen.
Und so ist es tatsächlich auch.
Die Bugs entstehen halt bei der Programmierung und das Debugging ist
dann eben ein Teil der Softwareentwicklung, ein späterer Teil, wo
diese Fehler dann wieder entfernt werden.
Manchmal, das ist vielleicht auch nur so ein bisschen als Motivation,
sagt man zu diesen verschiedenen Bugklassen auch Heisenbug und
Bohrbug.
Das kommt natürlich von Werner Heisenberg und Nils Bohr.
Der Heisenberg kennen Sie natürlich noch aus der Physik mit dem
Unschärfeprinzip.
Und so ganz stimmt die Analogie nicht.
Das ist halt ein bisschen quantenmechanisch gedacht.
Also der Heisenberg ist einer, wenn man ihn versucht anzugucken, dann
ist er auf einmal weg.
Das ist halt irgendwie so ein quantenmechanischer Effekt.
Durch Beobachtung ändere ich mein System.
Und wenn ich quasi den Debugger anschalte, verhält sich mein System
auf einmal anders, als wenn ich es normal durchlaufen lasse.
Der Fehler tritt nicht auf und verschwindet und ist deswegen ein
bisschen schwerer zu eliminieren.
Das kommt tatsächlich oft vor.
Die Gründe sind ganz verschieden.
Wenn es um Zeitverhalten geht, da ist manchmal der Debugger halt
anders.
Bei manchen Programmiersprachen funktioniert der Debugger auch
wirklich mit einer völlig anderen Semantik als das laufende Programm.
In Java zum Glück nicht.
Da geht es eigentlich ganz gut, aber auch da kann es vorkommen, dass
beim Debugging-Fehler auf einmal verschwinden.
Und der deterministische normale Bug, der tatsächlich erwartbar
überall dann auftritt, nennt sich dann eben nach Nils Bohr.
Der sogenannte Bohr-Bug.
Aber das ist jetzt auch nur ein bisschen Motivation.
Das sind so ein bisschen flapsige Begriffe.
Was wir auch schon kennengelernt haben, sind die verschiedenen
Terminologien, die um den deutschen Begriff Fehler mehr oder weniger
kreisen.
Und ich habe ihn hier nochmal auch schön durch eine Quelle belegt
aufgelistet, wie man die unterteilen kann.
Also hier unten... Moment, ich muss wieder meinen Laserpointer
anschalten.
Hier unten ist eben dieses Buch von Jean-Claude Laprie.
Der hat eben sehr schön eingeteilt, wie sich diese Begriffe
unterscheiden.
Und deswegen hier nochmal sozusagen jetzt als Erweiterung der
vorherigen Vorlesung die definitive Liste.
Also was ist ein Error?
Das ist irgendein Fehler, den man in der Entwicklung macht.
Den wohl irgendein Mensch in der Entwicklung macht, beziehungsweise
auch ein Fehler, der letztendlich, da kann man jetzt drüber streiten,
wird philosophisch, sind alle Errors irgendwie Mensch gemacht, ob es
jetzt Hardware oder Software ist.
Ja, dann ein Defekt ist es dann, wenn man ein Error gefunden hat.
Man merkt etwas stimmt nicht, dann bezeichnet man das als Defekt.
Und ein Bug ist dann tatsächlich ein Defekt, wo der Entwickler auch
sagt, ja stimmt, das ist ein Verhalten, was wir nicht erwartet haben.
Das kommt dann vielleicht als Bug-Report in irgendein System rein, so
Bugzilla, Jira, sonst noch wie, was man eben so hat.
Und deswegen, sobald es quasi anerkannt wurde und auch vielleicht dem
Ding eine Nummer verpasst wurde, nennt sich das Ganze dann der Bug, so
wie wir ihn gerade schon gesehen haben.
Ja, und dann haben wir eben die Begriffe Fault und Failure, die können
dann dadurch ausgelöst werden.
Der Fault eben ein Zustand, der durch diese Dinge hier ausgelöst wurde
und ein Failure ist dann tatsächlich, also auf deutsch ein Versagen,
ein kompletter Ausfall des Systems, wenn dann die Software sich
unerwartet verhält, beziehungsweise möglicherweise auch gar nichts
mehr macht.
Das Ganze ist dann ein Versagen des Systems.
Also wir wissen jetzt, der Bug, der ist irgendwo hier in der Mitte und
jetzt ist natürlich die Frage, was ist das Debugging?
Entwanzen würde man auf deutsch sagen, klar, aber da geht es jetzt
nicht um Kammerjäger, sondern es geht halt darum, diese Fehler
einerseits zu finden und sie andererseits zu beheben.
Ja, und normalerweise ist jetzt das Finden und Verstehen der größte
Teil des Aufwands.
Hier jetzt so ein bisschen über den Daumen gepeilt, 90%.
Das stammt eben aus diesem Buch, was ich anfangs zitiert habe.
Ich weiß jetzt nicht, wie gut es empirisch belegt ist, diese beiden
Zahlen, die hier auf der Folie sind, aber ich würde mal sagen, mit
meiner Erfahrung deckt es sich.
Erfinden und Verstehen des Fehlers ist meistens dann auch schon die
Hälfte vom Prozess des Verstandenhabens, wie man ihn behebt.
Denn wenn man mal weiß, warum der Fehler auftritt und den genau
lokalisieren kann, ist meistens die Korrektur dann einfacher.
Also es ist nicht immer einfach, aber meistens einfacher als der
Finden und Verstehensprozess.
Und das macht auch den Hauptunterschied aus, eben hier nach dem Autor,
von dem ich das übernommen habe, was einen effizienten Programmierer
zu einem ineffizienten Programmierer unterscheidet.
Das ist meistens nicht, dass der eine halt die Java-AP völlig
auswendig kennt, der andere muss immer nachschlagen.
Das ist es logischerweise nicht.
Nein, ein effizienter Programmierer kann vor allen Dingen gut debuggen
und kann durch wissenschaftliche Methoden auch die Fehler finden,
während der ineffiziente Programmierer da völlig unsystematisch
rangehen würde, deswegen viel länger braucht und möglicherweise auch
die Fehler gar nicht findet.
Das soll also ein wesentlicher Unterschied für den wesentlichen Faktor
ausmachen in der unterschiedlichen Produktivität von Entwicklern,
ungefähr sogar 1 zu 20.
Also können Sie sich vorstellen, unsystematisches Testen kann in
manchen Fällen sicher 20 Mal länger dauern als systematisches Testen.
Ja, jetzt habe ich ja schon gesagt, wir wollen hier mit der
wissenschaftlichen Methode rangehen, solche Fehler zu finden, deswegen
ja nochmal ganz kurz in fünf Stichpunkten hier zusammengefasst, was
verstehen wir denn eigentlich unter einer wissenschaftlichen Methode.
Also Wissenschaft basiert ja meistens auf Daten, die man irgendwie
gesammelt hat und diese Daten sollte man idealerweise darauf basieren,
dass man reproduzierbare Experimente macht.
Also wenn man ein Experiment macht, hat man hinterher irgendeinen
Datensatz, man hat irgendwas dadurch herausgefunden, aber das soll
möglichst eben reproduzierbar sein.
Wenn man das Experiment mehrere Male ausführt, kriegt man wenigstens
erwartbar ähnliche Daten oder sogar, wenn es völlig deterministisch
ist, dieselben Daten.
Und wenn man die sich dann angeschaut hat, formuliert man darüber eine
Hypothese.
Also so funktioniert dann eben empirische Wissenschaft.
Man schaut sich die Daten an und sagt jetzt, ich habe das und das
beobachtet, meine Hypothese ist, da steckt folgende Regel dahinter,
folgendes System, folgende Grundlage.
Genau, dann, wenn man diese Hypothese hat, kann man weitere
Experimente entwerfen, die dann diese Hypothese überprüfen sollen und
dann idealerweise kann man diese Hypothese überprüfen.
Was immer einfach ist, ist Falsifizierung.
Also wenn man ein Gegenbeispiel gefunden hat in einem Experiment, hat
man seine Hypothese schnell widerlegt.
Schwieriger ist es, eine Hypothese zu verifizieren, tatsächlich zu
beweisen, dass das stimmt und das werden wir dann auch sehen, wie man
das beim Testen machen kann.
Was auf jeden Fall immer hilft, ist, das Experiment zu wiederholen
oder auch weiterzuentwickeln und auch die Hypothese anzupassen.
Experiment wiederholen würde man jetzt als Informatiker sagen, ja beim
Physiker okay, der macht sein Experiment mehrmals, da kommen
unterschiedliche Sachen raus.
Aber hier in der Informatik ist doch alles logisch, ist doch alles
deterministisch.
Da muss doch eigentlich immer das Gleiche rauskommen.
Das ist natürlich erstmal irgendwie richtig.
Also die Prozesse sind wahrscheinlich alle deterministisch, aber man
hat halt nicht alle Informationen als Entwickler.
Man weiß nicht, wann die Java Virtual Machine sich entscheidet, eine
Garbage Collection zu machen.
Man kennt äußere Einflüsse nicht.
Was für ein Zustand hat der Rechner?
Was machen andere Benutzer zur gleichen Zeit auf dem Rechner?
Wie ist mein Durchsatz?
Was macht die Festplatte?
Solche Sachen können alle Einflüsse haben auf die Software und
deswegen ist es durchaus sinnvoll, das durch Experimente zu
wiederholen, um solche Fehler dann eben möglichst auszuschließen.
Wenn man so einen Fehler sucht durch Experimente oder durch eben die
wissenschaftliche Methode, muss man eben zuerst mal den Fehler
stabilisieren.
Da sage ich gleich auch noch etwas dazu, wie das funktioniert.
Also quasi wiederholbar zu diesem Fehler kommen und halt auch
eingrenzen können, in welchen Fällen der auftritt.
Das ist dann auch schon hier der zweite Teil.
Also die lokalisieren erst mal das Fehler, wo er auftritt, den
wiederholbar produzieren können und dann aber auch die Ursache dieses
Fehlers herausfinden.
Also welche Daten haben zu diesem Fehler geführt?
Dann mal sich diese Daten anschauen.
Was haben die für eine Eigenschaft?
Also erstmal eingrenzen ist der erste Punkt.
Erfassen.
Dann die sich anschauen.
Haben die Ähnlichkeit?
Kommt da immer gleiche Art von Daten drin vor?
Und dann letztendlich eben, ja, wie wir gerade gesehen haben,
Hypothese verfassen.
Hypothese in dem Fall, was löst den Defekt aus?
Und dann eben die Methode identifizieren, um diese Hypothese zu
überprüfen.
Und bei uns in der Softwaretechnik ist das Überprüfen halt vor allen
Dingen durch Tests, die manuell geschrieben, aber automatisch
ausgeführt werden.
Aber natürlich auch durch das manuelle Durchsuchen des Codes, zum
Beispiel in Code Reviews.
Ja, und dann muss durch eine dieser Methoden die Hypothese überprüft
werden.
Der Fehler wird behoben und dann wird wieder getestet, ob der Fehler
tatsächlich behoben wurde.
Der letzte Punkt ist auch wichtig.
Wenn man mal einen Fehler gefunden hat, kann man sich ähnliche
Fehlerklassen ausdenken und schauen, ob es die auch gibt.
Eben nach ähnlichen Fehlern dann suchen.
Ja, ich habe gerade gesagt, stabilisieren ist wichtig.
Man möchte halt ein reproduzierbares Experiment haben, wo der
gewünschte sozusagen Fehler immer auftritt.
Und wenn man das eben gemacht hat, nennt man das Stabilisierung.
Und durch diese Stabilisierung hat man es dann einfacher, die
Fehlerdiagnose zu machen.
Bei uns in Java haben wir es schon dadurch etwas einfacher, da es eine
bestimmte Klasse von Fehlern gibt, die nicht vorkommen kann.
Das sind nämlich hier diese hängende Zeiger.
Das ist also etwas, was in Programmen vorkommt, wo sie eben mit
Zeigern arbeiten.
Wir haben es ja schon gesehen, zum Beispiel in NC haben sie halt so
Zeigerarithmetik oder natürlich noch in grundlegenderen
Maschinensprachen wie Assembler.
Also wenn Sie einen Fehler haben, der sich ganz unvorhersagbar
verhält, kommt es oft vor, dass da eben so ein hängender Zeiger im
Spiel ist und mit irgendwelchen Speicheradressen irgendwas nicht
stimmt.
Also zum Beispiel, wenn Sie zwei Variablen zusammenzählen und das ist
irgendwie mal zufällig mal falsch und mal richtig, dann ist
wahrscheinlich so, dass eine oder mehrere dieser Variablen nicht
richtig initialisiert werden.
Und wenn Sie halt diese Zeiger benutzen in einer Sprache, in der das
geht, dann kann es oft sein, dass eben der Speicherplatz, auf den Sie
zeigen, durch irgendwas anderes überschrieben wurde.
Und das ist halt ein sehr häufiger Fehler, den man immer im Hinterkopf
behalten muss.
Vielleicht, wenn Sie mal mit einer anderen Programmiersprache dann
arbeiten.
Aber wie gesagt, in Java kommt das zum Glück nicht vor.
Ja, stabilisieren des Fehlers ist dann ein Testfall zu finden.
Und dann, wenn man den hat und der Fehler hervorgerufen werden kann,
den so zu vereinfachen, bis man den einfachsten Testfall hat, der noch
zu dem Fehler führt.
Und vereinfachen heißt eben, also der soll dann so minimal sein, dass
wenn man da irgendwas dran ändert, irgendeine Variable, dann ändert
sich schon das Verhalten des Fehlers.
Wenn Sie halt einen Testfall haben, wo Sie mehrere Variablen haben und
eine können Sie ändern, ohne dass was passiert, dann können Sie davon
noch abstrahieren und dadurch einen einfacheren Testfall erstellen.
Genau, und die Fehlerdiagnose ist dann eben, diesen Testfall zu
vereinfachen, zu verändern und das Programmverhalten zu beobachten.
Dann, wenn Sie diesen Fehler stabilisiert haben, kommt wie gesagt die
Lokalisierung.
Sie gucken es sich an, welche Daten waren es, was haben die gemeinsam
und dann machen Sie eine Hypothese darüber, was eben diese
Fehlerursache sein kann und überprüfen die dann durch weitere Tests
und weitere Inspektionen.
Ich habe hier mal ein etwas abstrakteres Beispiel dafür.
Also wir haben einen Testfall und es gibt zehn Faktoren, die führen
zusammen zu einem Fehler.
Dann machen wir eine Hypothese darüber, welche von diesen Faktoren
irrelevant sind.
Also wir sagen drei von denen, die haben keinen Einfluss darauf, ob
der Fehler auftritt oder nicht.
Dann ändern wir diese irrelevanten Faktoren und dann beobachten wir,
ob der Fehler immer noch auftritt.
Und wenn er immer noch auftritt, dann kann man diesen Faktor
tatsächlich eliminieren.
Dann hat sich dadurch nichts geändert und der Testfall ist dann
dadurch vereinfacht, dass man das weglässt.
Und wenn der Fehler aber plötzlich nicht mehr auftritt, weil Sie an
dem einen Faktor etwas ändern, dann ist es wohl ein relevanter Faktor.
Dann müssen Sie Ihre Hypothese korrigieren, denn offensichtlich ist
der Faktor nicht irrelevant, wie Sie vermutet haben.
Dann das zweite Beispiel, wenn Sie so Wertebereiche haben, das haben
wir schon gesehen beim Testen und Assertions, müssen Sie immer
innerhalb des Bereichs prüfen, manchmal auch außerhalb des Bereichs,
gucken, ob die passende Fehlermeldung kommt und vor allen Dingen auch
die Grenzen vom Wertebereich.
Das ist Ihnen selber bestimmt schon oft passiert, sondern oft bei One
-Fehler, gerade bei Array-Adressierung oder etwas anderem, bei
Schleifendurchläufen, da passiert einem das ganz oft, dass man statt 0
bis i 0 bis i minus 1 wählt oder kleiner gleich statt kleiner schreibt
und schon hat man diesen Fehler.
So funktioniert Debugging also prinzipiell und jetzt habe ich hier so
eine etwas längere Liste von Tipps zur Fehlersuche, das sind fast 20
Stück.
Die sind eben ja aus der Erfahrung wahrscheinlich dieses Autors, der
das Buch geschrieben hat, aber wie gesagt, die decken sich auch mit
dem, was ich so aus meinem Entwickleralltag kenne und möchte es Ihnen
deswegen hier jetzt einfach nacheinander vorstellen, also ohne
bestimmte Reihenfolge, die sind jetzt einfach nebeneinander gestellt.
Ja, das erste klingt irgendwie ein bisschen trivial, man soll alle
Daten, die man zur Verfügung hat, verwenden, um die Hypothese zu
formulieren und das ist natürlich, ja irgendwie, manchmal macht man es
nicht, manchmal hat man bestimmte Untermenge von Daten, die sind
einfacher zu handhaben, deswegen nimmt man nur die, aber man sollte
einfach alle nehmen, um seine Hypothese damit so zu formulieren, dass
es eine Klasse von Fällen am Ende beschreibt und nicht nur einen
einzelnen Fall.
Zum Beispiel, wir haben irgendwie eine Datenbank und möchten da Wörter
sortieren und wir haben gemerkt, alle Wörter, die mit T anfangen, die
können irgendwie nicht richtig sortiert werden.
Also sagen wir, Hypothese ist, irgendwas stimmt nicht bei der
Sortierung mit T.
Und dann machen wir genau das, was ich gerade beschrieben habe.
Wenn wir irgendwie plötzlich andere Wörter finden, die auch nicht
richtig sortiert werden, die aber nicht mit T anfangen, dann sollte
man nicht anfangen, irgendwie seine Daten, die man hat, zu verändern,
sondern man sollte die Hypothese ändern.
Also dann sage ich nicht, die passen nicht in meine Hypothese, ich
schmeiße die Wörter alle raus.
Das ist natürlich, verfälscht man dadurch das Ergebnis, sondern man
korrigiert die Hypothese und sagt dann eben, ja, es gibt auch andere
Fälle, wo es nicht funktioniert.
Dann wichtig noch, wenn man zwei Hypothesen hat, A und B, die beide
irgendwie möglich wären und A ist allgemeiner als B, dann reicht es
üblicherweise, wenn man die präzisere Hypothese überprüft, weil die
das dann schon mit einschließt.
Dann eben der zweite Tipp, die Testfälle, die man hat, soll man
verfeinern.
Also wenn man die Fehlerursache nicht rausfindet, dann muss man eben
diesen Testfall weiter verfeinern.
Was verfeinern heißt, ist dann abhängig von dem Fall, von dem
Programm, was jeweils untersucht wird und vor allen Dingen die
Parameter muss man vielleicht jetzt noch zu einem anderen Wertebereich
ausdehnen.
Vielleicht Werte, wo man bisher die irgendwie kategorisch
ausgeschlossen hat, aber möglicherweise kommt es doch irgendwie vor.
Ja, und dann muss man das Programmverhalten beobachten.
Ich habe gestern eine lustige Anekdote gehört.
Und zwar hat Jeff Bezos, der Chef von Amazon, anscheinend mal erzählt,
dass in der allerersten Version ihrer Bestellsoftware die Leute bei
der Bestellanzahl von Artikeln, die sie bestellen wollten, auch
negative Nummern eintragen konnten.
Und wenn man dann minus zwei Bücher bestellt hat, wurde einem der
Preis auf die Kreditkarte draufgerechnet statt abgezogen.
Da hat Jeff Bezos gesagt, es war zwar toll für die Kunden, aber Amazon
hat halt immer darauf gewartet, dass es vom Kunden dann mal zwei
Bücher geliefert kriegt und dass es nie passiert.
Also auch sowas wie man bestellt negative Anzahl von Büchern, sollte
man sich vielleicht mal überlegen, ob man das auch abtestet.
Dann ja, Benutzung von Unit-Tests.
Das ist ja das, was wir dann heute in den letzten zwei Dritteln von
der Vorlesung dann kennenlernen werden für JUnit.
Und was ist eben da die Idee dahinter?
Unit, also die Einheit, soll so klein sein, dass man den Fehler gut
lokalisieren kann.
Und es ist einfacher, in kleinen Programmabschnitten den Fehler zu
finden und dann zu isolieren.
Und JUnit ist dafür eben in Java gut geeignet und dazu später mehr.
Es gibt natürlich noch viele andere Werkzeuge neben Unit-Tests, die
Sie verwenden können in Ihrer Entwicklungsumgebung.
Zum Beispiel Eclipse ist natürlich ein interaktiver Debugger
eingebaut, mit dem Sie sich mal vertraut machen sollten und halt mal
vielleicht ein Web-Tutorial lesen, wie man so einen Debugger effizient
benutzt.
Das ist eigentlich eine sehr, sehr gute Methode.
Dann, der Compiler selbst gibt einem hoffentlich irgendwelche
sinnvollen Fehlermeldungen raus, die man verwenden kann.
Und dann gibt es natürlich noch etwas fortgeschrittener Sachen, wie
das dritte hier, Memory Profiler, die tatsächlich dann sich angucken
können, was passiert denn im Speicher während der Ausführung von einem
Programm, wo wird besonders viel Speicher verbraucht.
Und das ist natürlich dann nützlich, zum Beispiel hier, wenn halt ein
Teil des Programms irgendwie andere Ergebnisse überschreibt.
Wie gesagt, bei uns in Java nicht direkt möglich, dass in den
Speicherbereich reingeschrieben wird.
Aber es kann natürlich sein, dass irgendwelche Variablen oder andere
Umgebungsdinge überschrieben werden können, zum Beispiel Datenbank
-Zustand.
Das ist schwer dadurch zu überprüfen, dass man nur draufschaut und
analysiert.
Mit dem Debugger kann man dann tatsächlich sagen, hier an der Stelle
halt mal bitte an und zeigt mir, was im Speicher drin ist.
Und dadurch hat man es dann natürlich einfacher.
Nächster Tipp, Nummer 5.
Man sollte einen Fehler natürlich immer reproduzieren, aber auch auf
verschiedene Art und Weise.
Man sollte mehrere Testfälle schreiben, aber natürlich sollten die
sich auch unterscheiden.
Also fünfmal den exakt gleichen Testfall zu schreiben, ist irgendwie
sinnlos.
Da kann man natürlich auch denselben mehrfach ausführen.
Und ich habe das hier unten so ein bisschen geometrisch mal
angedeutet.
Es ist jedes Mal, wenn Sie quasi einen Test schreiben hier, dann
ziehen Sie so eine Art Gerade durch Ihren Raum durch.
Und wenn Sie einen Fehler gefunden haben, dann wissen Sie schon, der
liegt hier drauf.
Wenn Sie dann eine zweite Gerade hier durchziehen, dann wissen Sie
schon, okay, der liegt links oder rechts von der.
Und so können Sie sich halt durch verschiedene Schachtelungen dann
irgendwann so ein Polygon bauen, das möglichst klein ist, wo Sie immer
wissen, durch die verschiedenen Tests schränken Sie mehr Dinge ein.
Und dadurch haben Sie am Ende halt eine sehr gute Lokalisierung, weil
Sie wissen, hier genau in diesen Fällen tritt der Fehler auf und sonst
nicht.
Genau, also wie gehen Sie da vor?
Wenn Sie einen Testfall eben haben, dann konstruieren Sie einen
weiteren, der nicht zu einem Fehler führen sollte.
Wenn es aber doch so ist, dann haben Sie die Fehlerursache noch nicht
identifiziert.
Wenn tatsächlich kein Fehler auftritt, könnte es sein, dass Sie es
korrekt identifiziert haben, wissen Sie immer noch nicht.
Es könnte immer noch sein, dass es durch was anderes auftritt, dass
dann halt hier kein Fehler kommt.
Gut, Tipp Nummer 6.
Generieren Sie weitere Daten.
Natürlich haben Sie am Anfang hoffentlich Daten, auf denen Sie
aufbauen können.
Aber wenn Sie merken, dass da komme ich irgendwie nicht weiter,
versuchen Sie halt dann neue Daten zu generieren.
Manchmal muss man sich dann auch Programme schreiben, die diese
Testdaten generieren.
Das nennt man dann synthetische Tests.
Also wenn ich jetzt auf Benutzereingaben irgendwas testen möchte, dann
kann ich natürlich einfach 20 Benutzer hinsetzen, die Eingaben machen.
Ich kann aber auch irgendwie einen Zufallsgenerator dransetzen, der
irgendwelche wilden Kombinationen von Befehlen eingibt.
Das hilft nicht immer, weil dort werden vielleicht nicht häufige
Fehler gefunden, die in der Realität vorkommen.
Aber zum Beispiel bei der UI-Entwicklung gibt es ja sogenannte Monkey
-Tests.
Da wird einfach wild rumgeklickt durch den Zufallszahlengenerator und
man guckt, ob das Programm nicht abstürzt.
Genau, und sowas können Sie dann verwenden, um sich neue Testfälle und
zusätzliche Daten zu generieren und um Ihre Hypothese zu verschärfen.
Dann habe ich gerade schon gesagt, Widerlegung von der Hypothese ist
immer viel einfacher als Bestätigung, Verifikation.
Deswegen benutzen Sie negative Tests.
Also wenn Sie einen Test haben, der sagt, der Fehler liegt an einer
bestimmten Stelle und der Test schlägt aber fehl, dann wissen Sie,
okay, der Fehler liegt eben vielleicht an einer anderen Stelle und
dadurch können Sie Ihre Suche dann einschränken.
Also vielleicht durch auch diesen negativen Ausschluss, ist so ähnlich
wie diese Geraden, die ich gerade gezeigt habe.
Die wissen eben, wo der Fehler nicht liegt und durch Kombination
können Sie es dann einschränken, wo er tatsächlich liegt.
Jetzt kommen hier noch so paar weiche Tipps, die jetzt weniger
technisch sind.
Machen Sie ein bisschen Brainstorming.
Setzen Sie sich mit einem Kollegen zusammen, spinnen Sie einfach mal
rum, überlegen Sie sich unwahrscheinliche Dinge.
Ein Benutzer bestellt minus zwei Bücher zum Beispiel.
Sehr unwahrscheinlich, aber das kann durchaus mal vorkommen.
Schreiben Sie einfach mal alle Hypothesen hin und suchen sich dann
vielleicht die raus, die tatsächlich irgendwie machbar sind und
testbar sind und überprüfen Sie diese dann mit Testfällen, auch wenn
sie völlig unwahrscheinlich sind.
In Ihren Augen kann natürlich immer sein, dass Sie trotzdem in
irgendeiner seltsamen Situation vorkommen.
Dann auch so, auch trivial, erstellen Sie sich halt eine Liste aller
Methoden, die Sie für Debugging machen und gehen Sie die halt durch,
streichen Sie sich ab, welche Sie schon probiert haben.
Das gilt für alles.
Also das gilt für das Einkaufen im Supermarkt auch so.
Machen Sie sich eine Liste, streichen Sie ab, was Sie schon gemacht
haben.
Dann kaufen Sie es nicht zweimal.
So ist es hier beim Testen natürlich auch.
Also das ist vielleicht ein etwas einfacher Tipp.
Aber hier wird es wieder etwas spezifischer.
Wie grenzen Sie denn so eine Fehlerregion im Code ein?
Haben wir alle vielleicht schon mal gemacht.
Wir testen das ganze Programm und dann merken wir, der Fehler ist
irgendwie hier.
Dann machen wir da irgendwie so eine komische Kommandozeilen
-Standardausgaben -Anweisung rein, dass da halt was ausgegeben wird.
Wir schreiben was ins Log, wir gucken uns irgendwelche Traces an und
dann kommentieren wir mal was aus und versuchen, ob es da nochmal
auftritt.
Und dann haben wir jetzt aber fünf Zeilen auskommentiert.
Ja, welche von den fünf war denn jetzt dafür verantwortlich?
Deswegen also versuchen Sie so eine Art binäre Suche zu machen.
Schließen Sie aus, in der Hälfte war es nicht.
Okay, dann gucke ich mir die andere an und dann halbiere ich die
nochmal.
Ja, und dann gucken Sie eben, was das Programm macht.
Wenn der Fehler immer noch auftritt, dann liegt der Fehler
wahrscheinlich im nicht auskommentierten Code-Bereich.
Wenn er nicht mehr auftritt, liegt er im entfernten Code-Bereich und
statt auskommentieren benutzen Sie vielleicht schlauerweise dann
irgendwann so einen Debugger.
Mit dem geht es dann natürlich besser, als wenn Sie immer am Code
selbst rumarbeiten müssen.
Ja, dann auch wieder was ganz empirisches.
Einfach Vorsicht bei Klassen, die bisher schon mal Fehler irgendwie
gezeigt haben.
Da ist es einfach wahrscheinlich, dass da in Zukunft auch wieder
Fehler auftauchen.
Es kann sogar sein, dass Fehler, die Sie schon mal behoben haben,
nochmal auftauchen.
Die sogenannte Regression.
Das passiert ganz häufig, dass man halt irgendwie was ändert und der
Kollege ändert es wieder zurück, weil er denkt, warum wurde das
geändert?
Das ergibt doch gar keinen Sinn, hat doch früher auch geklappt.
So war es doch.
Und dann auf einmal haben Sie einen Fehler, den Sie eigentlich schon
gedacht haben, dass der behoben ist, müssen den Bug-Report neu
aufmachen.
Und ja, deswegen gucken Sie bei Klassen, die schon mal Fehler hatten,
ob die nicht schon wieder einen Fehler bringen.
Auch irgendwie logisch, wenn sich Code häufig ändert, wenn dort halt
sehr viel von verschiedenen Entwicklern gearbeitet wird, dann liegt da
normalerweise ein Fehler, der vorher nicht aufgetaucht ist, eben in
diesem Code.
Benutzen Sie Versionskontrolle, benutzen Sie DIV-Werkzeuge.
Auf GitHub ist das ja mittlerweile super bequem oder im ähnlichen
GitLab vom SCC geht genauso.
Dort sehen Sie sehr schön, wer was wann hinzugefügt hat und können
dann wirklich ausschließen, woran es eben nicht liegt.
Vergleichen Sie das Verhalten mit dem alten Code und dem neuen Code
und schauen Sie, was unterschiedlich ist und dann wissen Sie häufig
schon, wer den Fehler verursacht hat.
Man kann es auch andersrum machen.
Ich habe gerade gesagt, so top-down.
Wir gucken uns das ganze Programm an und schließen dann immer mehr
aus.
Man kann auch mit einer kleinen Code-Region anfangen und dann einfach
mit quasi dem Inversen von der binären Suche den Code-Bereich immer
erweitern und dann beim ersten Mal, wenn der Fehler auftritt, weiß
man, jetzt ist man wohl über diese Grenze gekommen, in dem das
fehlerhafte Code-Stück liegt.
Dann, was so ein bisschen zu diesen Unit-Tests gehört, die ich nachher
vorstellen werde, testen Sie kleine Programmstücke.
Also wenn Sie wissen, ein ganz kleines Programmstück, das funktioniert
so wie Sie möchten, das haben Sie getestet und Sie integrieren das,
also kombinieren Sie zum Beispiel eine Klasse mit einer anderen, dann
merken Sie, dann tritt ein Fehler auf, dann ist der Fehler wohl
irgendwo in der Interaktion dieser beiden und dann entfernen Sie
vielleicht die Klasse, die Sie gerade hinzugefügt haben, gucken, ob
der Fehler nicht auftritt, modifizieren Sie die und so weiter und das
hilft Ihnen dann bei der Fehlersuche.
Dann ist auch gut, sich einfach so eine Checkliste anzulegen von
Fehlern, die man schon mal hatte, weil nichts ist schlimmer, als wenn
man programmiert und macht einen Fehler und denkt, ach, das ist mir
schon mal passiert.
Mist, wie habe ich denn das damals gelöst?
Ich hatte auch sogar schon eine Lösung dafür.
Also schreiben Sie sich das vielleicht irgendwo auf, größere Gruppen
legen normalerweise dann so ein Entwickler-Wiki an oder wir haben ja
so einen Bug-Tracker, wo halt drinnen steht, was schon mal schief
ging, wie es gefixt wurde und dann arbeiten Sie halt diese Checklisten
ab und das klingt jetzt auch wieder so ein bisschen
psychotherapeutisch, aber das ist tatsächlich sehr, sehr wichtig.
Sprechen Sie mit Dritten über das Problem.
Also nicht mit Ihrem Kollegen, der den Code mitgeschrieben hat,
sondern vielleicht ein anderer Kollege von einer anderen Abteilung
oder halt jetzt von Ihnen Kommilitonen, der nicht direkt in Ihrem
Programmierprojekt drin war.
Gehen Sie mit dem mal drüber, machen Sie so ein kleines Code-Review,
schauen Sie sich den Code an.
Sagen Sie, wie verstehst du das?
Was glaubst du, was geht hier schief?
Distanz ist da immer wichtig.
Die Distanz kann man auch gewinnen, indem man einfach selbst ein paar
Wochen wartet und sich es dann wieder anschaut.
Aber gerade wenn irgendwelche Fristen, wie zum Beispiel Abgabefristen
von Programmieraufgaben drohen, dann hat man die Zeit eben meistens
nicht und deswegen am besten dann Dritten hinzuholen.
Ja gut, jetzt wird es völlig allgemein.
Coden Sie vielleicht nicht die ganze Nacht durch und nicht Stunden am
Stück.
Machen Sie mal eine Pause.
Ja, okay.
Wohnt sich was zu trinken, einen Kaffee, sowas.
Okay, wieder ein bisschen ernsthafter.
Eine Möglichkeit natürlich auch Fehler zu suchen, die wir bisher nicht
angeguckt haben, weil wir machen es ja immer schön systematisch und
mit Verfahren, ist einfach Brute Force.
Das beachtet man normalerweise nicht, weil man denkt, das ist einfach
zu dämlich.
Ich habe doch irgendeine schöne intelligente Methode, Fehler zu
suchen.
Aber manchmal hilft tatsächlich auch Brute Force.
Wenn zum Beispiel der Raum der Eingabedaten sehr überschaubar ist und
man einfach alle vollständig generieren kann, dann kann man die auch
testen.
Ja, man kann dann das, was hier steht, auch so ein bisschen als Brute
Force verstehen.
Zum Beispiel, man wirft einfach Code weg und schreibt ihn neu.
Das ist manchmal die beste Lösung, bevor man ewig debuggt.
Dann ja einen vollständigen Entwurf zu machen, der wirklich alles
abdeckt und dann im Code Review zu machen, was den ganzen Code
abdeckt.
Das dauert sehr lange, ist sehr aufwendig, aber kann gut sein.
Dann eben mit Debugging-Informationen das Programm kompilieren und
sich diese ganzen Debugger-Ausgaben anschauen.
Das kann auch überwältigend und sehr viel sein, aber manchmal hilft
tatsächlich nur das.
Also das ist nicht hier die Allgemeinlösung für alle
Programmierprobleme, sondern halt nur für manche.
Das gleiche passiert auch, wenn Sie jetzt hier in Ihrem Compiler eben
das Warning Level ändern, dann werden ja andere Fehlermeldungen
angezeigt, die sonst unterdrückt werden und das kann dann auch sehr,
sehr viel sein und dann passiert dasselbe.
Sie haben möglicherweise einfach zu viel Information, aber es kann
auch sein, dass Ihnen genau das hilft.
Dann eben Unit Tests sehen wir gleich.
Test Suite kommt auch jetzt in JUnit vor und das hier ist natürlich
auch so ein bisschen eine etwas anstrengende Lösung.
Da fühlt man sich dann halt wie so ein Roboter, der irgendwie so blöde
wiederholende Arbeiten langweilig machen muss.
Eben mithilfe von so einem Debugger einmal durch die ganze Schleife im
Code durchgehen und gucken, was passiert.
Das ist natürlich sehr, sehr aufwendig und bei großen Programmen
verliert man da einfach so die Übersicht, dass das nicht geht, aber
bei kleineren Programmen ist es möglicherweise sinnvoll.
Ja, dann auch Instrumentieren des Codes kann auch helfen.
Eine komplette Konfiguration des Endbenutzers anlegen ist auch
meistens anstrengend, aber es lohnt, dann hat man nämlich gleich einen
neuen Testfall.
Aber was sicherlich eine gute Strategie ist, ist eben kleine
Programmstücke, die man gut testen kann, auch komplett zu testen und
diese dann so zu integrieren, wie ich es gerade gezeigt habe.
Also das sind alles so Dinge, die kann man sich vielleicht auf so
einen gelben Zettel kleben und neben an den Monitor heften.
Vielleicht nicht alle von diesen 18, aber einige davon müssen Sie dann
für Ihr eigenes Programmierprojekt schauen, welche da zutreffen und
welche sinnvoll sind.
Damit möchte ich gerne den ersten Teil zusammenfassen.
Also was haben Sie jetzt im ersten Teil kennengelernt?
Das Debugging besteht eben nicht nur aus dem Finden, sondern auch dem
Beheben von Fehlern.
Das Ganze sollte wissenschaftlich vorgehen.
Das ist also das, Sie sind ja hier an der Universität und lernen das
wissenschaftliche Arbeiten.
Das können Sie tatsächlich auch für die praktische Methode des
Programmierens anwenden.
Hier ist also wiederholt, was wir gerade gesehen haben.
Ja, und diese Tipps, das ist eine lange Liste, das sind 20 Stück.
Suchen Sie sich immer davon die raus, die für Sie am besten zutrifft.
Vielleicht auch die, von der Sie denken, die hilft mir gar nicht.
Das kann manchmal auch die richtige sein.
Hier gibt es leider keine allgemeingültigen Tipps, die immer helfen
würden.
Damit wäre ich jetzt mit dem ersten Thema schon durch und muss hier
kurz eine Umkonfiguration vornehmen.
Die zweiten Folien, da möchte ich mich ein klein bisschen
entschuldigen dafür, die sind auf Englisch gehalten und wie Sie sehen
auch nicht mit PowerPoint, sondern mit LaTeX erzeugt.
Dafür entschuldige ich mich nicht, das ist gut so.
Da soll es jetzt nun um JUnit gehen.
Ich werde Ihnen da jetzt eine kleine Einführung in dieses JUnit geben.
Die meisten kennen es wahrscheinlich schon.
Wer von Ihnen hat schon mal JUnit selbst benutzt?
Hände hoch.
Ja, das sind schon einige.
Könnten noch mehr sein.
Hoffentlich werden es nach dieser Vorlesung mehr.
Also, was erwartet Sie jetzt im zweiten Teil?
Ich möchte nochmal kurz vorstellen, was für Arten von Tests es gibt
und wofür JUnit geeignet ist.
Dann, was ich schon öfter erwähnt habe, ist testgetriebene
Entwicklung.
Das ist, finde ich, eigentlich eine ganz gute Methode, um an das
Entwickeln von Programmcode ranzugehen.
Und da möchte ich in der ersten Hälfte auch ein klein bisschen was
vorstellen.
Vor allen Dingen eben hier Refactoring, wie das funktioniert und was
man da machen kann.
Dann kommt aber tatsächlich JUnit selbst dran.
Da kommt eine Übersicht.
Dann gibt es auch in JUnit Assertions.
Die hatten wir ja schon mal in der Vorlesung, die Assertions, aber das
sind hier eben andere Funktionen, die auch Assertions heißen, aber
nicht so funktionieren, wie das Assert das Java selbst anbietet.
Ja, und als fünftens möchte ich Ihnen dann das vorstellen, was JUnit
eine Fixture nennt.
Also das quasi, wie man so einen Test zusammenbaut, wie man die Daten
initialisiert, wie man es am Ende wieder deinitialisiert, wie man dann
parametrisierte Tests macht, wie man Tests zusammenbaut, so
Testsuites.
Und ja, ganz am Schluss möchte ich dann auch beispielhaft mal zeigen,
wie sowas in Eclipse eingebaut wird.
Auch dieser Teil hat ein kleines Vorwort, diesmal tatsächlich mit
einem Zitat, das belegbar ist und das Sie vielleicht auch schon
kennen, von Edgar Dijkstra, der eben gesagt hat, na, das
Programmtesten kann man benutzen, um die Anwesenheit von Bugs zu
zeigen, aber nie um die Abwesenheit zu zeigen.
Also hier wieder das gleiche Problem.
Also hier die Falsifikation ist einfach, einen Fehler zu finden, die
Abwesenheit von Fehlern zu zeigen, also die Verifikation, die ist sehr
schwierig.
Dann hier ein kleiner Hinweis, die Folien, die habe ich von einem
Tutorial teilweise übernommen.
Und was immer sehr gut ist, für jegliche Arten von Dingen, die mit
Java zu tun haben, sind auch die Tutorials von Lars Vogel.
Da habe ich Ihnen einen Link hier drauf gemacht.
Das können Sie selbst mal benutzen, um es durchzuspielen und selbst
mal mit JUnit ein bisschen was auszuprobieren.
Gut, dann am Anfang.
Welche Arten von Tests gibt es eigentlich?
Also wir haben bisher uns hauptsächlich die funktionalen Tests
angeschaut.
Wissen verhält sich im Programm so, wie wir es erwarten, wie wir es
spezifiziert haben.
Es gibt dann natürlich noch weitere funktionale Tests, die jetzt hier
in der Vorlesung nicht so viel die Rolle spielen, aber dann später z
.B.
im Programmierparadigmen vorkommen, eben Nebenläufigkeit und auch
Thread -Sicherheit.
Wo es funktionale Tests gibt, gibt es auch nicht funktionale.
Die testen dann also alles, was so an zusätzlichen Eigenschaften in
der Software vorhanden ist.
Wie ist die Performance von der Software?
Das ist vor allen Dingen auch etwas, was wir in unserer Gruppe bei
Professor Reusner machen.
Security ist natürlich auch ein ganz großes Thema.
Sicherheitsanalysen von Software werden immer wichtiger.
Benutzbarkeit, Interoperabilität mit anderer Software und auch
Zuverlässigkeit, der letzte Punkt.
Auch das alles kann man testen.
Das ist natürlich schwieriger oft als solche JUnit-Tests, wo
tatsächlich nur die funktionale Korrektheit überprüft wird und
getestet wird, aber auch das gibt es.
Dann kann man noch Arten von Tests unterscheiden von dem, was man weiß
über das System.
Wenn man nur die extern definierten Schnittstellen kennt von einem
System, dann spricht man von Blackbox-Testing.
Man verwendet das System also wie ein Benutzer, der auch nicht
reingucken kann in die Kiste.
Unterschied dazu ist Whitebox-Testing.
Da weiß man, wie die interne Implementierung aussieht und kann
vielleicht dort dementsprechend eingreifen, kann die Implementierung
ändern, kann dort auch Code ändern und Breakpoint setzen und so
weiter.
Die letzte Einteilung von Tests betrifft dann die Struktur-Unit-Tests.
Das, was wir uns heute jetzt anschauen mit JUnit, sind so die
kleinsten Dinge, die man testen kann.
Also eine Einheit ist dann halt eben eine Methode oder eine bestimmte
andere Funktion in ihrem Programm.
Die größeren Tests sind dann eben Integrationstests, wo sie
tatsächlich versuchen, funktioniert das auch in meiner Hardware
-Umgebung zum Beispiel, funktioniert das mit anderen Programmen,
funktioniert das mit der Datenbank-Version, die ich einsetzen will.
Und das letzte ist dann sozusagen der System-Test, wo das Ganze dann
im Feld sozusagen ausprobiert wird.
Wenn Sie jetzt eine mobile Anwendung für Android schreiben, gehen Sie
halt tatsächlich mal raus, versuchen, ob das mit dem GPS funktioniert
und so weiter.
Da wird dann das ganze System in seinem Zusammenspiel getestet.
Das sind also so die Klassifikationen von Tests.
Heute, wie gesagt, geht es hauptsächlich um die JUnit-Tests, aber
generell dieses Test-Driven-Development möchte ich Ihnen hier nochmal
ein bisschen ans Herz legen.
Das ist ein Begriff, der auch genannt Test-Infected-Development, was
ein bisschen negativ klingt.
Infektion klingt jetzt nicht so schön, aber man ist eben quasi wie
infiziert dadurch, dass man halt immer Tests schreiben möchte.
Der Ausspruch stammt von Erich Gammer, den Sie in der Software-Technik
dann auch noch kennenlernen werden, vor allen Dingen durch sein
Entwurfs -Musterbuch.
Man kommt dann irgendwann in so einen Zustand als Entwickler, wo man
einfach gar nicht mehr aufhören kann zu coden, bis endlich alle Tests
grün geworden sind, bis tatsächlich alles, was man vorher als Test
definiert hat, funktioniert.
Und das Ganze kann man dann ausweiten, sodass man sagt, es ist sogar
Extreme -Testing, also Testen ist sogar wichtiger als das Entwickeln.
Und Testing first, naja, klingt jetzt ein bisschen wie ein aktueller
politischer Slogan, da sage ich jetzt nichts dazu, aber das soll
eigentlich nur heißen, dass man eben wirklich zeitlich zuerst die
Tests schreibt und dann anfängt, die Funktionalität zu implementieren.
Was macht man also?
Man schreibt zum Beispiel mit JUnit Tests, und das kann ich schon mal
verraten, man hat dann meistens so eine Oberfläche, die einem
irgendwie durch grün signalisiert, der Test ist in Ordnung.
Und wenn man in dem Zustand ist, dann überlegt man sich, okay, was
habe ich noch nicht getestet?
Dann schreibt man einen weiteren Test und meistens schreibt man dann
einen Test für die Dinge, die man noch nicht mal implementiert hat.
Man weiß also, was soll rauskommen, schreibt dafür einen Test und weiß
auch schon, der wird fehlschlagen und das erwünschte Ergebnis ist dann
tatsächlich, dass der Test rot wird und fehlschlägt.
Und dann fängt man dann an zu coden, fängt an, die Funktionalität so
lange reinzubauen, bis alle Tests, die man gerade aktiviert hat und
scharfgeschaltet hat, wieder grün sind.
Und dann ist man in dem Zustand, dass man weiß, das, was ich testen
wollte, die neue Funktionalität, ist jetzt tatsächlich implementiert
worden und funktioniert.
Dann ist man aber noch nicht fertig.
Der dritte Schritt ist dann, okay, jetzt hat man in der ersten
Iteration das Ding entwickelt.
Dann geht es eben daran, den Code weiter zu vereinfachen oder eben
durch Refactoring zu verändern, um dort ihn besser lesbar zu machen,
um ihn besser wiederverwendbar zu machen, um ihn modularer zu machen
und so weiter.
Bei objektorientierter Entwicklung, und das machen wir ja hier, ist ja
vor allen Dingen das Problem, dass wenn Sie so eine Klasse schreiben,
die meistens nicht in Isolation arbeitet.
Die arbeitet eben zusammen mit anderen Klassen und viele Klassen kann
man dummerweise nicht unabhängig voneinander testen, weil die eben
voneinander abhängen.
Also reicht es oft halt nicht nur, diese Unit in dem Fall zu testen,
sondern man kann auch dieses Unit-Testing verwenden, um so Mikro
-Integrationstests zu machen.
Also ich teste, ob zwei Klassen, die ich selbst geschrieben habe,
miteinander funktionieren.
Und wenn man nur wenige Klassen hat, dann funktioniert das noch.
Sie können sich aber vorstellen, wie die Anzahl der Kanten in so einem
voll vermaschten Graph wächst, wenn Sie die Anzahl der Knoten erhöhen.
Also je mehr Klassen hinzugefügt werden, desto mehr Beziehungen unter
diesen Klassen gibt es eben.
Und deswegen steht hier auch, dann steigt der Testaufwand sehr stark
an.
Deswegen ja, isoliertes Testen wäre eigentlich das, was man gerne
haben möchte und versucht halt dann, das zu isolieren, was man
isolieren kann.
Und man orientiert sich dabei vor allen Dingen an typischen
Benutzungsszenarien.
Denn es ist das, in dem die Software dann in Zukunft eingesetzt werden
soll und das ist dann auch das, worauf man den Test Fokus legen soll,
haben wir ja auch schon gehört.
Je weniger Abhängigkeiten es dann zwischen den einzelnen Tests gibt
oder auch zwischen der Software, das findet man ja dabei dann raus,
desto einfacher ist die Software zu verwenden.
Und wenn es einen hohen Grad von Abhängigkeiten gibt in der Software,
die man versucht zu testen, merkt man dann meistens beim Testen auch,
dass man im Entwurf irgendwas falsch gemacht hat.
Hat man zum Beispiel ja schlecht modularisiert, ist der Entwurf
generell irgendwie schlecht.
Hat man vielleicht auch ein schlechtes Management für die
Abhängigkeiten zwischen Code.
Also in Eclipse zum Beispiel, da können Sie ja, wenn Sie für Eclipse
Erweiterungen entwickeln, können Sie das als Plugin entwickeln und
können sagen, das Plugin bietet eine bestimmte Version an und es
erfordert ein anderes Plugin von einem bestimmten Versionsbereich von
1.2 bis 1.4 funktioniert es nun mal.
Und dazu können Sie zum Beispiel managen, wie Code voneinander
abhängt.
Und wenn Sie sowas nicht haben, wenn Sie halt sagen, okay, jetzt hat
sich irgendwas an der anderen Klasse geändert, weiß nicht mehr, ob die
gut mit der zusammenarbeitet, dann haben Sie vielleicht so ein
schlechtes Abhängigkeitsmanagement von Code.
Deswegen, was Sie dann machen, ist eben das Refactoring, um das zu
verbessern, eben diese drei Dinge, die hier stehen, in einen besseren
Zustand zu bringen.
Es gibt ja viele Initiativen, die sagen, man soll sein Code einfach
halten, Clean Code zum Beispiel ist ja auch so eine Sache, was so ein
Gefordert wird.
Ja, und was heißt es denn, den Code einfach zu halten?
Man erreicht es vor allen Dingen durch Refactoring.
Oft schafft man es nicht durch das erstmalige Hinschreiben, sondern
ein komplett simples und trotzdem vollständiges und gut lesbares
Programm zu schreiben.
Und deswegen benutzt man, wie wir vorhin in diesem Schaubild gesehen
haben, das Refactoring zwischen den Tests erstmal.
Also man schreibt einfach den einfachsten Code, den einfachsten Code,
der den Test noch erfüllt und dann refactoren man dann zwischendrin
oder wenn tatsächlich die ganze Funktionalität schon drin ist, fängt
man an, den Code einfacher zu machen.
Und Sie sehen schon hier, ja, einfacher machen klingt toll, aber aus
Ihrer eigenen Erfahrung vielleicht auch, wenn Sie was versuchen zu
vereinfachen, macht man vielleicht auch gerne mal was kaputt.
Und deswegen ist es schön, wenn man sich vorher diese ganzen Tests
geschrieben hat, dann kann man nämlich so lange vereinfachen, bis man
möglicherweise merkt, jetzt habe ich irgendwas falsch gemacht, weil
jetzt schlagen die und diese und jene Tests fehlen.
Deswegen also testgetriebene Entwicklung meine Empfehlung.
Der Witz beim Refactoring ist normalerweise, dass man die Struktur von
einem Programm verändert, aber nicht die Semantik.
Also das Programm soll hinterher idealerweise genau das gleiche tun
wie vorher oder genau die Spezifikation erfüllen, das erwartete
Verhalten bringen und es gibt dann ebenso verschiedene Operationen,
die man machen kann, von denen man weiß, dass die Semantik erhalten
sind.
Die sind dann auch definiert in diesem Buch von Fowler, da kommt
gleich auch noch ein Literaturlink dazu.
Also was kann man zum Beispiel machen?
Klassen aufteilen, wenn sie zu groß werden.
Klassen zusammenfügen, wenn man merkt, die tun eigentlich das gleiche,
die sollten eigentlich eine Klasse sein.
Methoden oder Attribute in andere Klassen verschieben, meistens
entlang von der Vererbungshierarchie.
Also man zieht irgendwas in die Oberklasse hoch.
Umbenennung ist was ganz Klassisches und wie Sie auch schon vielleicht
gesehen haben, für diese Operationen bieten Ihnen die gängigen
Entwicklungsumgebungen dann auch Unterstützung.
Also in Eclipse klicken Sie auf sowas drauf, Alt-Shift-R und dann
können Sie das refactoren und es wird an allen Stellen im Code
geändert, konsistent, ohne dass Sie es manuell machen müssen,
idealerweise.
Genau, dann können Sie halt solche Methoden extrahieren oder
integrieren in andere Methoden.
Sie können, was wir auch schon in der Vorlesung hatten, Vererbungen
durch Delegationen ersetzen, weil ja oft die Delegation die bessere
Methode ist oder auch möglicherweise umgekehrt, wenn es sein muss.
Schnittstellen natürlich einführen, um dann sozusagen eine Art Vertrag
für die implementierenden Klassen zu definieren.
Das können Sie normalerweise machen, ohne die Semantik zu ändern von
Ihrem Programm.
Das klingt jetzt erstmal alles ganz schön.
Ja, und testen ist immer gut.
Das Testgetrieben ist sogar super.
Deswegen schreiben wir natürlich auch für unsere ganz, ganz großen
Programme, unsere ganz großen Systeme immer Tests.
Aber Sie sehen es hier schon, das Problem dabei ist, auch solche Tests
zu schreiben, verlangt viel Zeit.
Wenn Sie jetzt zum Beispiel, wie ich gerade gesagt habe, die
Abhängigkeit von zwei Klassen testen wollen oder sogar noch mehr
Klassen.
Da müssen Sie erstmal Objekte instanziieren, die müssen konfiguriert
werden, die müssen die richtigen Variablen bekommen.
Möglicherweise muss noch eine Umgebung stimmen, irgendeine Datenbank
muss da sein, eine Internetverbindung muss da sein, lauter so Sachen.
Das kostet also Zeit, sich sowas per Hand zu überlegen und es wird vor
allen Dingen dann schlimm, wenn man die Systemgrenze überschreitet.
Also was Sie in Ihrem Rechner lokal testen können, das geht meistens
noch ganz gut.
Wenn Sie aber dann halt zum Beispiel auf eine Middleware angewiesen
sind oder auf eine Datenbank, die Sie nur eingeschränkt kontrollieren
können, dann ist es meistens ein Grund dafür, dass Verzögerungen
auftreten.
Und vor allen Dingen ist es oft nicht trivial, solche Ausnahmezustände
irgendwie zu testen.
Internetverbindung reißt ab, das können Sie noch recht einfach testen.
Dann deaktivieren Sie das WLAN oder ziehen den Stecker und dann haben
Sie das irgendwie getestet.
Aber andere Dinge, wie zum Beispiel die Datenbank stürzt auf einmal ab
oder die Datenbank ist weg, das kann man auch noch irgendwie sich
überlegen, wie man das macht, automatisiert.
Ich kann ja schlecht die Datenbank runterfahren, ist für alle anderen
die Verbindung auch weg.
Wie mache ich das denn, dass nur mein Programm jetzt das Gefühl hat,
die Datenbank hat sich irgendwie verabschiedet, aber alles andere
läuft noch und die Datenbank funktioniert.
Sie sehen schon, in so einem einfachen Beispiel muss man sich da schon
was überlegen.
Und weitere Fehlerzustände, die Timeouts zum Beispiel, eine
Internetverbindung wird langsam.
Wie simulieren Sie das?
Also sowas zu reproduzieren, das ist schon nicht ganz so einfach.
Deswegen, um halt dieses Testen trotzdem hinzukriegen, sollte man sich
vornehmen, dass man die Tests einfach hält, dass die auch schnell
durchlaufen und dass die kohärent sind, also dass sie gut
zusammenhängen.
Und ja, das Schnell ist vor allen Dingen ein Faktor, denn wenn ein
Test eine Dreiviertelstunde braucht zum Durchlaufen, dann wird der
Entwickler irgendwann auch keine Lust mehr haben, den ständig
auszuführen, weil das seinen Entwicklungsfluss natürlich behindert.
Viele Dinge kann man dann auslagen, da sagt man halt, gut, der Test
läuft jetzt im Nightly-Build, den sollte der Entwickler vielleicht
nicht jedes Mal vorm Einchecken dann ausführen müssen.
Aber so ist es eben, wenn Sie die Tests eben klein und schnell halten,
dann kann man sich da auch möglicherweise nur eine Untermenge
aussuchen, die einem gerade interessiert und die dann sinnvoll in der
Zeit, die man hat, ausführen.
Ja, die Bad Smells, das ist auch etwas, was von Fowler stammt.
Da geht es jetzt darum, dass man eben manchmal Dinge tut, die man
eigentlich lieber lassen sollte, aber die leider häufig in der
Entwicklung vorkommen.
Bad Smells heißt auf Deutsch faule Gerüche, habe ich mal gelesen als
Terminus, aber wir nennen es jetzt hier auch mal weiterhin Bad Smells.
Also was Ihnen auch sicher vielleicht schon mal passiert ist, ist,
dass Sie einfach Logik dupliziert haben.
Sie brauchen die Funktionalität hier und Sie brauchen sie da.
Was machen Sie?
Sie kopieren einfach den Code an die andere Stelle rüber und dann
funktioniert es und haben sich da natürlich ein Problem damit
eingehandelt, denn Sie wissen nicht mehr, wohin Sie Ihren Code überall
kopiert haben.
Normalerweise startet jede Kopie dann irgendwie ein Eigenleben, wird
dann noch verändert, dann will man vielleicht das wieder mal irgendwie
rausziehen in eine gemeinsame Klasse oder in irgendeine Hilfsmethode
und merkt, oh, das wurde ja unabhängig voneinander geändert.
Das ist dann irgendwie ungünstig.
Außerdem wird Informationen über verschiedene Stellen im Code verteilt
und wenn es da Änderungen gibt, kriegen Sie es möglicherweise gar
nicht mit und plötzlich verhält sich die eine Hälfte des Programms so
und die andere so und Sie sagen doch, hey, muss die gleiche Funktion
sein, aber dadurch, dass Sie den Code dupliziert haben und er ein
Eigenleben entwickelt hat, ist es nicht mehr so.
Sie sollten auf jeden Fall große Klassen und lange Methoden
verhindern.
So Spaghetti-Code, der schlecht zu lesen ist, das haben Sie selbst
bestimmt auch schon gemerkt, das verhindert eben, dass man den
woanders hin umzieht.
Ja, wenn in so einer Methode ein Fehler auftritt, dann muss man
erstmal gucken, wo in diesem langen Code ist denn der Fehler dann.
Ist auch schlecht zu benutzen, weil die dann oft sehr viele Zwecke auf
einmal erfüllt und auch die Dokumentation ist schwierig.
Und vor allen Dingen, wenn man halt so lange Methoden hat, die sich
schlecht aufteilen lassen, fängt man dann an, Code zu duplizieren und
den eben an anderen Stellen durch Copy-Paste wieder einzufügen.
Was auch ein Bad Smell ist, ist schlechte Benennungen oder Code
-Strukturen, die einfach nicht passen, wenn Sie Ihre Variablen immer
nur x, y und z benennen.
Oder wenn Sie Variablen so einen langen Namen geben, habe ich auch
schon gesehen, Variablen mit 40 Stellen oder so, wo man erstmal 10
Sekunden liest, bis man dann verstanden hat, was die Variable heißen
soll.
Das macht den Code auch nicht übersichtlicher und erhöht den Aufwand
für denjenigen, der sich das Programm anschauen will und wissen will,
wie es läuft.
Das hier ist jetzt wiederum so ein bisschen ein zweischneidiges
Schwert.
Was Fowler spekulative Generalisierung nennt, bedeutet, dass man sich
beim Schreiben des Codes schon überlegt, wie wäre es denn möglich,
dass der Code in Zukunft erweitert wird?
Da wäre es doch schön, ich führe noch ein Interface ein und noch eine
Oberklasse und am besten eine ganze Klassenhierarchie.
Das sind ja erstmal alles Dinge, von denen ich Ihnen bisher erzählt
habe.
Das ist gut.
Sie strukturieren den Code durch Unterklassenbildung, Sie
strukturieren ihn durch die Einführung von Schnittstellen.
Aber es kann halt sein, dass wenn man das übertreibt, dass man so
viele mögliche Erweiterungsszenarien spekulativ schon mal eingebaut
hat, dass die Szenarien, die es momentan gibt, einfach überhaupt nicht
mehr verständlich sind.
Und jemand, der den Code anguckt, erstmal sich durch eine riesen
Hierarchie von Klassen wühlen muss, um nur das einfache Beispiel zu
verstehen.
Und schlimm wird es vor allen Dingen dann, wenn viele von diesen
Elementen überhaupt gar nicht benutzt werden.
Dann hat man zwar irgendwie theoretisch Flexibilität gewonnen, aber
dummerweise halt die Benutzbarkeit eingeschränkt.
Das gleiche ist für Kommentare.
Wir sagen natürlich immer, kommentieren Sie Ihren Code, dokumentieren
Sie Ihren Code.
Aber Fowler sagt, das ist nicht immer ausreichend.
Oft benutzt man sowas nur als Deodorant.
Es stinkt zwar immer noch, aber man macht halt irgendwie ein Deodorant
oben drüber.
Da hat man nicht dadurch verhindert, dass es stinkt, aber hat es halt
irgendwie überdeckt.
Und es gibt gewisse Arten von Kommentaren, die werden tatsächlich
überflüssig, wenn man ein ordentliches Refactoring macht.
Also wenn irgendeine Methode so komplex geschrieben ist, dass sie
nicht verstehbar ist, ohne dass man den Kommentar durchliest, sollte
man sich vielleicht eher überlegen, ob man die Methode umbaut, sodass
sie von sich aus verständlich wird, bevor man einen riesen
komplizierten Kommentar schreibt, den man sich erstmal durchlesen
muss.
Deswegen steht hier auch, also das bitte nicht damit verwechseln, dass
man Kommentare einfügt, um halt Entwurfsentscheidungen zu
dokumentieren oder halt öffentliche Schnittstellen in einem Javadoc zu
beschreiben oder halt Referenzen auf andere Klassen macht.
Also das hier, das ist der gute Einsatz von Kommentaren.
Aber man sollte eben sich jedes Mal dann, wenn man einen Kommentar
schreibt, der zu lang wird oder der was sehr Komplexes erklärt,
überlegen, hätte man es nicht doch durch Refactoring vielleicht besser
hingekriegt.
So viel allgemein zum Testgetriebenen entwickeln und zum Refactoring
und jetzt wird es dann tatsächlich ein bisschen praktisch und wir
gucken uns JUnit an.
Ja, was ist JUnit?
Ich habe schon gesagt, das ist ein Framework, um Tests zu schreiben
und das benutzt vor allem die reflektiven Eigenschaften von Java.
Also Sie wissen, in Java gibt es Funktionen, mit denen können Java
Klassen und Methoden sich selbst anschauen, also Reflektionen wie im
Spiegel.
Ich kann zum Beispiel eine Klasse fragen, welche Variablen sind denn
definiert bei dir, was haben die für einen Typ.
Ich kann zum Beispiel eine Methode fragen, welche Parameter hast du,
was ist dein Rückgabetyp, kann den dann dynamisch verarbeiten im
Programm und das wird eben in JUnit auch sehr viel verwendet.
Dann, wobei hilft JUnit dem Entwickler eben ja Tests zu definieren und
auszuführen und vor allen Dingen eben sogenannte Test-Suiten zu
bilden.
Also ein Test-Suite, wie in der Musik, eine Suite, mehrere
Musikstücke, sind hier halt mehrere Tests, die zusammengebündelt
werden und denen man einen Namen gibt.
Dadurch kann man dann auch tatsächlich Anforderungen formalisieren und
auch die Architektur irgendwie klarer darstellen.
Natürlich kann man zusätzlich ein Architekturmodell machen oder man
kann die Requirements, die Anforderungen noch hinschreiben in
natürlicher Sprache, aber oft helfen einem diese Tests schon, das
tatsächlich zu strukturieren.
Und in der testgetriebenen Entwicklung hilft einem dann das Testen
natürlich auch beim Schreiben und Debatten und eben das Integrieren
von Code, um damit immer zu so einer laufenden Version zu kommen, was
ja so ein bisschen dieses agile Manifest auch immer vorschreibt, dass
man immer eine laufende Version haben soll.
Das lernen Sie dann auch später in den Programmierparadigmen
wahrscheinlich.
Genau, was man noch dazu sagen soll, JUnit gehört nicht zum SDK von
Sun, steht hier noch, liebe Zeit, Oracle mittlerweile.
Okay, also das ändere ich vielleicht noch.
Sie sehen, die Folien sind auch schon ein bisschen abgehangen, aber
auf jeden Fall ist es nicht im Java JDK mit drin, JUnit, sondern es
ist eben ein Extrapaket, was Sie sich runterladen können.
In den gängigen IDEs ist es aber üblicherweise mit drin, zum Beispiel
Eclipse.
Ja, dann ein bisschen was geschichtliches.
JUnit kommt eben von diesem Erich Gammer und dieses Design Patterns
Buch, hinten in der Literatur auch erwähnt, das wird Ihnen auch noch
begegnen im Studium.
Und Kent Beck ist auch jemand, der hat dieses Extreme Programming vor
allen Dingen entwickelt, der hat daran mitgearbeitet.
Es gibt auch für andere Programmiersprachen ähnliche Unit Testing
Frameworks, die sich vor allen Dingen von JUnit haben inspirieren
lassen.
Es gibt für .NET zum Beispiel NUnit und für C++ gibt es auch so ein
Unit Testing Framework und für Java, muss man sagen, ist JUnit der de
facto Standard.
Also es ist, wie gesagt, nicht von Oracle festgelegt, aber im Prinzip
benutzt den jeder Entwickler.
Was auch so eine ganz kleine Stolperfalle vielleicht ist, die aktuelle
Version ist JUnit 4, hoffentlich mittlerweile ausschließlich
anzutreffen.
Es gab vorher eben JUnit 3, deswegen habe ich es auch hier nochmal
erwähnt, weil sogar heute, ich habe es gerade nochmal gegoogelt,
findet man so ein paar uralte Tutorials, wo tatsächlich noch auf JUnit
3 abgezielt wird.
Also wenn Sie irgendwo sehen JUnit 3, vergessen Sie es am besten
gleich.
Es ist zwar so, dass JUnit 4 damit kompatibel ist und alle diese
Konstrukte funktionieren heute noch, aber mit JUnit 4 ist es sehr viel
einfacher und das war eine komplette Neuentwicklung damals, als Java 5
eingeführt wurde, müssen Sie sich mal überlegen, das ist schon
ziemlich lange her und damit wurden dann eben auch solche Annotationen
eingeführt, die gab es noch nicht mal in JUnit 3.
Also das wollen Sie auf gar keinen Fall.
JUnit 5 ist momentan auch unter Entwicklung, soll dann für Java 8
irgendwann veröffentlicht werden, aber JUnit 4 ist momentan noch der
Standard.
Das vielleicht nur als ganz kleinen Hinweis.
Dann habe ich Ihnen hier jetzt die Terminologie kurz aufgeführt, die
wir im Folgenden benutzen.
Ich werde es jetzt nur einmal erwähnen, ich habe gleich noch ein
Schaubild.
Also eine Fixture ist quasi das, was die Daten zur Verfügung stellt,
also Objekte und primitive Daten, die man braucht, um so einen Test
auszuführen.
Also zum Beispiel, wir hatten ja dieses Objektmodell, wo irgendwelche
Angestellten verwaltet werden.
Wenn Sie so eine Klasse testen wollen, brauchen Sie erstmal irgendein
Beispieldatum, also irgendeinen Angestellten.
Dann ist so ein JUnit-Test, ist normalerweise ein Test, der eine
einzelne Klasse betrifft.
Also die Einheit, die da getestet wird, ist die einzelne Klasse.
Dann ist der Testfall innerhalb von so einem JUnit-Test, das, was eine
einzelne Methode testet für eine bestimmte Eingabekonfiguration.
Die Test-Suite bündelt solche Testfälle zusammen und der Test-Runner
führt das Ganze dann aus und berichtet drüber, was es für Ergebnisse
gab.
Und dann ganz am Ende möchte man dann vielleicht einen
Integrationstest machen, der mehrere von solchen kombiniert.
Und das geht mit JUnit auch, aber das ist nicht der Haupteinsatzzweck.
Das ist ein Unit-Testing-Framework, kein Integrations-Test-Framework.
Hier habe ich das Ganze nochmal grafisch, da kann man es vielleicht
besser sehen.
Also hier unten haben wir jetzt so zwei, drei kleine Test-Cases,
Testfälle.
Das sind diese grauen Kästchen da.
Die werden gebündelt zu so einem Unit-Test, der wie gesagt eine Klasse
testet und dann noch einen und noch einen.
Und diese verschiedenen Unit-Tests kann man dann wiederum bündeln zu
so einer Test-Suite.
Und unten diese Fixture ist quasi der Unterbau.
Die Unterfütterung sorgt dafür, dass die Daten da sind, die notwendig
sind, um das auszuführen.
Und der Test-Runner ist dann der, der es ausführt.
Also nochmal, das ist jetzt irgendwie getrippelt, aber na gut.
Das sind Unit-Tests für die Methoden hier in einer einzelnen Klasse,
also der Test-Case ist für die Methode, soweit es geht, und die Unit
für eine Klasse.
Es gibt mehrere Test-Cases für eine einzelne Methode.
Man kann natürlich eine Methode auf mehrere Arten testen.
Und ja, die Test-Suite vereint die Unit-Test, die Fixture ist quasi
der Unterbau und der Runner führt das ganze Ding aus.
Und die Integration, ja, die ist nicht so ganz gut unterstützt durch
JUnit, aber man kann es tatsächlich auch machen.
Gut, jetzt hat dieser Test-Runner diese Tests ausgeführt und was kommt
am Schluss raus?
Kommt ein Urteil auf English Verdict.
Das ist dann das erklärte Ergebnis, wenn ein einzelner Test ausgeführt
wird.
Und da gibt es drei verschiedene.
Das ist interessant.
Also es gibt nicht nur Bestanden, Nichtbestanden, wie wir vorhin
gesehen haben.
Grün und Rot.
Sondern es gibt eben Pass, das ist sozusagen Bestanden.
Das heißt, dass der Test seinen erwünschten Zweck erfüllt hat und dass
die Software sich so verhalten hat, wie man es von ihr erwartet hätte.
Fail heißt, dass der Test auch seinen Zweck erfüllt hat.
Und Zweck heißt hier meistens, das Programm ist einmal bis zum Ende
durchgelaufen, aber die Ergebnisse, die dabei herauskommen, waren
nicht wie erwartet.
Das nennt sich dann Fail.
Error im Gegensatz dazu heißt, dass der Zweck nicht erfüllt wurde.
Also das wurde zwischendrin irgendwann mal abgebrochen durch irgendein
unvorhergesehenes Ereignis.
Zum Beispiel ja, keine Ahnung, Speicher leer oder sonst irgendwas.
Datenbank war weg oder der Test konnte erst gar nicht aufgesetzt
werden.
Dann gibt es tatsächlich ein Error.
Und die beiden, die zu unterscheiden, das bringt sehr viel, wenn man
Tests definiert.
Jetzt habe ich die ganze Zeit schon gesagt, es gibt eben diese Unit
-Tests.
Was ist denn jetzt eigentlich ein JUnit-Test?
Ein JUnit-Test ist erstmal einfach nur eine Ansammlung von Java
-Methoden.
Also im Prinzip macht man halt das, was man per Hand auch machen
würde.
Nur man macht es mit etwas besserer Unterstützung von Seiten der
Software.
Man erstellt sich eben ein paar Objekte, macht irgendwas, was einem da
interessiert, macht man halt damit und guckt dann, ob die sich so
verhalten haben, wie man es erwartet hätte.
Und dafür schreibt man dann eben Programme, aber man fügt noch was
hinzu und zwar diese Assertions.
Nochmal, die Assertions hier in JUnit erfüllen einen ähnlichen Zweck
wie die in die Java-Sprache eingebauten Assertions, aber funktionieren
anders.
Also diese Assertions, die werden dann auf jeden Fall auch immer
ausgewertet, dann muss man nicht in der Kommandozeile die Assertions
erst anschalten, wie wir es gelernt haben für die Java-Assertions und
kann damit dann eben das machen, was man üblicherweise mit Objekten
eben so machen möchte.
Also man kann Objekte vergleichen, man kann gucken, ob die
Objektreferenzen identisch sind.
Das haben wir jetzt ja auch das letzte Mal vor allen Dingen gesehen,
dass es da wirklich sehr viele Unterschiede gibt zwischen demselben
Objekt identisch oder halt gleiches Objekt mit dieser Equal-Funktion.
Dann kann man eben überprüfen, ob gewisse Referenzen null sind oder
nicht null.
Das ist ja auch das, was oft zu einem Fehler führt.
Und ja, diese Assertions bestimmen dann tatsächlich, wie das Urteil
von so einem Test am Schluss aussieht.
Diese Tests sind dann eben in einzelnen Tests organisiert.
Also jeder einzelne von diesen Tests hat dann eben so ein Verdict und
dann werden die normalerweise an die Klassen halt dran geflanscht.
Wir haben ja gerade gesehen, so ein Unit-Test gehört immer zu einer
bestimmten Klasse idealerweise und dort hat sich die Namenskonvention
dann eingebürgert.
Wenn die Klasse zum Beispiel Value heißt, heißt der Testfall eben
Value -Test.
Also einfach genau wie die Klasse mit Test hinten dran.
Da kann man dann einfach sehen, was wozu gehört.
Ist keine Pflicht, man kann es auch ganz anders benennen, aber es ist
eigentlich gute Praxis, immer die Tests auch mit Tests enden zu
lassen.
So, jetzt aber, wie sieht so ein Unit-Test denn eigentlich aus?
Also wie wir gesagt haben, ist eine ganz normale Java-Klasse.
In den Imports oben müssen die JUnit-Pakete eingebunden werden, also
JUnit selbst und dieses Assert-Paket wird statisch importiert und dann
schreibt man halt einfach irgendeine Klasse und nennt die Test.
Also hier zum Beispiel MyProgramTest und dann füllt man da halt den
Rumpf mit irgendwas aus.
Also man initialisiert irgendwas, die Klassen können dann auch eigene
Variablen haben und so weiter und so fort.
Also soweit ganz normal wie ein anderes Java-Programm, eine andere
Java -Klasse auch.
Hier mal ein vollständiges, aber auch recht simples Beispiel.
Was macht dieser Test hier?
Der guckt, ob die Arithmetik-Funktion in Java richtig rechnet.
Naja, also ziemlich sinnlos, aber um halt zu zeigen, wie es
funktioniert, ganz okay.
Also hier oben die beiden Imports haben wir gerade schon gesehen.
Jetzt schreiben wir eine eigene Klasse, die heißt ArithmeticTest, weil
sie guckt, ob das Arithmetik-Paket von Java richtig rechnet.
Und dann machen wir hier verschiedene Tests, um Methoden zu
überprüfen.
Also hier zum Beispiel TestMultiply testet die Multiply-Methode,
TestIsPositive testet die Methode, die überprüft, ob Zahlen positiv
sind.
Also dass das alles durchläuft, sollte irgendwie klar sein.
Hier haben wir dann AssertEquals, da wird tatsächlich dann halt eine
erwartete Eingabe mit dem zu berechnenden Ergebnis verglichen.
Und hier bei AssertTrue und False soll halt hier rauskommen, 5 ist
positiv, minus 5 ist nicht positiv und 0 auch nicht.
Diese Test-Annotation hier an den Methoden deutet einfach an, dass
diese Methode ein Test ist und als solcher ausgeführt wird.
Ja, diese Assertions sind wie gesagt in diesem Paket Assert mit drin
und die sorgen dafür, dass wenn die nach Wahr auswerten, dass die
Methode weiterhin ausgeführt wird.
Also ganz ähnlich wie die Java-Assertions.
Aber jetzt kommt der Unterschied, wenn die nach False auswertet, aber
die Methode halt durchläuft, dann kommt Fail raus.
Also haben wir vorhin gesehen, Testziel erreicht, aber Ergebnisse
falsch.
Wenn die Methode irgendwann abbricht, wird es dann allerdings zum
Error und wenn alle Assertions halt nach Wahr ausgewertet haben und
die ganze Methode durchgelaufen ist, dann wird der Test nach Pass
ausgewertet und das ist dann der Urteil.
Und das sind alles statische Methoden, diese Assert-Methoden, die also
immer irgendwo aufgerufen werden können.
Ich habe hier nochmal kurz aufgeführt, welches da gibt.
Das ist halt praktischerweise, gibt dann auch eine für True, eine für
False, eine für 0, eine für Not-0, obwohl man das natürlich auch
irgendwie logischerweise selbst ausrechnen könnte.
Dann Assert-Same sind dann, ob es wirklich zwei Referenzen auf
dasselbe Objekt sind.
Also hier muss es wirklich dasselbe Objekt oder halt Not-Same das
nicht dasselbe Objekt sein und wenn zwei Objekte halt unterschiedlich
sind, also nicht dasselbe sind, aber mit Equals nach Gleich auswerten,
dafür gibt es eben dann das Assert-Equals und wenn man Assert-Equals
mit Expected und Actual aufruft, ist es das gleiche, wie wenn halt
Expected Equals Actual nach Wahr auswertet und dann wird eben der Test
bestanden.
Es gibt dann auch ein paar praktische Methoden, mit denen man zum
Beispiel Arrays vergleichen kann.
Assert-Array-Equals überprüft, ob die Länge gleich ist und dann führt
es eben für jedes Element an der jeweiligen Stelle aus, ob tatsächlich
hier Assert-Equals gilt und wenn das dann hier gilt für jede einzelne
Stelle i, dann gilt halt am Schluss das Assert-Array-Equals für alle
Arrays.
Hier steht übrigens dann nochmal i und i, weil man das dann auch
beeinflussen kann.
Also man könnte dann auch zum Beispiel symmetrische Arrays überprüfen
oder welche, die irgendwie versetzt sind oder so.
Man kann natürlich auch per Hand einfach ein Fail aufrufen, dann wird
immer das Verdict sein, dass der Test fehlgeschlagen ist, falls man
das nicht mit einer Assertion irgendwie kompliziert ausdrücken will.
Für ganz einfache Fälle kann man dann einfach auch Fail aufrufen.
Gut, und dann kommen wir jetzt hier zu dem spannendsten Teil, finde
ich, und zwar diese Fixtures.
Also wir brauchen Daten, um Tests auszuführen und wir brauchen so
einen Kontext, in dem so ein Testfall läuft und wir müssen halt
Objekte erzeugen.
Jetzt hier war es irgendwie einfach in diesem Beispiel, da haben wir
halt ein paar Zahlen reingeschrieben, die hat man schnell
hingeschrieben, aber so ein Objekt zu initialisieren ist ja vielleicht
ein bisschen aufwendiger, manchmal.
Und diese Objekte sollen halt nicht nur idealerweise für einen
Testfall benutzt werden, sondern in vielen.
Ja, und es gibt Aktivitäten, in denen das dann ausgeführt wird, und
zwar sogenannte Setup und Teardown.
Das ist quasi Initialisierung und Deinitialisierung.
Das wird vor den Tests und nach den Tests jeweils ausgeführt und dort
werden dann auch Ressourcen bereitgestellt, die man braucht, wie zum
Beispiel eine Datenbankverbindung oder irgendwas anderes.
Ja, man kann damit natürlich mehrfach Tests auf den gleichen oder
ähnlichen Objekten ausführen und man kann die auch wiederverwenden,
diese Fixtures.
Also wenn man einmal eine komplizierte Initialisierung seiner
Datenbank sich ausgedacht hat, kann man die dann für viele Tests
wiederverwenden und das ist ganz praktisch.
Um diese Dinge, die vor und nach den Tests ausgeführt werden sollen,
zu beschreiben, gibt es auch wieder Annotationen.
Die heißen Before and After.
Das ist eine ganz normale Methode in Ihrer Java-Klasse, aber wenn Sie
Before dran schreiben, wird die vor jedem Test ausgeführt.
Also wenn Sie mehrere Tests haben, wird vor jedes Mal, wenn so ein
Test ausgeführt wird, alle Methoden ausgeführt, an denen Before dran
steht und After dementsprechend nachdem der Test ausgeführt wird.
Das heißt, wenn ich 10 Tests habe, hier nochmal mit Betonung, wenn man
10 Tests hat, dann wird jede Before-Methode zehnmal ausgeführt, vor
jedem einzelnen Test.
Man kann auch mehrere Before-Methoden haben und die Namen sind
eigentlich egal, die dienen nur dafür, dass man sie irgendwie, wenn
man sie durchliest, irgendwie verstehen kann, aber die werden
ausgeführt, egal wie sie heißen.
Aber sie müssen Public-Void sein.
So eine Test-Fixture könnte dann dementsprechend aussehen.
Hier machen wir wieder irgendwas ganz Einfaches.
Wir testen irgendwie, ob eine Klasse, mit der wir Geld beschreiben
können, funktioniert.
Also haben wir hier irgendwie so eine Klasse, die heißt Money und da
haben wir uns drei Objekte erzeugt, irgendwie Schweizer Franken und US
-Dollar und hier in diesem Setup befüllen wir die mit irgendwas.
Also hier diese drei Attribute, die sind Private und die sind in der
ganzen Klasse, stehen die zur Verfügung und werden in so einem Test
-Setup dann halt mit irgendwelchen Werten befüllt, hier 12, 14, 28 zum
Beispiel.
Und das wird dann eben ausgeführt, vor jedem einzelnen Test.
Genau, diese Allotation deswegen halt verwenden, wenn sie jedes Mal
ausgeführt werden soll und After verwenden für das, was dann am
Schluss gemacht werden soll.
Zum Beispiel irgendwas auf die Platte geschrieben, irgendeine Logdatei
noch befüllt oder eine Datenbankverbindung wieder sauber beendet oder
sowas.
Und die wird auch dann ausgeführt, sogar wenn irgend so eine Exception
dazu geführt hat, dass das Ding mit einem Error abbricht.
Also diese After-Methode wird immer ausgeführt.
Dann kann man, ja wie gesagt, mehrere von denen, beliebig viele,
hinschreiben, aber man hat keine Garantie darüber, in welcher
Reihenfolge die ausgeführt werden.
Also wenn die untereinander stehen im Code, heißt es nicht, dass in
der Reihenfolge ausgeführt werden, sondern die können durch
irgendwelche Optimierungen im Testrunner halt in einer beliebigen
Reihenfolge ausgeführt werden.
Ich habe gerade gesagt, Before und After wird halt nach jedem
einzelnen Test ausgeführt.
Es gibt dann auch noch Before Class und After Class.
Die werden dann einmal vor jeder Test-Suite und danach ausgeführt.
Und die dementsprechend darf man dann auch nur einmal haben und die
müssen auch wiederum static sein.
Das ist vielleicht der sinnvollere Ort, wo man eine Datenbank
initialisieren würde, weil die möchte man ja nicht für jeden einzelnen
Test erst wieder die Verbindung aufbauen und sie wieder zumachen,
sondern man möchte das vielleicht einmal am Anfang machen für alle
Tests und dann ganz am Schluss wieder zumachen.
Ja, ein Beispiel hier jetzt in diesem simplen Geld-Szenario wäre eben,
dass man halt sagt, es gibt irgendwie so eine Art globale Währung, die
halt jetzt Standard ist, damit man nicht jedes Mal hinschreiben muss,
dass wir jetzt heute Schweizer Franken testen.
Dann schreibt man halt einmal hin, this currency ist CHF.
Das setzt dann hier dieses statische Attribut.
Und für alle weiteren Tests, die jetzt hier noch sein könnten, würde
dann jedes Mal dieses neu gesetzt.
Kann man jetzt auch sagen, wird es halt einmal gesetzt am Anfang.
Also jedes Mal, bevor die ganze Suite ausgeführt wird und bleibt dann
eben bis zum Ende so.
Genau.
Dann haben Sie ja sicher auch schon mit Exceptions gearbeitet und
normalerweise sind ja Exceptions so was, dass man nicht haben möchte
in der Entwicklung, aber manchmal kann es tatsächlich sein, dass man
halt eigene Exception-Klassen zum Beispiel eingeführt hat und man
möchte wissen, werden die korrekt geworfen.
Also man will irgendwie einen Fehlerzustand provozieren und gucken,
kommt die richtige Exception dabei raus.
Und dabei unterstützt einem auch JUnit und zwar kann man an diese Test
-Annotation hinschreiben, das ist sehr sehr praktisch, dass man eine
bestimmte Art von Exception erwartet, also dass die geworfen werden
soll in diesem Test.
Und wenn diese Exception dann nicht geworfen wird, also wenn das
Programm ohne Exception durchläuft, dann schlägt der Test sogar Fail.
Und wenn man tatsächlich bis zum Ende durchläuft, ohne dass eine
Exception fliegt.
Das ist sehr sehr praktisch, eben wie gesagt, um eigene Exception
-Klassen zu testen, die man dann möglicherweise anderswo dann abfängt
und behandelt.
Aber dort, wo sie halt definiert sind, kann man schon durch einen
JUnit -Test sicherstellen, dass die tatsächlich immer geworfen werden.
Zum Beispiel, wenn man jetzt hier so eine ArrayList sicherstellt und
die ist neu und dann holt sich das nullte Element, kriegt man erstmal
eine Index-Out-of-Bounds-Exception, denn es ist ja noch ein leeres
Objekt und da steht noch gar nichts drin.
Und so könnte man zum Beispiel jetzt testen, ob tatsächlich so eine
geworfen wird.
Ist bei ArrayList jetzt vielleicht nicht spannend, das sind ja alles
hier Klassen aus der Java-Standard-Bibliothek.
Aber wenn Sie eine eigene Klasse implementiert haben, dann möchten Sie
vielleicht auch wissen, habe ich das da ordentlich delegiert, wird da
auch die richtige Exception geworfen, wenn ich die versuche, auf die
zuzugreifen und die ist noch gar nicht initialisiert.
Dann, was auch sehr praktisch ist, wenn Sie einen Test haben und
wissen, ja, das Feature habe ich halt noch nicht implementiert, dann
müssen Sie den Test nicht irgendwie auskommentieren oder Sie müssen
ihn nicht irgendwie löschen oder sonst irgendwie rumtricksen, dass der
nicht ausgeführt wird, sondern Sie können einfach so ein Ignore vorne
dran schreiben und der Runner von dem Test weiß dann, dass dieser Test
zwar existiert und dass er aber nicht ausgeführt wurde.
Und das ist natürlich dann sehr praktisch, wenn Sie zum Beispiel in
eine Log-Datei irgendwas reinschreiben, die und die Tests wurden
ausgeführt, aber der eine nicht, da können Sie sogar noch einen Grund
angeben, dann steht dann drin, ja, der wurde nicht ausgeführt, weil in
diesem Szenario nicht von Belang oder Funktionalität wird erst in der
nächsten Version implementiert oder Funktion ist deprecated oder
sowas.
Also damit können Sie sehr gut Dinge dokumentieren.
Sie sehen, sonst würde jemand nur sehen, wenn ein Test irgendwie nicht
ausgeführt wird, ja, der wird auskommentiert.
Ja, gut, okay, vielleicht hat noch jemand drüber geschrieben, warum er
auskommentiert wurde.
In der Log-Datei steht gar nichts drüber drin, da weiß man nicht, was
Sache ist.
Und hier können Sie sehr gut nachvollziehen und dokumentieren, warum
etwas nicht gemacht wurde.
Wir können auch Timeouts mit dieser Testannotation sehr, sehr einfach
formulieren, indem Sie einfach reinschreiben, Timeout gleich Zahl und
wenn es länger dauert als so viele Millisekunden, dann schlägt dieser
Test auch automatisch fehl.
Also überlegen Sie sich mal, was Sie sonst machen müssten.
Sie müssten sich irgendwie mit Systemtime in Millisekunden geben
lassen, vorher und hinterher irgendwie ausrechnen, dann vergleichen,
ist es größer zehn, ja, dann bitte asserten und schlägt fehl und hier
können Sie mit einer Zeile das quasi hinschreiben.
Das ist sehr, sehr praktisch.
Und dann kommen wir auch schon zum allerpraktischsten, denke ich, was
ich Ihnen heute vorstellen will.
Oft möchte man ja so einen Test mehrmals ausführen mit verschiedenen
Parametern und wie wir vorhin schon gesehen haben, muss man entweder
halt drei Tests hinschreiben und halt per Hand 14, 20, 28 oder
irgendwas reinschreiben oder sich erst irgendwie so eine Collection
anlegen mit Werten und dann eine Schleife drüber drehen und hat dann
halt dummerweise den Nachteil, wenn ich so eine Collection habe mit 20
Werten und irgendwie bei Wert 12 gibt es einen Fehler, dann bricht es
irgendwie ab und man weiß nicht, was mit den Werten ist, die hinterher
passieren oder man kommt vielleicht sogar blöderweise in irgendeine
Art Endlosschleife rein und deswegen ist es dann sehr ungünstig.
Was man weiterhin machen müsste, wäre Testfälle für jede Kombination
von Daten zu schreiben und letztendlich müsste man sehr, sehr viel
Code schreiben.
Das ist eben der Grund, warum es hier parametrisierte Tests gibt und
mit JUnit kann man das halt sehr flexibel und sehr einfach machen.
Man muss da halt ein paar Schritte einhalten, die habe ich hier mal
kurz erwähnt.
Ja, das erste ist logisch, ein Testschreiben mit der Testannotation,
das war Ihnen auch noch klar, aber jetzt kommt was Neues dazu.
Es gibt dann so eine sogenannte Feeder-Methode, die einem die Daten
anliefert.
Die gibt so ein Collection-Type zurück und die kriegt diese
Dekorierung hier, dass da Parameters dranstehen.
Dann weiß man, ah ja, hier, diese Methode liefert mir die Parameter
für meine Tests.
Ja, dann muss ich halt, wie gesagt, noch irgendwie diese Parameter
befüllen und dann einen Konstruktor noch erstellen, der diese
Parameter entgegennimmt und das dann mit denen in Verbindung bringt
und das Ganze dann ausführen mit der RunWith-Notation.
Das klingt jetzt ein bisschen kompliziert, ich erkläre es am Beispiel
einfach auf der nächsten Folie.
Also, was machen wir hier?
Hier wollen wir irgendwie prüfen, hier haben wir eine Testmethode, die
irgendwie überprüfen will, ob jetzt so eine Zahl gerade oder ungerade
ist.
Ist jetzt auch nicht weiter spannend, aber wie wird das jetzt gemacht?
Also, ich habe hier parametrisierten Test, das ist klar.
Hier oben habe ich schon hingeschrieben, ich möchte es also als
parametrisierten Test laufen lassen.
Also, das hier oben sagt einem, welcher Testrunner dafür jetzt dann
halt verantwortlich ist.
Dann hat man hier auch wiederum Variablen, eben die Nummer, die man
prüfen will und den Rest.
Und jetzt hat man hier so einen Konstruktor, der halt sagt, ich nehme
zwei Werte entgegen, den p-Value und den r-Value, denn p ist dann eben
die Nummer, die ich testen will und r ist der Rest.
Dann braucht er also hier irgendwie so Paare von Integers.
Dafür muss ich ihm jetzt so eine Feeder-Methode schreiben, die die
liefert.
Also, hier sage ich, diese Parameters-Methode gibt eine List von
Integer -Arrays raus.
Also, das ist hier eine Liste und in denen drin sind halt immer diese
Paare, also eine Liste von Paaren.
Ich mache mir also hier ein neues Integer-Doppel-Array und sage halt,
ich möchte die Paare 1,1, 3,1, 6,0, 7,1, 9,1 und so weiter testen.
Jetzt sagen sie hier, na ja, toll, den ganzen Krempel hinzuschreiben,
das ist ja nicht viel weniger Aufwand, als einfach mal fünf Tests
hinzuschreiben, wo die Zahlen hartgecodet drinstehen.
Das stimmt natürlich, aber sie können sich überlegen, das kann zum
Beispiel automatisch generiert werden.
Sie können eine große Vorschleife machen, wo sie die Werte automatisch
generieren.
Sie können durch einen Zufallszahlengenerator beliebig große Arrays
generieren, wo irgendwas drinnen steht oder sie können durch
komplexere Methoden noch die Daten erstellen.
Und das müssen sie sonst per Hand machen und können hier dann einfach
sagen, auf diesen Daten bitte den Test ausführen.
Ja, und der Test, der greift dann wiederum hier nur auf diese
Variablen zu, die hier in ihrem Konstruktor gesetzt wurden und wird
dann halt dort eben jetzt überprüfen, ob das immer stimmt, dass hier 1
durch 2 Rest 1 gibt und 6 durch 2 Rest 0 und so weiter und so fort.
Also wie gesagt, behalten Sie sich das im Hinterkopf, wenn Sie einen
großen Test schreiben wollen, der viele Daten verarbeitet.
Da sparen Sie sich sehr viel Arbeit dadurch.
Ja, dann haben Sie Ihre Tests und möchten die halt vielleicht noch
irgendwie gliedern, halt gleiche Tests oder ähnliche Tests in Gruppen
zusammenführen.
Damit können Sie dann solche Test-Suiten machen.
Die können wiederum in anderen Suiten gruppiert werden und dadurch
können Sie dann Ihre Test-Szenarien gut partitionieren und das wird
normalerweise von diesen Test-Runnern auch sehr gut unterstützt.
In Eclipse können Sie dann zum Beispiel einfach, wenn so eine ganze
Test -Suite durchgelaufen ist, sich so eine Baumansicht aufmachen, auf
einen Test draufklicken und sagen, ja, die bitte nochmal ausführen
oder halt diese Gruppe nochmal ausführen oder halt einen Test, der
bisher ignoriert wurde.
Da können Sie dann einzeln drauf zugreifen und sich einzeln angucken,
warum welche Tests dann halt fehlgeschlagen sind zum Beispiel oder wie
sie sich verhalten haben.
Ja, und so eine Test-Suite zu erstellen, das ist dann wiederum auch
nicht besonders schwer.
Sie sagen halt dem wieder, dem Runner, dass das jetzt als Suite
ausgeführt werden soll und sagen in so einer Annotation, welche
Klassen alle zu dieser Suite hinzugehören.
Die liegen dann halt irgendwo, das sind andere Dateien.
Und dann gibt es halt am Schluss ja noch hier so einen Konstruktor, wo
jetzt also gar nichts drin steht.
Da könnten dann natürlich noch irgendwelche Variablen drinstehen für
irgendwelche parametrisierten Tests oder sonst irgendwas.
Gut, dann bin ich mit dem, was JUnit kann, soweit fertig.
Wenn sie dann JUnit-Tests ausführen wollen, bietet JUnit ihnen jetzt
nicht eine grafische Oberfläche direkt.
Wenn sie jetzt JUnit runterladen würden, direkt von der Seite, können
sie ja machen.
Bietet ihnen das erstmal nur eine API, mit dem sie halt das Ganze
ansteuern können.
Aber wie gesagt, in Eclipse und so weiter können sie das dann schön
machen und auch sich da grafisch das ausgeben lassen, was dann
passiert und das dann auch steuern.
Ja, mit diesen Test-Runners, die sie dann haben, können sie dann die
Klasse natürlich ausführen und dann werden erstmal alle Methoden
ausgeführt.
Auch hier gilt wiederum die Reihenfolge dieser Methoden.
Die ist nicht vorhersagbar.
Also das ist wie bei den Before-Annotationen auch.
Wenn sie mehrere Tests nacheinander haben, sollten sie keine
Abhängigkeiten haben.
Also sagen sie nicht Test 1 initialisiert die Datenbank, Test 2 greift
auf die Datenbank zu.
Verwenden sie für sowas diese Before- und Before-Class-Annotationen.
Die sind genau dafür da, halt für alle Tests die gleiche Umgebung
herzustellen, denn nur dann sind es Unit-Tests, die auch wirklich als
Einheit getestet werden, wo die Umgebungen dann tatsächlich
reproduzierbar sind.
Und das gehört eben auch dazu, dass man wissenschaftlich testet, dass
man diese reproduzierbare Umgebung schafft.
Genau, nicht vorhersagbar und es kann sein, dass diese Runners in den
IDEs einem irgendwie erlauben, dass man da die Reihenfolge festlegt,
aber generell kann man nicht davon ausgehen, dass es mit beliebigen
Testrunners immer in einer bestimmten Reihenfolge ausgeführt wird.
In Eclipse können sie natürlich dann möglicherweise sagen, okay, die
drei Tests jetzt bitte in der Reihenfolge ausführen.
Und genau das, was ich Ihnen gerade erklärt habe, steht hier nochmal.
Also gute Praxis, Tests zu schreiben, die unabhängig von der
Ausführung von anderen Tests sind und halt eben dann diese
wissenschaftliche Testmethode erlauben.
Ich wollte Ihnen jetzt hier keine Live-Demo zeigen, das können Sie
gerne alles bei sich zu Hause mal durchklicken.
Vielleicht gesehen haben Sie es schon, in Eclipse erscheint dann eben
so ein JUnit-Testfenster, wo man hier so eine baumartige Ansicht hat.
Hier ist in dem Fall jetzt nur ein einziger Test drin, das ist ein
bisschen langweilig, aber das können mehr sein.
Hier ist jetzt zum Beispiel so ein blaues Kreuz dran, wenn man es
erkennen kann.
Das hieß eben, dass der Test fehlgeschlagen ist, sonst wäre ein grünes
Häkchen dran, ein rotes X bei Error.
Also um mal wieder ein bisschen so psychologisch zu werden, es ist ein
sehr schönes Gefühl, wenn man halt ein Bug gefixt hat, dann durchläuft
und alles hier ist grün und am Ende ist ein grüner Balken da.
Das ist tatsächlich auch irgendwie dann das, was einem beim
Testgetriebenen -Entwickeln so ein bisschen positives Feedback gibt.
Genau, in Eclipse ist JUnit normalerweise drin.
Ne GUI gibt es auch und man kann dadurch sehr einfach Tests neu
ausführen und sich die History anschauen.
Hier unten bekommen Sie dann auch so ein Trace, wo Sie die
Aufrufgeschichte sich anschauen können, was denn genau passiert ist.
Ich habe hier auf der letzten Folie noch ein paar Referenzen, die
können Sie sich dann anschauen, wenn Sie die Folien runterladen.
Und jetzt muss ich noch mal ganz kurz hier einen technischen Switch
machen für die Zusammenfassung.
Ja.
Genau, JUnit, hier waren wir.
Ja, also Zusammenfassung.
Was haben wir jetzt in dem zweiten Teil kennengelernt?
JUnit als sehr mächtiges Test-Framework für Java und ja, müssen Sie
sich normalerweise nicht von der JUnit-Seite runterladen, ist in
Eclipse und Co.
normalerweise drin.
Die Testgetriebe, eine Entwicklung, die ich Ihnen vorgestellt habe,
sowas kann man eigentlich sinnvoll nur machen, wenn man sich so ein
Test -Framework zur Hilfe nimmt.
Und eben vor allem diese parametrisierten Tests, das können Sie
mitnehmen, das ist sehr praktisch, wenn man viele Tests machen will.