Herzlich Willkommen zur Vorlesung Programmieren.
Dann sind wir mit dem ersten Teil fertig und ich komme zur
eigentlichen Vorlesung.
Und da geht es heute um das Testen und Searchens und letztendlich um
den Standard JUnit.
Falls Sie sich übrigens wundern, ich habe beim Hochladen der Folien
die Nummerierung ein bisschen angepasst.
Deswegen ist heute nochmal Nummer 14, obwohl es das letzte Mal schon
da stand und auch im Webcast so ist.
Aber dafür haben wir jetzt dann lückenlose aufsteigende Nummern, weil
ich eben auch Foliensätze noch zusammengefügt habe.
Das nur als kleine Info.
Nochmal ein Überblick.
Diese Grafik kennen Sie schon.
Es ist fast nichts mehr übrig, was wir hier oben rechts noch nicht
behandelt haben.
Und da ist eben beim Fehler finden jetzt hier das Testen das, worum es
heute gehen soll.
Und das ist auch Teil der Methodik.
Aber ich werde Ihnen auch ein paar konkrete Frameworks, ein konkretes
Framework, nämlich JUnit vorstellen und konkrete Operatoren wie zum
Beispiel die Assertions.
Dann sind die Lernziele für heute, dass Sie die Definition von solchen
Softwarefehlern kennen.
Die hatten wir das letzte Mal schon ein bisschen.
Wir wiederholen es aber heute nochmal und bringen das dann beim Testen
ins Verbindung.
Sie kennen dann die Kriterien für gute Testfälle und können anhand
derer selbst gute Testfälle erstellen.
Sie können diese Zusicherung anwenden.
Das sagt Ihnen jetzt noch nicht viel, aber das wird dann so im zweiten
Teil der Vorlesung drankommen.
Und Sie sollen vor allem unterscheiden können, welche Arten von
Fehlern man durch Testen entdecken kann und auch welche man nicht
entdecken kann.
Testen ist kein Allheilmittel, denn sonst, wenn gut getestete Software
immer fehlerfrei wäre, gäbe es sehr wenig Probleme.
Aber so ist es nicht.
Es gibt ja immer noch Probleme, obwohl getestet wird.
Aber trotzdem sollen Sie auch wissen, warum das notwendig und sinnvoll
ist.
Letztendlich können Sie dann auch hoffentlich das JUnit-Framework
einsetzen.
Das müssten Sie natürlich noch ein bisschen üben, aber in dieser
Vorlesungssitzung kommen hoffentlich die richtigen Links dran und das
Prinzip, damit Sie es verstehen.
Und als etwas fortgeschritteneres Konzept können Sie vor allem auch
parametrisierte Tests schreiben.
Übrigens fand ich es auch witzig, bei der Evaluation hat jemand
geschrieben, das sei doch irgendwie doof, dass am Anfang die Lernziele
erwähnt werden, am Schluss eine Zusammenfassung gemacht wird und in
der Mitte erzählt wird, da hätte man doch alles dreimal.
Das finde ich aber gar nicht.
Sie wissen es ja immer, Wiederholung ist sehr wichtig fürs Lernen.
Ich versuche nicht alles mehrfach zu sagen, aber am Anfang zu sagen,
was dran kommt, am Schluss das nochmal zusammenzufassen.
Also den Kritikpunkt kann ich nicht annehmen.
Genau, aber dann kommen wir auch gleich schon mal weiter zum
eigentlichen Thema Testen.
Kurze Umfrage, wem von Ihnen sagt Murphy's Law etwas?
Kurz melden, das dachte ich mir, das sind die meisten.
Es gibt es in mehreren Fassungen, aber ungefähr heißen alle so, if it
can go wrong, it will go wrong.
Wenn also irgendetwas möglich ist in irgendeiner Lebenssituation, in
irgendeinem technischen Gerät, irgendwo das was schief gehen kann,
dann muss man sich irgendwie damit anfreunden, dass es tatsächlich
auch irgendwann schief gehen wird.
Und diese Dinge, die wir uns jetzt anschauen, danach sind
Programmierfehler, die eben passieren können und deswegen können Dinge
dann schief gehen.
Ein nächster Begriff, hier sind die Softwarekatastrophen.
Das klingt erstmal so ein bisschen witzig, ist aber gar nicht witzig
gemeint, sondern sehr ernst, sogar todernst, wie wir gleich sehen
werden.
Wir haben bei uns auch ein Pro-Seminar, das nennt sich
Softwarekatastrophen, also am Lehrstuhl von Professor Reusner, bieten
wir im Sommer wieder an.
Das ist sehr interessant, dort schaut man sich einige der Fälle an,
die ich Ihnen jetzt hier auf den Folien zeigen werde.
Und dort wird dann untersucht, wie konnte es dazu kommen, zum Beispiel
hier bei diesem Bestrahlungsgerät, dass tatsächlich wegen eines
Softwarefehlers Leute an Überdosis von Strahlung gestorben sind in
medizinischen Behandlungen.
Das war damals so, dieses Tera-Gerät, das war eine Weiterentwicklung
eines anderen Bestrahlungsgerätes, wo man mechanische
Einstellmöglichkeiten durch Software ersetzt hat.
Sie sehen also dieses Terminal vorne dran, da konnte man dann mit der
Software einstellen, wie dieses Bestrahlungsgerät funktioniert.
Und man dachte, toll, die Mechanik geht ja irgendwann kaputt, so ein
Hebel, der verschleißt, der muss man irgendwie ölen und so.
Und Software, die geht ja nicht kaputt, die altert auch nicht und die
hat weniger Fehler.
War aber nicht so, die hatte tatsächlich Programmierfehler, die man
erst sehr spät bemerkt hat, nämlich erst nachdem einige Leute
gestorben sind.
Und auch durch so ein Glaube daran, dass die Software und der Rechner
keine Fehler machen, wurde auch sehr spät erst dort gesucht.
Aber es war so, dass tatsächlich ein Programmierfehler und irgendeine
zeitliche Bedingung, wenn zwei Tasten schnell gedrückt wurden, kam ein
unerwartetes Verhalten.
Und dann wurde die Strahlendosis auf, ich glaube, das zehntausendfache
erhöht.
Und das war natürlich dann fatal für die Leute, die in dem Moment die
Dosis abgekriegt haben.
Das war aber schon in den 70ern, das sieht man ja auch an dem Gerät,
etwas aktueller, auch schon eine Weile her.
Wenn Sie die Landkarte von Nordamerika kennen, wissen Sie, dass hier
oben nicht so eine große Bucht ist.
Das war 2003 durch einen Softwarefehler, dass dort ein großer Teil des
Stromnetzes ausgefallen ist und deswegen einige Millionen Menschen da
betroffen waren.
Sie können sich überlegen, das ist ein sehr dicht besiedelter Raum, da
wohnen viele.
Auch da gab es dann indirekt in der Folge Todesopfer und das wurde
auch durch Softwarefehler ausgelöst.
Der hätte eigentlich bei einer Überlastsituation einen bestimmten
Alarm auslösen sollen, hat es aber nicht getan.
Der letzte Fehler vielleicht, der ist Ihnen eher bekannt, weil er noch
nicht so lange her ist, der Heartbleed-Fehler, der eben zusammenhing
mit einer fehlerhaften Implementierung von einer SSL-Library.
Und ja, da ging es tatsächlich darum, dass man eben vergessen hatte,
bei einer Anfrage zu prüfen, ob die Größe, die angegeben wird, auch
tatsächlich die Größe ist, die dann an Daten geliefert wurde.
Und wenn man da eben falsche Daten hingeschickt hat, konnte man Daten
vom Server auslesen, die man eigentlich nicht zu Gesicht hätte
bekommen sollen.
Und da mussten sehr viele Updates ausgespielt werden, um das zu
reparieren.
Der ist wahrscheinlich auf sehr, sehr vielen Geräten bis heute nicht
repariert.
Und da kann man jetzt nicht sagen, dass da unmittelbar Leute dadurch
zu Schaden oder zu Tode gar gekommen sind.
Aber es ist trotzdem ein schwerwiegender Fehler, den Sie vielleicht
auch mal bemerkt haben oder in den Nachrichten gelesen haben.
Also was lernen wir daraus?
Man muss irgendwie mit Fehlern umgehen.
Und man könnte sagen, eine umfassende Fehlerbehandlung ist
erforderlich.
Idealerweise sollte man jede Benutzereingabe überprüfen und jeden
möglichen Fehlerfall behandeln.
Dass das in der Praxis manchmal nicht möglich ist, dürfte Ihnen auch
klar sein.
Stellen Sie sich vor, so eine SSL-Datenbank, Sie können nicht alle
möglichen Server anfragen, die jemals jemand bei irgendwelchen Servern
irgendwo auf der Welt macht, simulieren und prüfen.
Das geht nicht.
Aber es ist trotzdem wichtig, ausgiebig zu testen und so strukturiert
zu testen, dass man möglichst genau ausschließen kann, dass Fehler
auftreten.
Dazu noch ein weiteres Zitat, was ich Ihnen mitgebracht habe, und zwar
eins von Edgar Dijkstra.
Der hat mal gesagt, Program Testing can be a very effective way to
show the presence of bugs, but is hopelessly inadequate for showing
their absence.
Also kurz zusammengefasst, mit Testen kann man Fehler finden, aber man
kann sehr schlecht beweisen, dass keine Fehler da sind.
Das kennen Sie auch aus anderen Wissenschaften, etwas zu beweisen,
dass es existiert, ist sehr viel einfacher, als zu beweisen, dass es
nicht existiert.
Und ja, dieser Ausspruch, der ist auch schon etwas älter, der ist von
1972 schon.
Jetzt haben wir schon viel über Fehler gesprochen.
Sie erinnern sich vielleicht an die letzte Woche, dort haben wir
versucht, diesen deutschen Begriff Fehler ein bisschen genauer
aufzuschlüsseln und haben dort mehrere englische Begriffe dafür
gefunden.
Ich zähle sie ja nochmal auf, es ist nur Wiederholung.
Also wir hatten so den ganz unten, den Fehler, der passiert, ist der
Fault.
Sobald er gefunden wird, Defekt.
Und wenn er anerkannt wurde und irgendeine Nummer kriegt, ist es ein
Bug.
Und der Zustand, der dadurch verursacht wird, durch die ersten drei
möglicherweise, ist dann ein Error.
Und wenn der zu einem Verhalten führt, was unerwartet ist, dann nennt
man es Failure.
Das ist also Absturz oder Ausfall oder falsche Berechnung, kann ganz
unterschiedlich sein.
Dann hier nochmal die Literaturreferenz.
Dann ist natürlich für uns jetzt interessant, das ist ja alles so ein
bisschen natürlichsprachlich beschrieben und gerade beim letzten, dem
Failure, was heißt denn eigentlich erwartetes Verhalten?
Woher wissen wir denn, was das erwartete Verhalten von einer Software
ist?
Das ist natürlich erstmal intuitiv irgendwie klar.
Wenn man eine Software benutzt, dann weiß man irgendwie, wofür man sie
sich geschrieben hat oder gekauft hat.
Aber eigentlich möchte man das natürlich formal gerne haben und dafür
muss man eben Anforderungen spezifizieren.
Anforderungen heißen im Englischen Requirements und da gibt es auch
einen eigenen Forschungszweig dazu, das sogenannte Requirements
Engineering.
Ganz im Allgemeinen ist das eine Aussage über eine zu erfüllende
Eigenschaft oder erbringende Leistung von einem Programm oder einem
System.
Das wird ganz oft auch einfach in Text ausgedrückt, in natürlicher
Sprache und die Anforderungen sind aber auch definiert, was das heißt
in einem ISO-Standard.
Da habe ich Ihnen das mal herauskopiert.
Unter der passenden Nummer hier, Requirement, heißt ein Statement, das
ein Bedürfnis ausdrückt und irgendwelche Einschränkungen und
Bedingungen, die dranhängen.
Also man möchte irgendwas haben und dafür stellt man eine Anforderung.
Und dann, das ist jetzt sehr Ingenieurssprachlich ausgedrückt, eben
eine Eigenschaft oder Fähigkeiten, die so ein System haben muss oder
eine Komponente usw.
Oder ein Dienst, der halt so eine Vereinbarung erfüllt, ein Standard
oder ein anderes formelles Dokument.
Oder drittens noch, es sind irgendwelche Kriterien aufgelistet, die
erfüllt werden müssen und letztendlich vielleicht noch so eine
Eigenschaft, die in einem Produkt drin ist, die tatsächlich in einem
Vertrag geregelt ist oder formell irgendwo beschrieben ist.
Das ist also eine Anforderung.
Es geht dort immer darum, welche Eigenschaften erwünscht sind oder
sogar vertraglich festgelegt sind.
Ja und so eine Anforderungsspezifikation ist dann einfach ein
Dokument, was diese Anforderung enthält und zwar wofür?
Für ein System, das, denkt man sich, dann ist eher etwas Großes oder
eine Komponente ist eher ein Teil von vielleicht einem größeren
System, die man dann wiederum zusammensetzen kann.
Das Ganze, wie gesagt, ein ISO-Standard.
Ich habe Ihnen jetzt nochmal hier ein kleines Beispiel mitgebracht, an
dem man sieht, wie von einerseits Requirements man zu einem Programm
kommt, aber in dem Programm Fehler drin sind.
In diesem Fall halt Fehler, die nach Fault übersetzt würden und die
dann zu einem Fehlverhalten, zu einem Ausfall führen.
Also wir haben hier eine Spezifikation, ganz einfach.
Wir möchten ein Element X in einem Array A suchen und zwar nicht ganz
am Anfang, sondern wir fangen an der Stelle P an.
Und die Methode soll einem das kleinste I größer P zurückgeben, an
dessen Stelle dann eben das gewünschte Element steht.
Und wenn gar kein solches Element drin ist, soll minus 1 zurückgegeben
werden.
Also wir machen das mal hier für Zahlen am Anfang und sagen, wir
machen so eine Search-Funktion, die ist ganz einfach, die ist auch
static, weil die eben unabhängig von dem Zustand des Objektes ist.
Wir geben ein Array A rein, sagen eben hier P ist, wo wir anfangen zu
suchen und X ist das, was wir suchen.
Also fangen wir mal an, darüber zu iterieren über dieses Array.
Ja, sieht gut aus, schreiten wir irgendwie fort.
Und dann, wenn wir an der Stelle sind, dann geben wir das P zurück.
Ja, das sieht ja auch irgendwie sinnvoll aus.
Und wenn wir hier durchgelaufen sind, ohne dass ein Return passiert
ist, kommen wir halt hier raus und dann wird minus 1 zurückgegeben.
Ja, sieht doch eigentlich ganz gut aus.
Und dann machen wir vielleicht nochmal einen Test hier, machen ein
Array mit 2, 5, 4, 3, 2, 3 und sagen, wir fangen an, an Index 4 zu
suchen und suchen das Element 3.
Also müsste irgendwie hier 5 rauskommen, weil an der Stelle 5 steht es
eben.
So, jetzt, wenn Sie vielleicht aufgepasst haben hier oben, wo ich so
schnodderrecht drüber weggegangen bin und gesagt habe, das sieht doch
ganz gut aus, so ganz richtig stimmt es nicht.
Nämlich der erste Fehler, den wir gemacht haben, der ist hier in der
Schleife.
Wir fangen nämlich am Anfang des Arrays an zu suchen, aber oben in der
Spezifikation steht ja, wir sollen bei Index P anfangen.
Also, man hat so eine Vorschleife halt mal hingeschrieben, man fängt
bei 0 an, weil es ist standardmäßig so, aber der erste Fault, denn P
wäre richtig gewesen.
Der zweite, den habe auch ich erst als letztes gesehen, als ich mir
das angeguckt habe.
Hier wird tatsächlich das Element P zurückgegeben, das ist ja der
Startindex, der interessiert uns nicht, den geben wir ja rein.
Uns interessiert, wo das gefundene Element steht.
Eigentlich wäre I richtig gewesen.
Und auch bei der Schleife haben wir noch einen Fehler gemacht, so ein
klassischer off by one.
Wir gehen bis kleiner gleich von der Länge, aber eigentlich müsste es
bis kleiner Länge sein, denn ein Array, was die Länge n hat.
Und wenn man bei 0 anfängt zu zählen, darf man eben nur bis N-1 gehen.
Klassischer Fehler.
Und das kommt dann eben dann unten auch dazu, dass hier 4
zurückgegeben werden, dass hier 5 zurückgegeben werden kann, obwohl es
eigentlich 5 sollte es sein und 4 wird aber zurückgegeben.
Und das ist dann in dem Fall ein Fehler, der entsteht dadurch, dass da
oben mehrere Faults waren.
Jetzt ist natürlich die Frage, wie kommt es dann zu fehlerhafter
Software.
Wir haben jetzt am Schluss irgendwie gemerkt, okay, da kommt ein Wert
zurück, der passt irgendwie nicht.
Und wir haben jetzt auch in dem Beispiel gesehen, klar, der wurde
durch diese Faults ausgelöst.
Wir hatten jetzt drei Stück, wir wissen nicht mal, welcher genau dafür
verantwortlich war und ob man alle entfernen muss oder ob, wenn man
einen entfernt, vielleicht dann doch wieder was anderes Falsches
rauskommt.
Naja, also das ist jedenfalls immer eine Möglichkeit, dass direkt von
so einem Fault dann fehlerhafte Software mit unerwünschtem Verhalten
ausgelöst wird.
Was aber auch sein kann, ist, dass man sich schon in der Anforderung
geehrt hat.
Das war bei uns jetzt in dem Beispiel nicht der Fall.
Aber oft ist es auch möglich, dass die Anforderung falsch geschrieben
ist, dass die möglicherweise gar nicht erfüllbar ist, dass da
widersprüchliche Aussagen drin stehen und dass man das gar nicht
implementieren kann.
Das gibt es auch.
Natürlich ist es auch möglich, dass man die Anforderung falsch
interpretiert.
Man hat irgendwie was anderes gelesen, als derjenige, der es
geschrieben hat, eigentlich im Sinn hatte.
Oder es ist auch möglich, dass man was schreibt und falsch
dokumentiert.
Also das Programm hat ein bestimmtes Verhalten, aber man schreibt noch
einen Dokumentationstext dazu und der ist falsch.
Das sind jetzt also alles mögliche Gründe, warum eine Software
fehlerhaft sein kann.
Und der erste Punkt ist der, den wir uns heute vor allem anschauen
wollen, denn der ist gut durch Testen behebbar und auffindbar.
Also Testen hilft, solche Softwarefehler zu finden und vor allen
Dingen eben diese Fehler, die in der Entwicklung geschehen.
Die anderen Fehler unten drunter, die kann man teilweise auch mit
Testen finden, sind aber wesentlich schwerer zu finden und manchmal
auch nicht möglich.
Was auf jeden Fall wichtig ist, ist, dass jedes Programm getestet
werden muss.
Das erscheint uns irgendwie als angehendes Softwareingenieur
selbstverständlich, aber ich habe es schon oft genug erlebt bei
Industriepartnern, die halt sagen, naja, wenn wir zu wenig Zeit haben,
dann machen wir lieber irgendwie das Programm vollständig und sparen
lieber beim Testen oder lassen das Testen ganz weg, weil das kostet ja
nur zusätzlich Geld und Zeit.
Und auch ohne das Testen haben wir ja am Ende ein fertiges Produkt.
Das ist natürlich fatal, aber wird leider häufig so gemacht.
Genau, dann ist die Frage, wie geht man denn beim Testen vor?
Hatten wir teilweise schon letzte Woche.
Ich möchte es nochmal wiederholen.
Typischerweise geht man zweistufig vor.
Man überlegt sich, welche Eingabedaten hat man denn und muss dabei
nicht nur die Standardfälle, sondern auch die Randfälle betrachten,
auch die mit ungültigen Eingaben und wählt sich somit die Testfälle
aus.
Und die Daten, die man dann wählt, sollten aber irgendwie
repräsentativ sein.
Das hängt dann davon ab, wie das die jeweilige Umgebung oder das
Anwendungsszenario der Software ist, was repräsentativ heißt.
Man kann nicht alle Situationen abdecken, weil oft ist es viel zu
komplex, aber man muss versuchen, sie alle abzudecken.
Und vor allen Dingen die Situation, wo man schon weiß, dass es vom
erwarteten Verhalten abweichen könnte, die sollte man sich vor allem
anschauen.
Ich habe Ihnen jetzt noch wiederum ein ganz einfaches geometrisches
Beispiel gebracht, an dem Sie mal selbst überlegen können, was gute
Testfälle sind.
Wenn Sie jetzt die Folien schon haben, zum Download der Nachteil, da
steht die Lösung jetzt schon drin, aber das war auch ein Kommentar in
der Evaluation, dass bei den Quizfragen die Lösung doch bitte
mitgeliefert werden sollte.
Aber ich habe jetzt nicht extra einen Spoiler-Alert eingebaut.
Das erwarte ich jetzt von Ihnen, dass Sie da nicht schummeln und schon
nachschauen.
Also was wollen wir denn hier machen?
Wir möchten ein Programm schreiben, wo der Benutzer drei ganze Zahlen
eingibt.
Das sind jeweils die Längen von einem Dreieck und das Programm soll
mir dann sagen, ist das, was ich eingegeben habe, ein gleichseitiges
Dreieck, ein gleichschenkliches, nichtgleichseitiges oder ein
unregelmäßiges, also was keine der Eigenschaften erfüllt.
Die Frage jetzt an Sie wäre, was sind denn gute Testfälle dafür?
Sie sehen, da sind schon ein paar Zeilen drin.
Wie üblich dürfen Sie sich das jetzt kurz überlegen.
Wenn Sie ein, zwei Minuten überlegt haben, diskutieren Sie es
vielleicht mit irgendwelchen Leuten in Ihrer Nähe und dann fassen wir
mal zusammen, was Ihnen so eingefallen ist.
Gut, also was ist Ihnen eingefallen?
Wer möchte mir eine Lösung sagen?
Bitte einfach per Handzeichen melden und dann bitte laut und deutlich
sagen.
Ja, zum Beispiel negative Zahlen.
Das ist im Prinzip ein ungültiger Wert, das weiß man, aber hier steht
nur ganze Zahlen, also ist die Spezifikation schon irgendwie nicht
präzise genug.
Dann müsste man schon sagen, ja, das ist ein guter Testfall.
In dem Fall würde man nicht die Implementierung testen, sondern eher
sogar die Spezifikation.
Aber wahrscheinlich wird derjenige, der das implementiert hat, gemerkt
haben, dass negative Zahlen unsinnig sind und dann vorgesorgt haben.
Also ist ein guter Testfall, weil er den ungültigen Eingabe prüft.
Weiter?
Ja, genau, zum Beispiel, also die Antwort war verschiedene
Permutationen von demselben Dreieck.
Also ich gebe an 2, 2 und 5 oder 2, 5 und 2, 5, 2 und 2 sowas.
Das sollte ja immer dasselbe liefern.
Genau, also verschiedene Permutationen von demselben ist auch eine
gute Art, Testfälle auszuwählen.
Und vielleicht noch einen Beitrag, also es gibt sehr viele.
Noch irgendwas, was ist Ihnen noch eingefallen?
Da oben vielleicht, ja.
Ich verallgemeinere das und sage gültige Dreiecke, also gültige
gleichständige 1, 1, 25.
Also ich decke es mal hier auf.
Natürlich sollen Sie auch die gültigen Fälle testen.
Die sollen richtig funktionieren.
Und dann eben werden hier schon so die etwas ausgefalleneren Fälle,
sogenannte entartete Dreiecke, so nennt man die in der Mathematik, wo
eine Seite null ist, also eigentlich nur eine Linie zwischen zwei,
Strecke zwischen zwei Punkten.
Die negative Länge wurde am Anfang auch schon gewählt.
Dann irgendwie, dass die Dreiecksungleichung stimmen muss, kann man
hier unten auch noch überprüfen mit Eingaben, die das eben erfüllen
oder verletzen und so weiter.
Sie sehen schon ein sehr einfaches Beispiel.
Jeder von Ihnen weiß, was ein Dreieck ist, aber wenn man es
implementiert, muss man eben an ein paar Sachen denken.
Ich habe hier Ihnen mal so eine Beispiel-Implementierung aufgeführt
und gezeigt, wie so ein Prozess sein könnte.
Also ich fange mal an, sage, es gibt hier eine Enumeration und sage
die drei Typen, die im Beispiel vorne dran genannt wurden.
Das ist jetzt Englisch hierfür.
Equilateral heißt gleichseitig.
Isosceles heißt gleichschenklich und unregelmäßig ist dann eben other.
Und dann würde man eben anfangen, erstmal so eine Spezifikation zu
schreiben.
Die schreibt man vielleicht hier in den Code-Kommentar rein und sagt,
diese Klassifizierungsfunktion soll eben das zurückgeben, wenn alle,
also gleichseitig, wenn alle gleich sind, gleichschenklich, wenn zwei
gleich sind und größer 0 wurde hier sogar schon ausgeschlossen und ein
anderes, wenn eben die alle größer 0 sind.
Also schon dieser Fall mit dem negativen hat sich hier der Entwickler
überlegt und in die Spezifikation reingeschrieben, obwohl er in meinem
Text gar nicht drinne stand.
Dann kommt man aber irgendwann auf den Trichter, naja, es gibt ja noch
diese Geschichte mit der Dreiecksungleichung, die muss ja auch
irgendwie stimmen.
Und dann braucht man noch einen vierten Typ, nämlich dass die Eingabe
ungültig ist, auch wenn jetzt zum Beispiel jemand minus 1 eingibt.
Also ich mache noch einen vierten Fall hier invalid, wenn irgendjemand
minus 1 eingibt oder wenn jetzt die größte Seite länger ist als die
anderen beiden zusammen, dann gibt es kein Dreieck mehr.
Das können Sie sich geometrisch ja vorstellen.
Und dann muss man eben hier auch das Programm anpassen und noch
invalid hinzufügen.
Das Ganze kann man dann auch testen.
Da kann man dann anfangen, hier zum Beispiel einen Testfall zu
schreiben, zum Beispiel für ein gleichseitiges.
Und hier kommt schon eine kleine Vorschau, kommt schon so eine
Assertion.
Hier soll anscheinend irgendwie überprüft werden, dass das Ganze
richtig klassifiziert wird.
Das Ganze dann auch für das gleichschenkliche Dreieck und Sie sehen
Test 1, Test 2, das kann beliebig weitergehen.
Irgendwann zwischendrin macht man mal so ein entartetes Dreieck, das
halt so auf einer Linie liegt.
4, 4, 8, das stimmt gar nicht mal.
Entschuldigung, 4, 4, 8 gilt noch, das ist noch ein entartetes
Dreieck.
Aber 4, 4, 9 zum Beispiel wäre ungültig.
Okay, das soll dann ungültig rauskommen.
Kommt darauf an, das müssen wir noch in der Spezifikation nachschauen,
ob solche entarteten Dreiecke zählen oder nicht.
Dann je nachdem, sollte dieser Test fehlschlagen oder nicht.
Also Sie haben es schon gesehen, ich kann hier beliebig viele
Testfälle machen, natürlichen Zahlen sind unendlich groß, ganze Zahlen
haben wir sogar nur gesagt, gibt es unendlich viele.
Also ich kann nie im Leben unendlich viele Testfälle schreiben.
Aber ich kann trotzdem, wenn ich Testfälle schreibe, gucken, dass die
gewisse Eigenschaften haben, die vorteilhaft sind.
Zum Beispiel, ein Testfall sollte möglichst immer nur eine spezifische
Eigenschaft abfragen.
Also entweder abfragen, ob ein Dreieck gleichschenklich richtig
erkannt wurde oder ob eine negative Zahl richtig erkannt wurde, aber
nicht beides in einem Test.
Wenn der nämlich fehlschlägt, weiß man nicht, was wurde denn jetzt
falsch erkannt.
Also möglichst nur eine Eigenschaft pro Test abprüfen.
Die Tests sollten auch möglichst kurz sein, sodass man auch versteht,
was sie tun, auch prägnant sein und ganz wichtig, sie sollten isoliert
voneinander ausführbar sein.
Also zwei Tests sollten nicht voneinander abhängen.
Da kommen wir nachher bei JUnit auch nochmal gleich dazu.
Auch Tests, haben Sie gesehen, sind Codes, sind ganz normale Methoden
erstmal und deswegen müssen auch die gut dokumentiert sein.
Und am besten soll, wenn man noch ein weiteres Spezifikationsdokument
hat, zum Beispiel so ein Pflichtenheft oder sowas, sollte in dem Test
drinnen stehen, hier beziehe ich mich jetzt auf die Eigenschaft F087
aus dem Pflichtenheft.
Weiterhin sind Testfälle dann gut, wenn man auch Testframeworks
benutzt, wie zum Beispiel JUnit, was ich Ihnen nachher vorstelle und
nicht alles per Hand schreibt.
Und Tests sollten auch möglichst vorausschauend sein.
Ist auch schwierig, man kann nicht auf alle möglichen Änderungen, die
jemals stattfinden, schon mal vorausschauend reagieren.
Aber wenn man weiß, dass ein Programm möglicherweise was geändert
wird, dann sollte man den Test so schreiben, dass nicht der ganze Test
dann nutzlos wird, wenn das Programm mal an einer Stelle geändert
wird.
Tests folgen dann häufig einem Schema und das ist auch später bei
JUnit so.
Zuerst kommt die Initialisierung.
Das heißt, ich muss irgendwelche Daten zusammenstellen, die ich
brauche, irgendwelche Objekte erstellen, eine Datenbankverbindung
aufbauen, sowas.
Dann den Test durchführen, dann überprüfen, ob das Ergebnis dem
Erwarteten entspricht und dann am Schluss finalisieren, zum Beispiel
temporäre Dateien löschen, eine Datenbankverbindung wieder abbauen,
ein Ergebnis zurückliefern und dann eben berichten, ob der Test
erfolgreich war.
Man kann also sagen, dass Testen, also jetzt nochmal als
Begriffsdefinition, ein dynamischer Vergleich von tatsächlichem und
erwünschtem Verhalten ist.
Dynamisch heißt, ich muss Programme dafür ausführen, ich muss das
System laufen lassen.
Das klingt jetzt vielleicht für Sie trivial, weil so ein Programm, das
kann ich ja immer mal kurz ausführen.
Aber wenn Sie was testen wollen, was zum Beispiel voraussetzt, dass
mehrere Rechner, die in einem großen Netzwerk verteilt sind, in einem
bestimmten Zustand sind, dann kann dieses dynamische Ausführen auch
einen großen Aufwand bedeuten.
Da muss man dann für die Initialisierung möglicherweise sehr viel
Aufwand machen oder es ist sogar unmöglich, sowas dynamisch zu testen,
wenn Sie sagen, mein System, wie verhält sich das denn, wenn eine
Million Benutzer gleichzeitig darauf zugreifen, können Sie gar nicht
immer simulieren, dass tatsächlich so eine hohe Last auf Ihrem System
ist.
Da müssen Sie dann andere Dinge machen, die nicht dynamisch sind.
Aber die Ziele vom Testen sind vor allen Dingen nachzuweisen, dass die
Funktionalität erfüllt ist.
Es sollen auch Fehler erkannt werden.
Und der Unterschied zwischen diesen beiden, der ist psychologisch
tatsächlich wichtig.
Also erstmal zu zeigen, dass die Funktionalität erfüllt ist, ist
irgendwas Positives und Fehler erkennen klingt irgendwie negativ.
Psychologie spielt bei der Entwicklung von Programmen natürlich auch
eine Rolle und deswegen ist auch die Frage, ob Programmierer ihre
eigenen Programme testen sollten, eine gültige Frage.
Also nicht nur, weil man vielleicht sagt, der Programmierer meint eh,
er programmiert fehlerfrei und wird nie zugeben, dass er Fehler
gemacht hat, aber es ist auch so, Sie kennen selbst, wenn Sie den Code
schon sehr gut kennen, wenn Sie den selbst geschrieben haben, dann
werden Sie irgendwann auch blind und sehen nicht mehr, dass da
vielleicht offensichtliche Fehler drin sind.
Geht einem ja bei anderen Dingen auch so, wenn man Texte schreibt,
dann merkt man den Rechtschreibfehler erst, wenn man hier in der
Vorlesung steht und der an die Wand projiziert wird.
Also im Allgemeinen sollten Programmierer nicht nur die Programmierer
die Programme testen, die sie auch geschrieben haben, aber manchmal
ist es halt aus organisatorischen oder Kostengründen dann doch
erforderlich, dass die testende Person eben auch die ist, die den Code
geschrieben hat.
Das ist nicht ideal, aber lässt sich oft nicht vermeiden.
Dann noch eine Definition, was ein Testfall ist.
Testfall ist immer eine Kombination von einer bestimmten Eingabe und
einer erwarteten Ausgabe und noch ein paar Randbedingungen, die dann
da gelten und für jeden Testfall braucht man eigentlich auch eine
Begründung, warum man den jetzt genau gewählt hat, warum der
repräsentativ ist, warum der sinnvoll ist und welchen Fehler er
aufzeigt.
Wenn Sie hier eine Folie haben, was testen eigentlich ist, dann gibt
es natürlich auch eine Folie, was testen nicht ist.
Testen ist nicht immer Fehler-Lokalisierung.
Also wenn Sie jetzt wissen, okay, das Dreieck wurde falsch erkannt,
sagt Ihnen der Test normalerweise nicht und dein Fehler war in Zeile
37 deines Programm-Codes.
Das müssen Sie selbst finden.
Das nennt sich dann Fault-Lokalisation.
Natürlich gibt es auch dafür Unterstützung.
Debugging hilft Ihnen dabei.
Aber Testen an und für sich lokalisiert die Fehler erstmal nicht.
Natürlich behebt das Testen die Fehler auch nicht.
Das dürfte klar sein.
Da müssen Sie selbst den Code verändern.
Und Testen ist auch nicht die einzige Methode, um Fehler zu finden und
zu beheben.
Es gibt noch weitere.
Zum Beispiel die statische Code-Analyse.
Sie haben auf den vorhergehenden Folien gesehen, Testen ist ein
dynamisches Verfahren.
Ich muss den Code ausführen.
Statische Code-Analyse ist so, dass Sie den Code nicht ausführen
müssen.
Die einfachste Art ist, ich lese es mir nochmal durch oder ich hole
einen Kollegen, der auch in den Code reinschaut.
Das ist die manuelle Analyse.
Aber es gibt natürlich auch automatisierte Verfahren.
Die einfachsten kennen Sie bei Ihnen in Eclipse, wenn Ihnen CheckStyle
automatisch die Zeilen einrückt oder wenn Ihnen sofort beim Eingeben
einer Funktion die Entwicklungsumgebung sagt, Moment mal, die Variable
hier, die ist gar nicht deklariert, bitte macht das doch mal.
Das ist eine statische Analyse.
Das wird allein am Code geguckt, ohne dass das Programm laufen muss.
Und das gibt es dann in verschiedenen Stufen bis hin zu mathematischen
Beweisen, wo tatsächlich durch mathematische Beweisverfahren, wie Sie
sie kennen aus der Mathe-Vorlesung, bewiesen wird, dass ein Programm
seiner Spezifikation genügt.
Das Ganze nennt man dann formale Verifikation, kommt bei Ihnen dann
später noch in der theoretischen Informatik dran.
Ein weiteres wichtiges Verfahren ist das sogenannte Review.
Da treffen sich mehrere Entwickler und gehen eben den Code durch,
schauen sich den an, da wird auch Protokoll drüber geführt, da gibt es
recht ausgefeilte Verfahren dafür und das ist ein gängiges Mittel zur
Qualitätssicherung und auch zur Fehlerbehebung, Fehlerentdeckung und
so weiter.
Inspections ist sowas ähnliches, dort wird eben der Code angeschaut,
ein Walkthrough ist auch eben sowas, wo mehrere Entwickler
zusammenkommen, dann geht einmal durch, durch alle möglichen
Ausführungspfade und schaut sich an, wie der Code, in welchem Zustand
der ist.
Jetzt wissen Sie schon, was Testen ist und was es nicht ist.
Ich habe hier nochmal ein paar Kategorien von Tests aufgeschrieben,
auf die ich jetzt dann nicht mehr genau eingehen werde.
Was wir uns vor allen Dingen anschauen, sind hier die funktionalen
Tests.
Also wir gucken uns an, ob ein Programm sich korrekt bezüglich seiner
Spezifikation verhält.
Dann kann man natürlich noch sich weitere Sachen anschauen, was
passiert denn, wenn das Programm Nebenläufigkeit enthält.
Das wird Ihnen später in der Vorlesung Programmierparadigmen vor allen
Dingen begegnen.
Das muss aber auf jeden Fall auch getestet werden, ist nochmal eine
ganz eigene Komplexitätsklasse.
Ja, wo es funktionale Tests gibt, gibt es natürlich auch nicht
funktionale Tests.
Ich habe vorhin schon gesagt, was passiert denn, wenn eine Million
Benutzer gleichzeitig mein Programm aufrufen?
Ist das ein Problem für den Speicher?
Wie groß muss die Maschine sein, damit es so und so viele Benutzer
aushält?
Das wäre der erste Punkt Performance.
Ist Ihnen ja sicher bekannt, dass das bei uns am Lehrstuhl, bei
Professor Reusner eine große Rolle spielt in der Forschung.
Auch die Sicherheit spielt eine große Rolle.
Wie sicher ist denn ein System?
Da gibt es ganz verschiedene Sicherheitsbegriffe, also die sind auch
einteilbar in Security und Safety.
Das ist davon, dass Ihre Daten nicht dorthin kommen, wo sie nicht hin
sollen oder dass niemand zu Schaden kommt.
Das wird alles unter diesem Begriff zusammengefasst.
Benutzbarkeit heißt eben, wie es in der Interaktion mit Menschen ist.
Interoperabilität, wie es in der Interaktion mit anderen Programmen,
anderen Systemen ist.
Und Zuverlässigkeit heißt auch, in wie viel Prozent der Fälle ist denn
mein System verfügbar und in wie viel Prozent der Fälle gibt es
richtige Ergebnisse zurück.
Man kann Tests auch noch darüber einteilen, was man vom System weiß.
Wenn man von einem System nur die Spezifikationen und die
Anforderungen kennt, kann man es nur mit Blackbox-Tests testen, weil
man eben nicht weiß, wie die Implementierung im Inneren aussieht, kann
man nur die Schnittstellendefinitionen, die von außen sichtbar sind,
verwenden, um es zu testen.
Wenn man die Möglichkeit hat, die Implementierung einzusehen, kann man
auch Whitebox-Tests machen.
Dort kann man zum Beispiel schauen, dass alle Zeilen im Code abgedeckt
sind oder alle Verzweigungen einmal abgedeckt sind.
Aber dafür muss man wissen, wie die Struktur des Programms ist.
Deswegen ist es sozusagen eine weiße Kiste, in die man reinschauen
kann.
Dann kann man Tests noch daher unterscheiden, welches Strukturelement
man sich anschaut.
Das kleinste ist die Unit.
Da kommen wir dann auch bei JUnit wieder darauf zurück, denn das ist
eben fürs Unit-Testen geeignet.
Integrationstests gucken sich dann an, wie Komponenten miteinander
funktionieren.
Und wenn dann eben die Komponenten völlig zusammengesetzt sind zu
einem funktionierenden System, hat man dann einen Systemtest.
Ich habe schon ein paar Mal gesagt, Testen ist nicht einfach.
Das kostet Zeit und Geld.
Und ich möchte hier noch ein paar Gründe anführen, warum Testen
schwierig ist.
Und vor allen Dingen, wann ein Testfall, den Sie erstellen, ein guter
Testfall ist, das kann man an ein paar Kriterien festmachen.
Also erstmal könnte man sich ja überlegen, ein Testfall ist gut dann,
wenn er einen Fehler findet, weil wir möchten ja Fehler finden.
Und was ein Fehler ist, kriegen wir nur mit, wenn wir eine
Spezifikation haben und vergleichen können, ob die verletzt wird.
Diese Eigenschaft berücksichtigt aber einen wichtigen Fall nicht, denn
das ist kein guter Testfall für ein fehlerfreies Programm.
Denn wenn ein Test mal überprüfen soll, ob ein Programm die erwartete
Ausgabe bringt, dann findet der natürlich nie einen Fehler, aber zeigt
halt, dass es immerhin fehlerfrei funktioniert im Fall, wo es erwartet
ist.
Und deswegen kann man nicht sagen, es ist ein schlechter Testfall, nur
weil er keinen Fehler findet.
Man kann es ein bisschen abschwächen und dann sagen, die Fähigkeiten,
wahrscheinliche Fehler mit angemessenem Aufwand zu finden, macht einen
guten Testfall aus.
Und das kann man auf jeden Fall unterschreiben, denn es kostet eben
Zeit und Geld, Tests zu schreiben und auszuführen und auszuwerten.
Und man kann sich sogar überlegen, dass man die Fehlersuche dann
irgendwann abbricht, je nachdem, ob man meint, man hat jetzt alle
schwerwiegenden Fehler gefunden und man hat vielleicht 95 Prozent der
Fehler gefunden und für die letzten 5 Prozent bräuchte man nochmal
genauso lange wie für die ersten 95.
Dann kann man sich überlegen, ob man das dann wirklich bis ins Letzte
testet oder da eine Abwägung trifft.
Dann gibt es sogenannte Testsuiten, das sehen wir nachher bei JUnit,
da gibt es das auch.
Das sind eben Gruppierungen von Tests und die sollte man möglichst
klein halten und eben auch die Testfälle so kurz wie möglich und so
lang wie nötig, damit das eben übersichtlich bleibt.
Ein Test sollte auch so geschrieben sein, dass man die Fehler dann
tatsächlich leicht lokalisieren kann, also dass eben die Fehlermeldung
einem schon Hinweise darauf bietet, wo der Fault ist, der das
ausgelöst hat.
Und Sie sehen, überall hier wird verwiesen auf die erwartete Ausgabe
und um die zu definieren, brauchen wir eben eine Spezifikation.
Gut, dann ist noch eben die Frage, wie man solche Tests auswählt.
Man will eigentlich die Testfälle auswählen, die gut sind und eine
gute Kosten-Nutzen-Abwägung haben.
Und eigentlich kann man die natürlich nie ideal finden, deswegen muss
man sie approximieren.
Man muss die Eingabedaten dazu irgendwie in Blöcke gruppieren, dass
möglichst jedes Element eines Blocks denselben Fehler provoziert und
das Ganze macht man dann auf Basis von Anforderungen und halt vom
Wissen um typische Fehler und möglicherweise auch durch irgendwelche
zufälligen Eingaben.
Dafür braucht man aber Wissen um die Domäne, in der das Programm
arbeitet.
Man muss die Anforderungen kennen, man muss auch wissen, sind
vielleicht in früheren Projekten schon solche Fehler entstanden.
Das hatten wir letzte Woche in der Vorlesung.
Programme, die früher schon mal Fehler gezeigt haben, zeigen
wahrscheinlich in der Zukunft wieder Fehler.
Und dann die Testfälle anhand von Nutzerprofilen zufällig zu wählen,
ist auch nur dann sinnvoll, wenn man weiß, Benutzer machen
normalerweise solche Eingaben in diesem Wertebereich und in dieser
Häufigkeit, dann kann man anhand davon zufällige Eingaben erstellen.
Strukturbasierte Tests geht eben nur bei Whitebox-Testing, wo man auch
reingucken kann in den Code und dann kann man solche Bedingungen
aufstellen, wie ich möchte jede Zeile einmal ausgeführt haben.
Jede Bedingung muss einmal wahr und falsch werden und das sind dann
sogenannte Abdeckungskriterien, Code Coverage.
Die Frage ist dann immer noch, wann sind wir denn fertig mit dem
Testen?
Man könnte sagen, na gut, wenn wir keine Zeit mehr haben und das ist
natürlich ein nutzloses Kriterium.
Man kann ja einfach auch nichts tun die ganze Zeit und warten, bis die
Zeit rum ist.
Dann hat man gar nichts gemacht und es ist trotzdem ein Kriterium, was
erfüllt ist, aber es ist leider in der Praxis Kriterium Nummer 1.
Es wird vorgegeben, so und so viel Zeit ist zum Testen und wenn die
Zeit um ist, dann ist auch der Test um.
Man könnte natürlich sagen, wir sind fertig, wenn keine weiteren
Fehler mit existierenden, also schon existierenden Tests gefunden
werden.
Das ist aber auch nutzlos, denn das hängt sehr davon ab, welche Tests
ausgewählt sind.
Wenn man die falschen Tests auswählt und sagt, naja, die finden alle
keinen Fehler, wir sind fertig, das ist auch nicht gut.
Der nächste Punkt wäre, man hat so ein strukturelles Kriterium, geht
wie gesagt nur bei Whitebox-Testing, also jede Zeile wird einmal
ausgeführt.
Das kann nützlich sein, wenn es einen Zusammenhang mit Fehlererkennung
gibt und das nützlichste Kriterium wäre eigentlich, wenn eine vorher
spezifizierte Anzahl von Fehlern gefunden wurde, aber das erfordert
sehr gute Abschätzung und ist zwar das beste Kriterium, aber sehr
schwierig zu schätzen, wie viele Fehler so ein Programm üblicherweise
hat.
Also bis hin zu fast unmöglich, würde ich fast sagen.
Also wie geht man genauer vor, um solche Testfehler auszuwählen?
Man hat dafür Strategien.
Entweder gruppiert man sich die Daten, die die Eingabeparameter für
das Programm darstellen, die vergleichbar sind.
Das haben wir zum Beispiel beim Dreieck gemacht.
Also wir haben gesagt, vergleichbar sind alles gleichschenkliche
Dreiecke.
Da gehört 2, 2, 5 dazu, 4, 4, 6 und so weiter.
2, 2, 5 ist Unsinn.
2, 2, 1.
Und die führen dann eben zu vergleichbarem Verhalten und deswegen
haben wir sie gruppiert.
Man muss sich auch die Ränder anschauen, also zum Beispiel was
passiert, wenn ich den größten Integerwert angebe, was passiert, wenn
ich eine leere Eingabe mache, wenn ich 0 reingebe, wenn die Liste nur
ein Element lang ist, wenn das Array 0 ist und so weiter.
Also diese Randfälle muss man sich immer anschauen und diese
Programmbasierten Strategien gehen wie gesagt nur bei Whitebox-Testing
und sind verschiedene Kriterien dafür.
Ein Begriff, der dabei eine große Rolle spielt, ist testgetriebene
Entwicklung.
Viele von Ihnen werden sich jetzt überlegt haben, ja wann schreibe ich
denn die Tests?
Mache ich das ganz am Ende, wenn ich fertig bin?
Mache ich das mal so zwischendrin?
Sie kennen es vielleicht selbst, Sie coden was, dann schreiben Sie
vielleicht so eine kleine System Out Print Line zwischendrin, um mal
zu sehen, was ist denn gerade so der Zustand von meinem Programm?
Und da ist eigentlich die Frage, wie geht man daran?
Dass ausgiebiges Testen unerlässlich ist, das wissen Sie jetzt bereits
von den Folien und trotzdem unterlassen es die Leute.
Dummerweise, habe ich auch schon erklärt, aus Zeitmangel und dadurch
gerät man ja eigentlich in so einen Teufelskreis.
Also man schreibt wenige Tests, hat dann irgendwie eine geringere
Produktivität und hat einen instabilen Code.
Dadurch wird der Druck noch viel höher, es fertig zu bringen und je
mehr Druck man hat, desto weniger Tests schreibt man.
Das ist also keine gute Strategie, deswegen sollte man die Tests am
besten gleich zusammen mit dem Code schreiben oder noch besser, man
sollte die Tests schreiben, bevor man den Programmcode schreibt.
Und das Ganze nennt man dann Test getriebene Entwicklung, also man
schreibt, man überlegt sich, welche Funktion möchte ich denn erfüllen,
wie kann ich später testen, dass das erfüllt wird und schreibt zuerst
seine Tests.
Man kann das dann hier auf der rechten Seite mit einem Diagramm
illustrieren.
Am Anfang bin ich immer in einem Zustand, wo alle Tests okay sind,
also keiner schlägt fehl.
Vor allen Dingen, wenn ich gar keine Tests habe, ist das
trivialerweise der Fall und wenn ich irgendeine Funktionalität zu
meinem Programm hinzufügen will, füge ich zuerst mal einen Test hinzu,
der diese Funktionalität überprüft, obwohl ich schon weiß, dass der
Test fehl schlagen wird, weil ich den Programmcode ja noch nicht
geschrieben habe.
Dann schreibe ich solange eben an meinem Programm weiter, bis die
Tests erfüllt worden sind und dann bin ich wieder in diesem Zustand in
der Mitte, wo alle Tests okay sind und dann kann ich weitermachen.
Dann kommt mehr Anforderungen, mehr Funktionalität hinzu und wenn dann
irgendwann alles an Funktionalität drin ist, fange ich noch an, meinen
Code zu vereinfachen und mit Refactoring umzubauen.
Was Refactoring ist, habe ich Ihnen auf der nächsten Folie kurz
erklärt.
Bei Refactoring ist das Ziel, den Code so einfach wie möglich zu
halten.
Also vor dem Testen sollte man dann überlegen, dass diese Tests, die
dann später erfüllt werden sollen, durch möglichst einfachen Code
erfüllt werden sollen.
Also schon, wenn man schreibt, diesen Einfachheitsgedanken
berücksichtigen und wenn man dann fertig ist und wenn die Tests
erfüllt sind, kann man meistens noch weiter vereinfachen.
Man hat in objektorientierten Sprachen einige Operationen.
Viele von denen gibt es auch in den Frameworks, so wie zum Beispiel
Eclipse.
Da drückt man eben Alt-Shift-R, wählt Refactor aus und kann dann
diverse Dinge tun, zum Beispiel zwei Klassen aufteilen oder zwei
Klassen kombinieren.
Das geht ohne das Verhalten des Programms zu ändern normalerweise.
Man kann Methoden und Attribute oft in andere Klassen verschieben, vor
allen Dingen in Oberklassen, wenn man eben was generalisiert.
Das wissen Sie alle, das verändert das Verhalten des Programms in den
meisten Fällen nicht.
Man kann eigentlich auch jede Klasse, jede Methode umbenennen, so wie
man möchte.
Solange man das konsistent an allen Stellen im Code macht, ändert das
auch nicht das Verhalten.
Und wie gesagt, dafür gibt es in den Entwicklungsumgebungen dann
Hilfsmittel, dass das wirklich an allen Stellen geändert wird.
Methoden extrahieren und integrieren kann man dann auch natürlich, die
irgendwo mittendrin stehen.
Wenn man einen bestimmten Code hat, von dem man weiß, der ist
eigentlich eine eigene Methode, kann man die extrahieren.
Man kann Vererbung durch Delegation ersetzen und umgekehrt und
Schnittstellen einführen, wie wir es vor zwei Wochen in der Vorlesung
schon gehört haben.
Dieser Begriff Refactoring, der stammt aus dem Buch hier links unten
von Martin Fowler.
Das ist ein sehr gutes Buch, ist jetzt nicht 2018 erschienen, das ist
nur die zweite Auflage.
Das ist schon älter, aber ist eigentlich ein Standardwerk für jeden
Softwareentwickler und auf jeden Fall hier ein Lesehinweis für weitere
Informationen.
Dann kommen wir ganz am Schluss noch zu einem Verfahren, das ich
vorhin kurz angerissen habe, nämlich dieser statischen Analyse.
Und wir haben uns ja gerade am Anfang gefragt, wenn es so schwierig
ist, Testfälle auszusuchen, ja, warum testen wir nicht alle Eingaben
und haben schon selbst die Antwort gefunden, es sind viel zu viele und
die Testfälle kann man dann nicht mehr handhaben.
Manchmal sind es auch unendlich viele und es geht überhaupt gar nicht.
Die Laufzeit der Testfälle wird dann viel zu groß und man kann dann
eigentlich nicht sagen, dass man alle Eingaben testen kann.
Und bei der statischen Analyse kann man es eben doch irgendwie.
Das funktioniert dann nämlich so, dass man so ein Programm umwandelt
in eine logische Formel und versucht dann, diese Einhaltung der
Spezifikation nicht zu testen durch alle möglichen Eingaben, sondern
durch einen mathematischen Beweis.
Wie Sie alle wissen, ein mathematischer Beweis gilt dann normalerweise
für alle Werte im Definitionsbereich und Sie müssen es nicht für jeden
Einzelnen beweisen.
Dafür gibt es eben verschiedene Logiksprachen, habe hier nur mal ein
paar erwähnt, die Ihnen dann vielleicht in weiterführenden Vorlesungen
begegnen.
Und auch, was dann wichtig ist, damit diese Logiksprachen benutzt
werden können, muss auch schon die Spezifikation in formaler Logik
vorliegen.
Es reicht also nicht, die Spezifikation nur auf Englisch oder Deutsch
hinzuschreiben, sondern die muss auch schon in Formeln ausgedrückt
werden.
Und Sie sehen schon, das ist sehr komplex, das ist nicht immer
machbar, dauert auch sehr viel länger normalerweise als testen, aber
hat dann halt eindeutige beweisbare Aussagen.
Wird deswegen meistens in Hochsicherheitsbereichen nur gemacht, also
Kernkraftwerkssteuerung, Flugzeugsteuerung.
Es gibt eine Linie der Pariser U-Bahn, wo das Verhalten von einem
Sicherungssystem eben auch mit so einem System mathematisch bewiesen
wurde, dass das die Spezifikation erfüllt.
Und das Schöne ist, dieses Beweisen müssen nicht Sie immer machen,
manuell, wie bei einem mathematischen Beweis mit Papier und Stift,
sondern das geht auch rechnerunterstützt und zum Teil sogar
vollautomatisch.
Das Ganze nennt sich dann eben diese formale Verifikation.
Das aber wie gesagt nur als kleiner Ausblick auf die theoretische
Informatik.
Kommen wir jetzt wieder zu was ganz Praktischem zurück, nämlich zu den
Assertions.
Und dafür zur Motivation habe ich Ihnen ein Webcomic mitgebracht,
wurde auch in der Evaluation positiv bewertet, Memes und lustige
Bilder tragen auch irgendwie zur Qualität der Vorlesung bei.
Weiß nicht, ob Sie den Comic Geek & Poke kennen, rechts ist auch die
URL, können Sie mal nachschauen, gibt es noch viele andere.
Zwei Entwickler gucken sich Code an und sagen, da steht so eine
Zusicherung Assert drin, Assert False, also soll irgendwie zusichern,
dass irgendein Wert falsch ist.
Das ist doch gerade umgekehrt, sollte das nicht Assert True sein?
Kurz drüber nachdenken.
Da sagt der andere, wenn ich es umkehre, werden alle meine Tests dann
gehen auf Rot, also auf Fehlgeschlagen, Tests nicht erfüllt.
Ich habe zwar keine Ahnung, was es tut, aber es muss wohl hier falsch
sein, auch wenn True irgendwie intuitiv das Richtige wäre.
Und wir haben vorhin Test Driven Development gelernt, hier unten heißt
es dann Development Driven Tests, also man schreibt sich seine Tests
so, dass sie irgendwie nicht fehlschlagen und man weiß aber eigentlich
nicht, warum das eigentlich so ist.
So soll es natürlich nicht sein, das ist also nur ein Scherz, wie es
nicht laufen soll und wie man richtig mit solchen Zusicherungen
arbeitet, das stelle ich Ihnen jetzt auf den folgenden Folien vor.
Also solche Zusicherungen sind natürlich ein wichtiges Hilfsmittel für
die Vermeidung von Fehlern und man verwendet die erstmal, um
Vorbedingungen anzugeben, sogenannte Pre-Conditions, die also sagen,
wie müssen Parameter aussehen und globale Variablen, bevor eine
Methode ausgeführt werden kann.
Wo es eine Vorbedingung gibt, gibt es auch eine Nachbedingung.
Man soll also angeben, was nach so einer Methodenausführung gilt und
an zentralen Programmpunkten kann man noch solche Eigenschaften wie
zum Beispiel, was Variablen für Werte haben sollen, kann man
festhalten.
Und mit Zusicherungen kann man dann Korrektheitsaussagen sogar in
manchen Fällen mathematisch beweisen.
Ich habe Ihnen mal jetzt ein Beispiel mitgebracht, wo nur mit
mathematischen Formeln solche Zusicherungen beschrieben sind.
Jeder von Ihnen kennt aus der Schule noch das Skalarprodukt von zwei
Vektoren, also ich habe hier gleich den vollkommenen Code.
Wir haben die Vektoren hier als Arrays von Doubles und Sie wissen alle
aus der Schule, Skalarprodukt von zwei Vektoren kann man nur machen,
wenn die beiden Vektoren gleiche Größe haben.
Also haben wir hier schon mal eine Vorbedingung, die drinnen steht und
die mir sagt, eigentlich muss hier die Länge von diesen Arrays gleich
sein.
Dann gehe ich hier irgendwie über die Schleife drüber und ich habe
hier so eine mathematische Eigenschaft, die mir sagt, naja, hier
zwischen in der Schleife gibt es so eine Invariante, bei jedem
Durchlauf der Schleife ist diese Eigenschaft hier gegeben.
Nämlich, dass man da eben an der Stelle, wo man gerade ist, schon
diese Multiplikation stehen hat.
Und die Nachbedingung ist dann, dass es eben für alle Werte, die in
dem Vektor drinnen stehen, dann die Summe der jeweiligen Produkte das
Ergebnis ist.
Das ist jetzt kein ideales Beispiel, weil wie Sie hier sehen, ist die
Berechnung dieser Zusicherung genauso komplex wie das eigentliche
Berechnen des Ergebnisses, aber das ist nicht immer der Fall.
Denken Sie mal an andere mathematische Funktionen, wie zum Beispiel
die Primfaktorzerlegung.
Die kann recht komplex sein, aber wenn Sie die Primfaktoren haben,
auszurechnen, ob die wirklich die Zahl ergeben, ist sehr einfach.
Sie multiplizieren sie einfach auf, gucken, ob es alle Primzahlen
sind.
Okay, das ist wieder ein bisschen schwieriger.
Aber da können Sie recht einfach was überprüfen mit einer Zusicherung
und die Berechnung ist aber sehr viel schwieriger.
Was machen also solche Zusicherungen in Java?
Sie können mit solchen Zusicherungen dann erforderliche Bedingungen
zur Laufzeit überprüfen.
Dafür gibt es in Java ein eigenes Schlüsselwort, das nennt sich
assert.
Und da können Sie dann eine Bedingung einfach hintendran schreiben,
also einen bool'schen Ausdruck, und Sie können auch noch eine
Fehlermeldung hinzufügen, die dann ausgegeben wird, wenn diese
Zusicherung nicht erfüllt ist.
Also diese mathematische Zusicherung habe ich jetzt hier nochmal in
Java hingeschrieben, sieht auch ganz ähnlich aus.
Ich habe hier einen bool'schen Ausdruck und wenn der nicht erfüllt
ist, dann wird eben diese Zusicherung fehlschlagen und ich weiß, okay,
da war schon die Vorbedingung nicht gegeben von der Methode, dann kann
die nichts Korrektes berechnen.
Also mit dieses Assert können Sie direkt in den Programmtext
reinschreiben und Sie haben den Vorteil, dass Sie das zur Laufzeit
überprüfen können.
Also diese Java Virtual Machine, die kennt natürlich dieses
Schlüsselwort, respektiert das, haben aber auch den Nachteil, denn
wenn Sie jetzt da so eine schöne mathematische Formel reinschreiben
wollen in die Assertions, geht es natürlich nicht.
Sie müssen es in Java reinschreiben.
Häufig ist es natürlich einfacher, solche Zusicherungen in die
Kommentare zu schreiben, dann können Sie auch von mir aus so eine
mathematische Formel in LaTeX, Syntax oder irgendwas reinschreiben
oder Sie verweisen auf irgendeine Grafik oder ganz egal, haben dann
den Vorteil, Sie können dort alles reinschreiben, ist ja nur ein
Kommentar, aber das wird natürlich nicht mehr automatisiert von der
JVM überprüft, denn für die ist das nur ein Kommentar und wird
weggeschmissen.
Es gibt natürlich noch Aufsätze für Java, die dann diese Formeln, die
Sie möglicherweise in so einen Kommentar reinschreiben, auslesen und
dann eben diese mathematischen statischen Analyseverfahren machen.
Also in jedem Fall sind Assertions aber auch, selbst wenn sie nur in
die Kommentare geschrieben werden, eine gute Dokumentation, weil der
Methodennutzer eben weiß, was kann ich erwarten, die Vorbedingungen
und was muss ich dann liefern, die Nachbedingungen.
Das Ganze kennt man auch als Design by Contract.
Dort wird sozusagen ein Vertrag erstellt, wenn die Vorbedingung gilt,
muss auch die Nachbedingung gelten.
Kommt auch in den Programmierparadigmen in der Vorlesung dann genauer
dran.
Ein praktischer Tipp noch, standardmäßig werden diese Assertions gar
nicht ausgewertet, sondern Sie müssen eben, wenn Sie Java ausführen,
so einen Switch anschalten, dieses EA, Enable Assertions, oder halt in
Ihrer IDI irgendein Häkchen setzen und dann wird das auch tatsächlich
überprüft.
Also eine Zusicherung nochmal ist eine Eigenschaft, die an einer
entsprechenden Stelle gelten muss.
Die Precondition jetzt bei Java Methoden speziell muss vor der
Abarbeitung eines Methodenrums erfüllt sein.
Die Nachbedingung, die muss erfüllt sein, wenn die Methode fertig ist.
Und dann gibt es eben noch verschiedene Invarianten,
Schleifenvarianten haben wir schon gesehen, aber es gibt zum Beispiel
auch Klasseninvarianten, die müssen zu jeder Zeitpunkt gelten, wenn
ein Objekt von einer Klasse existiert.
Also vor und nach dem Methodenaufruf.
Hier so ein Beispiel, wie das aussehen kann.
Das ist jetzt tatsächlich so eine etwas formalere Sprache, die wird
von Java erstmal nur als Kommentar erkannt, die sogenannte Java
Modeling Language.
Aber wenn Sie jetzt einen Übersetzer hätten, der diese
Verifikationsfunktion eingebaut hat, der würde das erkennen.
Oder zum Beispiel könnten Sie dann einfach reinschreiben, wenn Sie es
wieder mit der Java-Syntax machen, dass da so eine Assertion dann
reinkommt.
Schleifeninvariante, wie gesagt, das muss immer am Anfang und Ende von
jedem Durchlauf von einer Schleife gelten.
Aber jetzt mal so ein Beispiel von einer Invariante.
Wie schreibe ich denn die mit so einer Java Assertion rein?
Neues Beispiel, wir wollen jetzt einfach Uhrzeit verwalten, haben eine
Klasse Time geschrieben, die besteht aus Stunde und Minute.
Na und wir wissen alle, Stunden sind irgendwo zwischen 0 und 24 und
Minuten sind zwischen 0 und 60.
Dann also hier 0 eingeschlossen und das jeweils auf der rechten Seite
wird nicht erreicht.
Das habe ich jetzt in einen Kommentar reingeschrieben, möchte das aber
mit Assertions explizit machen und sage eben jetzt hier im
Konstruktor, ich möchte sicherstellen, dass da die richtigen Werte
eingegeben wurden.
Dann habe ich unten noch so eine Addierfunktion, da möchte ich eben
das Gleiche sicherstellen und jetzt sieht man schon gleich, was hier
das Problem ist.
Natürlich, wenn ich addiere, kann es sein, dass ich über die 24
rauskomme oder über die 60 drüberkomme.
Das merke ich dann aber in dem Fall, weil ich eine Exception kriege,
die heißt dann Assertion Violated Exception oder so ähnlich und sehe,
okay, hier wurde anscheinend eine Invariante verletzt, also muss ich
das Programm erweitern und das ist auch nicht schwer.
Ich führe so eine Normalisierungsfunktion ein, die jedes Mal, wenn es
über 60 oder 24 drüberläuft, einfach das abzieht und damit habe ich
dann das Programm schon korrigiert.
Ja, man könnte sich natürlich überlegen, auch das Ganze mit einer IF
-Abfrage zu machen und dann von Hand eine Exception zu werfen, was
Exceptions sind, haben Sie ja alle schon kennengelernt, aber da gibt
es auch ein paar Vor- und Nachteile, die es abzuwägen gilt.
So eine Assertion verwendet man eher, wenn man eine Annahme zur
Korrektheit des Programms ausdrücken will, auch zur Dokumentation und
für Fehler, die man nicht zur Laufzeit behandeln kann.
So eine Assertion kann man abschalten, Sie haben gesehen, den Switch
in Java und die wird eben ja durch dieses Schlüsselwort Assert
aufgerufen.
Wenn Sie selbst eine IF-Abfrage schreiben, dann sollte das eher dafür
sein, dass Sie Eingabedaten des Benutzers prüfen, denn Sie wissen ja
nicht immer, was Sie hier reinbekommen und für Sonderfälle sollten Sie
dann so eine Exception werfen.
Die Exception kann dann auch behandelt werden, denn Sie können so eine
Exception abfangen, Sie können im einfachsten Fall einfach eine
Fehlermeldung zeigen oder halt dem Benutzer nochmal sagen, bitte
erneut eingeben.
Also das sind Fehler, die man abfangen und behandeln kann, im
Gegensatz zur Assertion.
Und Exceptions kann man auch nicht abschalten.
Und dasselbe, was wir jetzt gerade gesehen haben mit der Assertion,
das wäre dann hier auf der rechten Seite einfach, dass man eine IF
-Abfrage macht und dann eine gewisse Exception wirft.
Also nicht immer sind Assertions das Mittel der Wahl.
Sie haben jetzt hier gelernt an den Kriterien, wofür was gut ist.
Gut, dann möchte ich gerne die nächsten, die letzten 20 Minuten jetzt
noch der Vorlesung in dieses Test-Framework JUnit vorstellen.
Ich habe mir hierfür ein paar Tutorials und Einführungen angeschaut,
die sind hier unten verlinkt und aus denen sind einige Materialien
jetzt drin.
Was ist also JUnit?
Das ist ein Open-Source-Framework, wie vieles im Java-Bereich, mit dem
Sie Tests schreiben können.
JUnit verwendet die Java Reflection API, die kam jetzt in der
Vorlesung noch nicht vor, aber das ist eine API, mit der Java
-Programme sich selbst untersuchen können.
Also in welchem Zustand bin ich, was kommt zurück, welche Klassen gibt
es und so weiter.
JUnit hilft eben den Leuten dabei, Tests und solche Sammlungen von
Tests, genanntes Weeten, zu definieren, Anforderungen an die
Architektur hinzuschreiben, dann Code zu debuggen, Code immer zu
integrieren, sodass man zu jedem Zeitpunkt eine laufige Version seines
Programms hat und damit eben sicherstellt, dass die Qualität zu jedem
Punkt stimmt.
Was bei JUnit wichtig ist, das wird nicht mit dem SDK von Oracle
ausgeliefert, ist aber in fast allen Entwicklungsumgebungen mit drin,
wie zum Beispiel Eclipse.
Erich Gammer haben Sie vielleicht schon in der Software-Technik mal
gehört von den Entwurfsmustern und Kent Beck habe ich ja vorhin auch
schon erwähnt.
XP soll nicht Windows XP heißen, sondern Extreme Programming, das ist
auch so ein Programmierstil.
Die haben eben JUnit entworfen und es gibt auch weitere Frameworks,
die dem Beispiel gefolgt sind, also NUnit für .NET, CPP für C++, PHP,
ABAP und noch viele andere Sprachen.
Und JUnit ist kein ISO-Standard, kein festgelegter, aber halt ein de
facto -Standard, weil eigentlich jeder, der Java testet, JUnit
verwendet.
Und für das Testgetriebe in der Entwicklung ist es auch der wichtigste
Standard.
JUnit gibt es in verschiedenen Versionen.
Version 3 ist hoffnungslos veraltet, also wenn Sie irgendwo JUnit 3
finden, bitte nicht beachten.
4 ist immer noch aktuell, 5 ist gerade in der Entwicklung und momentan
noch als Beta verfügbar, deswegen benutzen wir hier JUnit 4.
Und das Beispiel, was ich Ihnen hier gezeigt habe, ist jetzt schon so
ein JUnit-Test.
Wie Sie sehen, ist eine ganz normale Java-Klasse, ist auch eine ganz
normale Java-Methode und da stehen aber solche Annotationen dran.
Und dann gibt es hier noch irgendwelche abgefahreneren Assert
-Funktionen, die hier jetzt irgendwie gucken, ob das da hinten gleich
ist, heißt Equals, und geben noch irgendeine Nachricht aus oder sagen,
was Sie eigentlich testen.
Also das nur mal so als kleine Motivation.
JUnit ist etwas, was auch wieder nur Java-Code ist mit speziellen
Zusätzen, die wir uns jetzt gleich angucken.
In JUnit gibt es auch Testfälle und ein Testfall per Definition testet
immer eine einzelne Methode, soweit es möglich ist.
Also diese Unit von JUnit, die kleinste testbare Einheit, das ist eben
die Methode.
Mehrere Testfälle sind dann in so einem Unit-Test zusammen und der
testet eben die Methoden von der einzelnen Klasse.
Von denen kann es wiederum viele geben, diese Tests, so wie es auch
viele Klassen gibt und eine Suite ist dann diese Kombination dieser
Unit -Tests.
Dann haben wir natürlich vorhin gesehen, der erste Schritt ist, man
muss irgendwie für Tests die Daten zusammenbringen, auf denen die
Tests arbeiten.
Und dafür gibt es die sogenannte Fixture, auf Deutsch heißt das eine
Vorrichtung, die liefert eben das alles, damit Tests ausgeführt werden
können.
Und der Runner führt dann die Tests aus und stellt die Ergebnisse dar.
Das ist also nur die abstrakte Struktur und die Terminologie, die
werde ich jetzt gleich nochmal verwenden am konkreten Beispiel.
Also was ist jetzt so ein Testfall?
Die Testfälle sind wiederum andere Java-Methoden.
Und Sie haben schon gesehen, dort werden solche Annotationen
hinzugefügt und unten bestimmte Assertions, so wie dieses Assert
-Equals.
In JUnit gibt es ein Assertions-Paket, das schauen wir uns auch gleich
an, dort können Sie Gleichheit überprüfen, identische
Objektreferenzen, Prüfung auf Null.
Und diese Assertions, die werden dann benutzt, um ein Urteil zu
bilden, ob der Test fehlgeschlagen ist oder nicht.
Diese Urteile, die kommen in drei Varianten.
Also Verdict heißt es auf Englisch, das Urteil, wie der Test
ausgegangen ist.
Da gibt es Pass und das bedeutet, der Test hat seinen Zweck erfüllt,
also ist durchgelaufen und die getestete Software hat sich verhalten,
wie erwartet.
Gegenteil dazu ist Fail, das heißt, dass der Test zwar durchgelaufen
ist, aber sich die Software nicht so verhalten hat, wie erwartet.
Und der dritte Fall, den es auch noch gibt, der ist Error, das
bedeutet, dass der Testfall nicht durchgelaufen ist, also dass er
seinen Zweck gar nicht erfüllen konnte.
Zum Beispiel es trat ein unerwartetes Ereignis aus, kein Speicher mehr
da, Netzwerkverbindung weg oder der Test konnte gar nicht richtig
aufgesetzt werden, weil die Datenbank nicht verfügbar war.
Das gibt dann nochmal einen dritten Zustand neben Pass und Fail.
Was Sie jetzt schon hier merken, das sind so ähnliche Begriffe wie die
Definition, die wir schon hatten vorne, aber die sind leider nicht
entsprechend.
Da müssen Sie jetzt mit leben, dass das kein festgelegter Standard
ist, an den sich alle halten.
Also Failure und Error heißen nicht das gleiche wie vorhin.
Dann gibt es eben dieses Assertions-Paket, das ist nicht das Search
-Schlüsselwort, was wir gerade kennengelernt haben, sondern ein
eigenes Paket.
Und die sind spezifisch für JUnit.
Diese Assertions, die werden halt reingeschrieben in den Code und für
so einen Test gilt, wenn irgendeine dieser Assertions verletzt wird,
dann ist das Testurteil Fail, auch wenn alle anderen unverletzt
bleiben.
Wenn irgendwo eine Ausnahme auftritt, dann kommt man eben zu diesem
Error.
Und wenn gar keine Assertion verletzt wird, dann ist das Testurteil
Pass.
Und diese Assertions-Methoden, die sind statisch definiert, ich habe
sie Ihnen hier schon mal alle aufgeführt.
Die sind eben die Methoden, die in diesem Paket drin sind.
Da muss man eigentlich nicht so viel erklären.
Was man sich vielleicht wundert, warum gibt es denn jetzt hier
speziell eine Fail-Methode?
Das kann manchmal sinnvoll sein, wenn man mit Absicht will, dass ein
Test Fail schlägt, weil man schon weiß, man hat die Funktion noch gar
nicht implementiert oder man möchte eben mal testgetrieben arbeiten
und schreibt es schon mal hin und schreibt nichts weiter rein außer
Test Fail.
Da muss man sich nicht irgendwie Assert, True, False oder irgendwas
komisches reinschreiben.
Die gibt es natürlich hier.
Man kann überprüfen, ob eine Variable True oder False ist, ob zwei
Werte gleich sind.
Bei Arrays wird dann nicht der Inhalt überprüft, sondern tatsächlich
nur, ob die Objektreferenz gleich ist.
Es gibt aber auch noch Assert, Array, Equals, was dann eben das tut.
Dann kann man tatsächlich bei auch Zahlenvergleichen eine Toleranz
angeben mit identischen Dezimalstellen.
Gucken, ob Objekte Null sind, ob sie nicht Null sind, ob beide
Variablen auf dasselbe Objekt verweisen oder ob sie auf ein
unterschiedliches verweisen.
Also das sind eigentlich alles Dinge, die Sie auch mit dem ganz
normalen Assert, True und False ausdrücken könnten, aber es ist ein
bisschen praktischer, wenn man hier schon verschiedene Funktionen
dafür hat.
Genau, dieses Assert, Equals wird auch genau dann zu VAR auswerten
oder gültig sein, wenn diese Equals-Funktion nach VAR auswertet.
Und diese Array-Methode, die ich gerade eben gesagt habe, kann man
dann eben benutzen, um zu sagen, Arrays, die gleich lang sind und an
jeder Stelle den gleichen Wert haben, die sind dann auch Equal.
Also da wird für jeden Wert an jeder Stelle dieses Assert, Equals
aufgerufen.
Gut, das war es dann mit den Assertions.
Dann kommen wir weiter zu dieser Fixture, also die den Test
vorbereitet und alles zur Verfügung stellt, was man braucht.
Zu dieser Fixture gehört eben dieses JUnit-Paket, das man importieren
muss, wenn man so einen Test schreibt, und dann diese ganzen
Annotationen, die man an die Methoden dran machen kann.
Also Test sagt, das ist eine Testmethode.
Man kann auch Before und After sagen, die werden dann vor und nach
jedem Test ausgeführt, beziehungsweise vor allen Tests einer Klasse
und nach allen Tests einer Klasse.
Man kann einen Test auch auf Ignoriert setzen, zum Beispiel, wenn man
jetzt weiß, man schreibt Testgetrieben, man weiß, eine Funktion wird
erst viel später implementiert, aber der Test ist schon mal da.
Und damit nicht immer die ganze Testsuite als Ergebnis Fail liefert,
ignoriert man den und kann sogar noch für das Log dann eine Nachricht
reinschreiben, warum der ignoriert wurde.
Genauso kann man bei so einem Test hinschreiben, dass man schon
erwartet, dass eine bestimmte Exception geworfen werden muss.
Also dann führt die Exception nicht zu einem Error, sondern das ist
dann ein erwünschtes Verhalten.
Und man kann tatsächlich auch Timing-Verhalten in diese Tests
reinschreiben.
Also zum Beispiel hier 100 Millisekunden darf es dauern und wenn es
länger dauert, ist es dann Fail.
Diese Annotationen Before und After, die sind sehr praktisch, denn
damit können Sie sowas einmal definieren und vor jedem Test wird dann
diese Before-Methode ausgeführt.
Man kann sogar mehrere Methoden mit Before annotieren und alle von
denen werden dann vor allen Tests ausgeführt.
Die Reihenfolge ist jedoch nicht festgelegt, sondern die kann beliebig
sein und ist implementierungsabhängig.
Die Methoden, oh das ist hier ein Druckfehler, Entschuldigung, das
heißt natürlich mit der Annotation After werden nach jedem Test
ausgeführt und das ist auch wichtig, die werden auch dann ausgeführt,
wenn eine Exception geworfen wurde oder wenn der Test fehlschlägt.
Das ist sehr wichtig, wenn Sie zum Beispiel eine Datenbankverbindung
aufmachen und die wieder zumachen wollen am Ende, möchten Sie ja keine
offene Datenbankverbindung hängen lassen, wenn der Test fehlschlägt.
Also deswegen wird das auf jeden Fall ausgeführt.
Was auch schön ist zur Strukturierung Ihres Codes, Sie dürfen diese
Methoden benennen, wie Sie möchten.
Also die müssen nicht irgendwie Test, ABC oder sonst was heißen, die
dürfen beliebige Namen haben, müssen aber Public und Void sein.
Und das gleiche gilt für diese Before-Class und After-Class Methoden,
die darf man auch benennen, wie man möchte, die müssen aber Static
sein.
Und pro Klasse gibt es dafür dann auch nur eine Methode mit dieser
Annotation Before-Class und After-Class.
Das ist also wichtig zu wissen.
Dann haben wir schon vorhin in diesem Schaubild gesehen, solche Tests
können zu Suiten kombiniert werden, also nur eine Sammlung von Tests.
Suiten können wieder andere enthalten und Sie haben damit eine gute
Möglichkeit, sich diese Testfälle zu partitionieren und zu
strukturieren.
Sie können dann sagen, ein bestimmter Suite testet eine bestimmte
Anforderung, einen bestimmten Teil des Programms, eine bestimmte
Funktionalität und können die dann auch in verschiedenen Kombinationen
ausführen.
Diese Suiten werden gut durch die Test-Runner unterstützt.
Ich zeige auch nachher nochmal ein Screenshot von Eclipse, wo man
sieht, wie sowas dort ausgeführt wird.
Bei einer Suite schreiben Sie einfach eine normale Klasse, die dürfen
Sie auch benennen, wie Sie möchten, machen dort aber so eine Run
-Annotation rein, damit man weiß, das soll als Suite ausgeführt werden
und geben an, welche anderen Klassen da drin enthalten sind.
Also zum Beispiel hier, ich möchte einen Geometrie-Test machen, habe
einen Dreieck-Test, einen Kreistest und so weiter.
Gut, dann möchte ich jetzt noch als letztes ein sehr interessantes
Feature von JUnit vorstellen, das mir schon sehr viel geholfen hat in
der Entwicklung, was aber auch nicht so bekannt ist, habe ich die
Erfahrung gemacht und deswegen ist es interessant, dass wir uns das
heute anschauen.
Und zwar sind es parametrisierte Tests.
Wenn Sie nochmal an unser Dreieck-Beispiel zurückgehen, da möchten Sie
ja vielleicht nicht nur eine Zahlenkombination testen, sondern viele,
viele verschiedene, um zu gucken, ob es immer die richtige Lösung
gibt, auch wenn die Zahlen groß werden, auch wenn die Zahlen komisch
werden, keine Ahnung.
Und um sowas zu machen, einen Test also mit verschiedenen Werten
auszuführen, müssten Sie entweder sich irgendwie so ein großes Array
machen, wo die alle drinstehen und dann darüber irgendwie iterieren
mit einer Vorschleife und da kommt schon das erste Problem.
Wenn Sie jetzt viele Werte haben und irgendwo in der Mitte schlägt
irgendwas fehl, dann haben Sie schon mal das Problem, dass der Rest
der Werte nicht getestet wird.
Also wenn eine Exception fliegt, das ganze Programm beendet wird, dann
sind Sie irgendwo in der Mitte stehen geblieben und es geht nicht mehr
weiter.
Möglicherweise kommen Sie auch in eine komische Bedingung, wo eine
Endlosschleife erzeugt wird, haben Sie das selbe Problem, irgendwann
müssen Sie es halt mal hart abbrechen von außen und haben den Rest der
Werte ungetestet.
Dann könnten Sie sagen, okay, dann schreibe ich halt eben für jede
Kombination einzeln einen Testfall, aber das ist natürlich sehr viel
Aufwand und das möchte man nicht.
Man möchte ja jetzt nicht 5000-Zahlen-Kombinationen per Hand irgendwie
hinschreiben, sondern dafür hat man dann in JUnit eine Unterstützung
für eben diese parametrisierten Tests.
Wie erstellt man so einen parametrisierten Test?
Man schreibt einen ganz normalen Test mit dieser Annotation und dann
muss man noch eine weitere Methode schreiben, die einem sagt, was die
Parameter sind.
Dafür gibt es eine extra Annotation, Parameters heißt die und da muss
eine Collection zurückkommen.
Da sehen wir gleich ein Beispiel.
Dann kann man Variablen deklarieren und sagen, das sind jetzt meine
Parameter und muss dann noch per RunWith sagen, so wie wir vorhin
gesagt haben, führe das als Suite aus, muss man sagen, führe es als
parametrisierten Test aus.
Das habe ich jetzt hier als Beispiel, also wir haben nochmal das
Dreieck von vorhin und haben uns jetzt überlegt, wir möchten einen
parametrisierten Test schreiben.
Dafür kommt zuerst hier diese Annotation rein, es ist also ein
parametrisierter Test, der hat einen bestimmten Runner und jetzt fange
ich an, mir meine Parameter zu definieren.
Das sind auch wieder ganz normale Variablen, können Sie benennen, wie
Sie möchten, können jeden Typ haben, der Ihnen zur Verfügung steht,
aber man schreibt jetzt rüber, okay, das ist jetzt ein Parameter in
meinem parametrisierten Test durch diese Annotation und dann fängt man
an, die Daten zu erstellen, die man eigentlich testen will.
Hier in dem Fall sind es jetzt immer Kombinationen von Zahlen, also
gebe ich hier so eine Collection von Integer Arrays zurück.
Und um die zu erstellen, mache ich ein zweidimensionales Array und
sage, na, die erste Kombination, die ich will, ist 1, 2, 2, die zweite
ist 5, 3, 5, die zweite ist 2, 2, 7 und so weiter und so fort.
Dieses Array könnten Sie jetzt zum Beispiel dann auch manuell
hinschreiben, so wie hier, da haben Sie nicht besonders viel gewonnen,
aber das Array könnten Sie auch mit einer Funktion befüllen, zum
Beispiel, ich möchte alle ungeraden Zahlen kleiner 100 zum Beispiel
oder alle Permutationen, wie es der eine Kollege vorhin gesagt hat,
sowas kann man ja automatisch erstellen.
Und das Ganze wird dann von einem zweidimensionalen Array mit dieser
Hilfsfunktion aus dem Arrays-Paket in eine Collection umgewandelt,
also eine Liste in diesem Fall speziell und zurückgegeben.
Und dann schreiben Sie im Test nur noch hin, ich möchte einen Test
ausführen, wo irgendwo diese Parameter drin vorkommen, also die
Variablen müssen hier natürlich stimmen, ABC.
Und dann können Sie noch überprüfen, in dem Fall wollten wir jetzt
immer gucken, ob die hier alle gleichschenklich sind.
Das sind sie wohl auch.
Ja gut, hier hinten ist noch eine andere Bedingung nicht erfüllt, aber
egal.
Aber hier möchte ich also anscheinend gleichschenkliche Dreiecke
überprüfen.
Und wenn Sie jetzt diesen Test ausführen, werden tatsächlich alle
diese Kombinationen nacheinander ausgeführt, ohne dass Sie das hier
explizit hätten hinschreiben müssen.
Wie gesagt, nochmal, hier sieht man explizit die drei Werte, aber man
könnte auch da automatisch Werte generieren in großer Anzahl und hat
dann auch den Vorteil, wenn man das später ausführt, dass man sogar
sieht, bei welcher Parameterkombination man gerade war.
Letztendlich ist dann die Frage, wenn man das jetzt alles geschrieben
hat, wie führt man so einen Test aus?
Und dafür gibt es dann eben verschiedene Testrunner.
JUnit selbst bringt auch einen mit, kann man dann per Kommandozeile
ausführen.
Und die Testmethoden, die halt dann in so einer Klasse drin sind,
werden standardmäßig alle ausgeführt.
Also überall, wo AddTest dran steht, das nimmt es als Test wahr und
die werden ausgeführt.
Vorher wird immer Before aufgerufen, danach immer After und so weiter
und so fort.
Was wichtig ist, ist, dass diese Reihenfolge, in der die Methoden
aufgerufen werden, nicht vorhersagbar ist.
Das ist nicht so, wenn sie untereinander stehen, dass sie in der
Reihenfolge aufgerufen werden.
Das ist nicht so spezifiziert in JUnit und das hängt von der
Implementierung ab.
Andere Runner, zum Beispiel in Eclipse, können es dann dem Benutzer
ermöglichen, eine Teilmenge auszuwählen oder eine Reihenfolge zu
bestimmen, aber im Allgemeinen sollte man davon nicht ausgehen.
Es ist also, wie auch vorhin schon beim Testen, wie ich das beim
Testen erwähnt habe, hier genauso wichtig, dass Sie das als gute
Praxis ansehen, Ihren Test so zu schreiben, dass die unabhängig von
der Ausführungsreihenfolge sind.
Das heißt vor allen Dingen auch, machen Sie nicht den Fehler, okay,
ich schreibe mit einem Test, mit einem Test gucke ich, ob die
Datenbank initialisiert wird, mit dem zweiten gucke ich, ob es richtig
drin steht und mit dem dritten gucke ich, ob sie richtig
runtergefahren wird.
Dann hätten Sie nämlich Abhängigkeiten von einem Test zum anderen und
müssten auch sicherstellen, dass Sie in der Reihenfolge ausgeführt
werden.
Stattdessen machen Sie es lieber mit diesem Before, Before Class und
so weiter.
Schreiben Sie eine Before-Methode, Datenbank hochfahren und eine After
-Methode, Datenbankverbindung trennen von mir aus oder Datenbank
runterfahren und damit haben Sie dann eine bessere Isolation und
können dann auch einfacher die Fehler finden, wenn sie auftauchen.
Zuletzt hier noch so ein kleines Bildchen, wie sieht es dann in
Eclipse aus.
Dort kann man dann tatsächlich bei der Run-Configuration auch angeben,
welche Tests möchte man denn ausführen.
Hier kann man entweder alle oder eine Untermenge angeben.
Hier habe ich tatsächlich mal diesen Test von der vorhergehenden Folie
ausgeführt, diesen Triangle Perimeterized Test.
Und da sieht man dann tatsächlich, der wurde mit drei Werten befüllt,
wurde ausgeführt und das Ergebnis war in dem Fall Pass.
Sie könnten jetzt auch zum Beispiel in Eclipse einfach mit rechts auf
einen Test draufdrücken und sagen, wiederhole mir nochmal genau den
mit den Parameterwerten oder wiederhole mir nur die, die
fehlgeschlagen sind oder wiederhole mir nur die eine Suite von denen
und so weiter.
Also das ist da recht praktisch und ist in allen IDEs eigentlich sehr
gut integriert und ermöglicht dann diese testgetriebene Entwicklung.
Damit komme ich zur Zusammenfassung für heute.
Was haben Sie jetzt heute gelernt?
Die wichtigste Message natürlich, ausgiebiges Testen ist unerlässlich.
Ich finde es auch ganz ehrlich persönlich für mich immer ganz schön,
wenn ich ein Programm entwickle und dann mir gleich einen Testfall
schreibe und dann sehe, okay, jetzt ist hier alles grün und es
funktioniert anscheinend alles so, wie ich es getestet habe.
Und es ist dann weiter auch so, wenn man das Programm dann mal ändert
und was weiterentwickelt, sieht man gleich, was daran jetzt dann
möglicherweise verändert wurde an dem Verhalten und wo jetzt plötzlich
Fehler auftauchen.
Also sehr nützlich und deswegen eigentlich unerlässlich.
Die Ziele des Testens sind der Nachweis, dass Funktionalität erfüllt
ist und das Entdecken von Fehlern, das wie gesagt zwei ganz
unterschiedliche Sachen sind.
Und dann haben Sie weiterhin heute gelernt, wie man Tests schreibt,
dass Tests so viele Situationen wie möglich abdecken sollen, dass es
Kriterien gibt, um gute Testfälle zu schreiben.
Es gibt Teststrategien, die eben die Qualität der Tests verbessern und
damit auch eine gute Abdeckung erreichen und damit letztendlich zu
einer fehlerfreien Software besser werden.
Zusicherung haben Sie gelernt als Assertions in Java.
Damit können Sie erforderliche Bedingungen, die Sie angenommen haben,
überprüfen, allerdings nur zur Laufzeit, können es aber wie gesagt
auch als Dokumentation verwenden und weitere Assertions möglicherweise
dann sogar in automatisierten Beweisverfahren oder auch in manuellen
beweisen.
Als letztes habe ich Ihnen JUnit als Framework zum Testen von JavaCode
vorgestellt.
Verwenden Sie das ruhig mal selbst für Ihre eigenen Programme.
Das ist sehr, sehr nützlich, wenn Sie es nicht eh schon tun.
Das hilft sehr.
Und als allerletztes hatten wir dann heute die parametrisierten Tests,
die eben das automatische Testen von großer Mengen an Testdaten
ermöglichen.
Damit wäre ich für heute fertig.
Ich danke sehr für Ihre Aufmerksamkeit und wünsche Ihnen noch einen
schönen Tag.