Freitag, 1. Juni 2007

Erste Erfahrungen mit ECMA Unit

In unserem letzten, stark JavaScript-lastigen BSP-Projekt kam ich leider viel zu spät auf den Trichter, was für eine grosse Hilfe Unit Tests beim Entwickeln darstellen (das Thema schwebte schon längere Zeit im Raum). Erst gegen Ende entwarf ich für zwei grosse JavaScript-Dateien je eine Testseite, die passende Testsuiten auf Basis des ECMA Unit Tools aus dem Kupu-Projekt abarbeiten. Schnell war ich bei 30 Tests, die mir bei den letzten noch durchzuführenden Erweiterungen sehr zugute kamen: Als ich ein neues Feld in die Stammdaten und dazu gehörige neue Regeln in die Geschäftslogik aufnehmen musste, arbeitete ich "Test First", wie in Frank Westphals Buch beschrieben: Zuerst schrieb ich neue Tests und erweiterte bestehende um die neue Logik. All diese Tests schlugen natürlich fehl, da die Erweiterung ja erst noch zu implementieren war. Während des Implementierens rief ich immer wieder die Testsuite auf und konnte so meinen Arbeitsfortschritt kontrollieren. Wenn andere, frühere Tests wieder fehlschlugen, wusste ich: Es muss an meinen gerade getätigten Änderungen liegen. Das war für die Diagnose sehr hilfreich. Insgesamt stelle ich fest, dass ich mit diesem Programmierstil besser und sicherer vorankomme.

Es war interessant, dass einer meiner ersten Unit Tests einen Fehler der Software aufzeigte, der bei den Anwendungstests zuvor nicht aufgefallen war. Das lag daran, dass die Konstellation, unter der er auftrat, sich bei den Anwendungstests nicht so häufig ergab. Dennoch wäre er früher oder später im Produktivbetrieb aufgeschlagen. (Es war zwar kein schlimmer Fehler, nur ein Anzeigeproblem - aber schlimme Fehler könnten ebensogut entdeckt werden).

Ich wagte sogar grundlegende Änderungen, die nicht direkt etwas mit meiner Aufgabe zu tun hatten, aber den Code verbesserten. Änderungen, vor denen ich vor kurzem noch wegen der hohen Aufwände aufgrund möglicher Seiteneffekte zurückgeschreckt wäre.
Wertvoll ist, dass ich eine grössere Freiheit geniesse, einmal gefällte Entuwrfsentscheidungen zu ändern, wenn ich die potentiellen Seiteneffekte solcher Änderungen besser unter Kontrolle habe.

Ich verfüge - wie sicher viele Web-Entwickler - nicht über eine designierte IDE zur Entwicklung von JavaScript-Code. Stattdessen arbeite ich mit einer Reihe von Tools. Ich verwende einen Plain Text Editor mit Syntax Highlighting, Klammerzuordnung und Funktionsliste - UltraEdit. In der neuesten Version von UltraEdit ist sogar ein JavaScript-Interpreter eingebaut, der allerdings für die Scripting-Unterstützung beim Arbeiten mit dem Editor gedacht ist. Mit diesem JavaScript Interpreter werde ich noch einige Experimente machen. Er ist für die Integration von Tools in den Editor sicher gut geeignet, so dass man sich letztlich selbst eine massgeschneiderte IDE zusammenbauen kann. Tools, die mich beim Entwickeln von JavaScript-Code unterstützen, sind JSLint, eine erweiterte JavaScript-Syntaxprüfung von Douglas Crockford sowie der Microsoft Script Debugger.

Was ich in meinem letzten Blog ankündigte, habe ich mittlerweile Wirklichkeit werden lassen: ECMAUnit und JSLint laufen gleichzeitig (ersterer auf dem Client, letzterer auf dem Server). Wie bereits befürchtet, ist die Klasse CL_JAVASCRIPT jedoch nicht billig. Um ein 50 KB schweres JavaScript-File zu "linten", braucht der Server etwa eine Sekunde. Das ist zwar keine tolle Performance, aber ich kann damit leben, denn bei Druck von "Aktualisieren" auf dem kombinierten Browserfenster erhalte ich ja clientseitig zunächst die ECMAUnit-Testergebnisse und kann mir diese erst einmal anschauen, bis dann der Server ist und sein Ergebnis mit Ajax/DHTML in die Seite eingespielt wird.





Ich erwähnte schon das beruhigende Gefühl, das der grüne OK-Balken ausstrahlt. Dieses ist natürlich umso trügerischer, je schlechter der Code durch Tests abgedeckt ist. Man sollte hier nicht perfektionistisch sein. Eine hundertprozentige Code-Abdeckung wird man mit realistischen Aufwänden nicht hinbekommen. Die Zeitknappheit, die wir schon jetzt beim Entwickeln spüren, macht sich auch beim Entwickeln von Tests bemerkbar. Wir sollten wenigstens die wesentlichen Funktionen des Codes abdecken, auch mal ein paar komplexere, die nicht so einfach zu beschreiben sind. Natürlich - je mehr Tests man hat, umso besser. Wenn später Fehler gemeldet werden, können diese Fehler in Form weiterer Tests formuliert werden, die man einer Testklasse hinzufügt.

