Donnerstag, 10. Mai 2007

Unit Tests für JavaScript-Code

In den letzten Tagen habe ich mir das Buch von Frank Westphal über testgetriebene Entwicklung vorgenommen. Die Beispiele sind zwar mit den Java-Werkzeugen JUnit und FIT verfasst, aber sie sind leicht auf andere Programmiersprachen wie JavaScript und ABAP Objects übertragbar, zumal es auch in ABAP seit Basisrelease 7.0 Unit Tests gibt. Und zwar sowohl für Klassen als auch für Programme und Funktionsgruppen.

Dank Frank Westphal weiss ich nun Bescheid über Fixture Factories, Stubs, Self-Shunts und Mock-Objekte. Das Buch ist eine wahre Fundgrube von Anregungen für den Entwurf von Tests und Testfällen, die auch in der ABAP Welt sinnvoll anwendbar sind.
Ironischerweise habe ich nun mit ECMAUnit, einem Beiprodukt von Kupu, statt mit ABAP Unit, begonnen! Denn in dem Projekt, in dem ich zur Zeit arbeite, ist viel clientseitige Logik zu implementieren - und das heisst JavaScript. Ich bin daran, ECMAUnit mit dem ausgezeichneten und sehr wertvollen Tool JSLint von Douglas Crockford zu einer Testseite zu kombinieren (JSLint ist - ähnlich dem lint-Tool in C - ein Syntax Checker für JavaScript-Code, der auch auf instabile oder gefährliche Codestellen hinweist).

Wir arbeiten hier an Web-Anwendungen, die teilweise JavaScript-Logik im Umfang von 50 KB enthalten. Ohne Debugger - wir nehmen hier nicht Venkman, sondern den Microsoft Script Debugger, da das Projekt auf dem Internet Explorer zu entwickeln ist - und ohne JSLint wären wir da schon lange verloren gewesen.

Für jede dieser JavaScript-Dateien habe ich nun eine parallele BSP-Seite angelegt, die nichts anderes macht als die ECMA Unit Tests für die Datei aufzurufen (die für die Datei [name].js in einer begleitenden JavaScript-Datei [name]test.js enthalten sind) und dann den - hoffentlich grünen - Test-Balken zu rendern. Demnächst werden diese parallelen BSP-Seiten auch noch den JSLint aufrufen und dessen Ergebnisse hinzufügen. Wenn ich die JavaScript-Datei [name].js und/oder die zugehörige Testdatei [name]test.js bearbeite, speichere ich sie gelegentlich und drücke dann "Aktualisieren" in einem parallel geöffneten Browserfenster, das die ECMA Testseite zeigt. So erkenne ich sofort, ob die zuletzt ausgeführten Änderungen schädliche Nebenwirkungen zeitigten. Wie sicher ich dabei bei einem grünen Balken sein kann, hängt natürlich von der Anzahl der Testfälle und der Code-Abdeckung ab.

Es ergibt sich in JavaScript das technische Problem, dass ich die JavaScript-Datei nicht gleichzeitig im selben HTML-Dokument den Unittests unterziehen und mit JSLint syntaxchecken kann (es gibt Namenskonflikte mit den von JSLint verwendeten Symbolen). Ich müsste den Syntaxcheck entweder in einem iFrame laufen lassen oder auf dem Server ausführen. In letztere Richtung bewegen sich meine Ideen. Ein Web Application Server hat einen eingebauten JavaScript-Interpreter. Mit diesem lässt sich JSLint auch auf dem Server ausführen. Leider kostet das - hier notwendig ungepufferte - Lesen der MIME-Dateien ziemlich viel Zeit, und auch der JavaScript-Interpreter ist keine Rakete. Man muss damit rechnen, dass eine 50KB-Datei in etwa einer Sekunde abgearbeitet wird. Damit man nicht warten muss, empfiehlt sich, den Request mit Ajax-Mitteln asynchron auszuführen und das Ergebnis erst bei Rückgabe vom Server in das Dokument einzuspielen. So hat man auch beide Ressourcen sinnvoll beschäftigt: Nachdem der Browser zu onLoad den JSLint-Request an den Server geschickt hat, führt er die UnitTests aus. Irgendwann kommt das SAP-System mit dem JSLint-Ergebnis. Dann wird es ebenfalls in die Seite eingemischt. Die Implementierung eines entsprechenden Requestbehandlers ist kein Hexenwerk. Ich bin gespannt, wie sich das entwickelt.

Nun zu den Inhalten der ECMA Unit Tests: Problematisch beim Testen von JavaScript ist, dass viele Funktionen mit dem HTML-DOM der Seite, zu der sie gehören, nahezu untrennbar verknüpft sind. Solche Funktionen sind nur schwierig isoliert testbar - es geht zwar, aber m.E. nur mit einem immensen Aufwand (und den bezahlt uns keiner). Man könnte sich vorstellen, statt des DOM-Objekts document in document.getElementById("ean").value durchgängig eine globale Variable gDocument zu verwenden, die im Normalbetrieb mit document belegt wird, im Testbetrieb jedoch mit einem Mock-Objekt. Weitere globale Variablen für window und event dürften nötig werden. Das Mock-Objekt müsste in obigem Beispiel nicht nur die Methode getElementById() mocken, sondern auch ein weiteres Mock-Objekt zurückliefern, das wiederum Attribute und Methoden enthält und sich z.B. wie ein Inputfeld im DOM verhält. Das alles wäre zu implementieren, damit der JavaScript-Code isoliert von der HTML-Seite getestet werden kann, zu der er gehört.

Für die Funktionen, die HTML-DOM-Elemente abfragen und manipulieren, gibt es daher nur im Rahmen eines Web-Aufzeichnungstools wie Mercury realistische Testmöglichkeiten: Man kann nur schauen, ob das JavaScript funktioniert, wenn die HTML-Seite, zu der es gehört, verfügbar ist. Da die HTML-Seite aus Serversicht dynamisch aus einzelnen BSP-Views zusammengesetzt wird, deren Inhalte wiederum über BAPI-Aufrufe von beliebig komplexer Anwendungslogik des SAP-Standard abhängen, hat man mit einem Web-Aufzeichnungstool keine Chance, Unit Tests ausführen zu können - sondern nur Anwendungstests.

So beschränke ich mich für die ECMA Unit Tests auf die isoliert lauffähigen Teile des JavaScripts. Die Logik lässt sich recht häufig von den DOM-Aufrufen trennen, so dass die Tests doch schon eine recht gute Sicherheit geben. Wenn das Programm beispielsweise eine Listbox mit Werten füllt, so bemühe ich mich, den Teil der Funktion, der den Array mit den key/value-Paaren liefert, zu extrahieren, so dass er isoliert testbar wird. Das Abfüllen der Listbox (das man sowieso am besten an geeignete Frameworks wie Prototype delegiert) ist dann nicht mehr durch Unit Tests abgedeckt, sondern kann erst in den nachfolgenden Anwendungstests geprüft werden.

Keine Kommentare :