Eine weitere Beschränkung ergibt sich dadurch, dass wir nur die von uns entwickelte Programmlogik testen wollen: Programmteile, die in starker Wechselwirkung mit der Datenbank, der Benutzerschnittstelle oder mit Bibliotheken von Drittanbietern stehen, sollten daher über Schnittstellen mit diesen externen Einheiten reden. Die Testklasse bezieht sich dann auf eine interne Test-Implementierung dieser Schnittstelle, die die externe Einheit bloss simuliert. Wenn die Tests laufen, müssen der zu testenden Klasse diese Testobjekte untergeschoben werden.

Hier ein konkretes Beispiel für diese Vorgehensweise: In einer Methode counter_post() benutze ich Ajax, um Daten an das SAP-System zu übermitteln und (asynchron) eine Antwort von diesem entgegenzunehmen:


function counter_post() {
var lRequestor;
...
lRequestor = getRequestor();
lRequestor.open("POST",this.postService,true);
lRequestor.onreadystatechange = function() {
doAfterSave(lRequestor);
};
lRequestor.send(lBody);
...
}


Die Funktion getRequestor() liefert dabei eine für den Browser passende Instanz von XMLHttpRequest. Wenn wir wissen, dass unsere Anwendung nur auf dem Microsoft Internet Explorer läuft, könnten wir z.B. schreiben:


function getRequestor() {
return new ActiveXObject("Msxml2.XMLHTTP");
}


Stabiler ist es, an dieser Stelle ein schmales Framework wie Sarissa zu verwenden, um eine browserübergreifende Instanz von XMLHttpRequest zu erhalten.

Was ist nun zu tun, um die Logik zu testen? Wir wollen ja den SAP-Service, der hier aufgerufen wird, nicht mittesten. Wir wollen ihn nur simulieren. Dazu habe ich im Testscript die Funktion getRequestor() überschrieben durch


function getRequestor() {
return gRequestor;
}


Dabei ist gRequestor der (zuvor definierte) Simulator:

var gRequestor = {
readyState:0,
responseText:"",
requestText:"",
open:function() {
this.readyState = 0;
},
onreadystatechange:0,
send:function (iBody) {
this.requestText = iBody;
},
sendResponse:function(iResponseText) {
this.responseText = iResponseText;
this.readyState = 4; // Gleich die "Antwort" zurückschicken
this.onreadystatechange();
},
setRequestHeader:function() {
}
};


Ein Test der Funktion counter_post verwendet nun den Simulator, statt wirklich das SAP-System zu informieren. So ist ein isolierter Funktionstest möglich, ohne Abhängigkeit von Umsystemen. Hier ein Ausschnitt aus einem Test:


this.testCountOneAndSync = function() {
this.initCounting();
...
counter.post( true );

// Wurde Request korrekt verschickt?
this.assertEquals( gRequestor.requestText,
'' +
'<countings>' +
... +
'</countings>');

// Response des Servers simulieren
gRequestor.sendResponse('{records:[1]}');

// Queue darf keinen Eintrag mehr haben
this.assertEquals( counter.queue.length,
0,
"Zählsatz nach Rückmeldung aus Queue löschen");
// Attribut recNum darf beim Verbuchen nicht erhöht werden
this.assertEquals(...);
...
};


Indem man sich den requestText z.B. in einer Variablen des gRequestors merkt, kann man also testen, ob die Methode counter.post() ihn korrekt setzt. Ebenso kann man (hier mit gRequestor.sendResponse()) simulieren, dass eine Antwort vom Server eingetroffen ist. Danach kann man wie gewohnt seine Erwartungen mit assert()-Aufrufen kontrollieren.

Ähnliche Probleme stellten sich mit dem UI, hier also dem Browser. Die durch Unit Tests kontrollierten Code-Teile können nur die isolierten Teile der Geschäftslogik abdecken, auf die sie sich beziehen. Diese sollten von DOM-Aufrufen sorgfältig getrennt werden. Zum Beispiel indem man die UI-relevanten Methoden wie display(), setListbox() etc. für die Tests durch eigene Testfunktionen verschattet.

Man muss deutlich die Unit Tests von den Anwendungstests unterscheiden. Es ist sicher auch nützlich, Tests zu haben, die auch das UI und die Datenbank einschliessen. Das läuft aber unter Anwendungstests und sollte mit anderen Tools bewerkstelligt werden. ECMAUnit ist hierfür nicht die korrekte Wahl. Um automatisierte Anwendungstests im SAP-Umfeld - auch für Web-Anwendungen mit SAP als Server - zu erstellen, bietet sich ein Tool wie Mercury an. Aber Anwendungstests werden während des Entwickelns nie so eine grosse Rolle spielen können wie Unit Tests. Es lohnt sich, in regelmässigem Turnus automatische Anwendungstests abzuspielen, um den Status Quo des Systems sicherzustellen. Aber für den Entwicklungsprozess hat man noch viel kürzere Perioden: Mit derselben Selbstverständlichkeit, mit der ich schon jetzt alle paar Minuten den Compiler aufrufe, um die syntaktische Integrität während des Entwickelns sicherzustellen, werde ich in Zukunft meine Testsuiten aufrufen, um auch die funktionale Integrität meiner Programme zu verifizieren.

Keine Kommentare :