Montag, 25. Juni 2007

Der Atem des Lebens

Bekanntlich hat es den jungen Gautama, der als Prinz in einer behüteten, heilen Welt aufwuchs, zutiefst erschüttert, auf der Welt Alter, Krankheit und Tod wahrnehmen zu müssen. Sind diese unerfreulichen Dinge bloss die notwendige Kehrseite der Medaille, sind sie der unumgängliche Preis dafür, dass es auch schöne Dinge gibt? Muss es Alter, Krankheit und Tod geben, damit es Jugend, Gesundheit und Leben geben kann? Ist dies das Gesetz der Polarität, dass es kein Licht ohne Finsternis geben kann, den Tag nicht ohne die Nacht, den Sommer nicht ohne den Winter?

Wenn das Gute wirklich so unentwirrbar verflochten mit dem Schlechten ist, dann wäre es geboten, dem Kreis des Werdens und Vergehens für immer zu entkommen, wie Buddha es konsequenterweise getan hat. Seine Erleuchtung liess ihn über das Stirb und Werde triumphieren:

Den Wiedergeburtsweg endlos
habe vergeblich ich durchirrt.
Des Daseins Baumeister suchend;
leidvoll ist der Geburten Los.
Hauserbauer! Entdeckt bist du!
Nicht wirst du wieder bauen das Haus.
Zerbrochen sind die Balken dein,
des Hauses Zinnen sind zerstört.
Das Herz, dem Irdischen entflohn,
hat alles Wollens End' erreicht.[1]

Es ist zweifellos eine erschütternde Einsicht, dass alles, was uns umgibt, so schön es auch sein mag, bereits den Keim der Verderbnis in sich trägt. Alles ist vergänglich, uns selbst eingeschlossen. Schon im Embryo hat man Sterbeprozesse nachgewiesen. Der Knochenmann mit Stundenglas und Hippe steht immer bereit, uns einen nach dem anderen abzumähen, wenn die Zeit gekommen ist. Das gehört zu den Bedingungen, unter denen wir angetreten sind und in die wir uns fügen müssen.

Aber mit Lehren wie dem Polaritätsgesetz kommt man diesen Fragen nicht bei. Man macht es sich zu einfach, verpasst ein wesentliches Detail. Man lässt sich zu schnell dazu verleiten, mit dem Verstand eine Abstraktion zu entwickeln, um die Erfahrung einzuordnen. Denn die Vergänglichkeit steht nur wie eine äussere Notwendigkeit dem Erlebnis entgegen, dass alles Lebendige im Werden begriffen ist. Die Natur trägt, wenn ich auf sie lausche und mich mit ihr fühlend verbinde, eine grosse Verheissung in sich. Es ist dieser frische Atem des Werdens, der mich anders über die Vergänglichkeit denken lässt. Ich sehe und anerkenne die Vergänglichkeit, aber sie haftet dieser Welt nur akzidentell an, nicht essentiell. Das lehrt mich das Leben selbst. Das Lebendige zeigt sich zwar angekränkelt, wie beschädigt durch eine Art kosmischen Betriebsunfall, es lässt uns aber seine ursprüngliche Grösse ahnen und auf deren Wiederherstellung hoffen. Leben und Tod sind eben nicht unentwirrbar ineinander verschlungen, wie es das Polaritätsgesetz uns zu denken verleitet. Das Leben bedarf zu seiner Existenz nicht des Todes. Der Tod ist nur wie eine vorübergehende Beeinträchtigung eines grossen Plans. Es ist ein Rätsel, warum es ihn zur Zeit in der Welt geben muss. Aber ich spüre, dass jedes Wesen ein ideales Sein in sich trägt, eine Grösse, die es über seine gegenwärtige Erscheinung in der Welt weit hinaushebt.

[1] Die "Siegeshymne" des Buddha, zitiert aus: Hermann Oldenberg: Buddha. Sein Leben, seine Lehre, seine Gemeinde. Berlin 1890. S. 211.

Mittwoch, 20. Juni 2007

ABAP Modultests - aber wie?

Eigentlich implementieren ABAP-Entwickler bereits mithilfe von Tests. Während sie in einem Modus entwickeln, rufen sie in einem zweiten Modus eine Anwendung auf, die den Stand der Programmierung wiederspiegelt, an der also die Entwicklungsziele orientiert sind. Nach jeder kleinen Änderung wird im zweiten Modus verifiziert, dass die Anwendung noch läuft und dass die Änderung den gewünschten Effekt zeitigte. Das ist sicher kein ABAP-spezifisches Vorgehen, sondern ist in allen Programmiersprachen und Entwicklungsumgebungen sinnvoll. Das Prinzip "kleine Schritte" ist damit sichergestellt, und der zweite Modus dient der permanenten Kontrolle, ob die Funktionen während des Entwickelns stabil bleiben. Gerät die Anwendung aus dem Lot, muss es an der letzten kleinen Änderung liegen. Ebenso wenn das gewünschte neue Feature noch nicht so funktioniert wie es soll. Die Taktik der kleinen Schritte ermöglicht es, während des Entwickelns schnell Korrekturen vorzunehmen und das Ziel dabei nicht aus dem Auge zu verlieren. Wenn das neue Feature wie gewünscht erscheint - der neue Button ist zu sehen, auf Knopfdruck wird der korrekte Fcode ausgelöst, die neue, den Fcode behandelnde Codestrecke kommt dran, der Feldtransport der Dynprofelder in die entsprechenden ABAP-Daten funktioniert - dann denke ich mir einen neuen Anwendungsfall für diesen oder den nächsten Entwicklungspunkt aus. Wenn es dagegen nicht klappt, bemühe ich den Debugger, um das Programmteil zu finden, bei dem es hapert und betreibe dort die Fehleranalyse im Einzelschrittmodus.

Auch das Aktivieren eines Programms, der Syntaxcheck, ist Testen auf Micro-Ebene. Man aktiviert in regelmässigen, kurzen Abständen, um die syntaktische Konsistenz des Codes stets sicherzustellen. Je kürzer die Abstände sind, desto einfacher fällt mir die Korrektur der Fehler, da ich die gerade geänderten Stellen noch frisch im Gedächtnis habe.

Dieses Vorgehen hat aber zwei wesentliche Unterschiede zum Unit Testing:


  • Es basiert auf Anwendungstests, nicht auf Modultests. Ein Anwendungstest umfasst alle Schichten der Software, das User Interface ebenso wie die Datenbank und fremde, aufgerufene Bibliotheken. Es wird stets alles mitgetestet, was in die Anwendung involviert ist. Es ist nicht möglich, den gerade entwickelten Code isoliert zu testen, unabhängig von UI, verwendeten APIs und der DB.

  • Ich kann mit der herkömmlichen Entwicklungsweise immer nur einen Modultest ausführen. Wenn mir mehrere Tests einfallen, suche ich mir einen besonders typischen heraus und benutze ihn zur Kontrolle der Entwicklungen, bis alles OK ist. Erst danach kann ich mich den weniger typischen, aber auch wichtigen Fällen befassen. Mit anderen Worten: Ich muss die Tests seriell abarbeiten, und das ist zeitaufwendig. Möglicherweise fallen mir auch nicht alle relevanten Testszenarien ein, oder ich habe schlicht nicht genug Zeit, um sie alle durchzuspielen. Es ist gar keine Bestandssicherung möglich, da ich gar nicht weiss, welche Testfälle zum gesicherten Bestand der Anwendung gehören!



Die Entwicklung mit Unit Tests setzt bei diesen Schwachpunkten an:

Unit Tests können Code isoliert vom Rest der Welt testen.

Um das zu erreichen, wird ein Standardtrick der Programmierung benutzt - die Indirektion, auch als "Abstrakter Server"-Entwurfsmuster bezeichnet (Robert C. Martin). Statt in meinem Code direkt die Datenbank aufzurufen -
select * from aufi into table lt_aufi
where abeln = lv_abeln.

verwende ich für diese Angelegenheit einen lokalen Experten. "Lokal" ist in diesem Fall wörtlich gemeint. In meinem Programm führe ich eine lokale Klasse lcl_db ein, die für Datenbankabfragen zuständig ist (und für Updates, Sperren etc.). Das Programm verfügt über eine globale Instanzvariable go_db vom Typ lcl_db, und der Aufruf lautet nun:
go_db->select( exporting iv_abeln = lv_abeln
importing et_aufi = lt_aufi ).

Das ist zwar auf der einen Seite ein etwas erhöhter Schreibaufwand, denn ich muss den select, statt ihn direkt hinzuschreiben, nun über die Klasse lcl_db umleiten. Aber der Gewinn ist beachtlich: Denn in meinen Modultests kann ich nun ohne Datenbank arbeiten, indem ich der Instanzvariablen go_db für die Tests einfach einen anderen Wert zuweise: Einen Zeiger auf das Objekt einer Unterklasse, in der ich alle Methoden von lcl_db redefiniert habe und dort mit von Hand aufgebauten internen Tabellen arbeite. Statt des select auf die aufi kommt nun eine durch den Test selbst aufgebaute und kontrollierte interne Tabelle zurück. Tabellen wie aufi enthalten natürlich viele Felder - die müssen wir nicht alle füllen. Es reicht, diejenigen Felder zu füllen, die in unserem Code auch effektiv abgefragt werden. Sie werden feststellen, dass das nicht besonders viele sind.

Die gleiche Indirektion ist für API-Aufrufe nötig. Es empfiehlt sich eine weitere lokale Klasse lcl_api, die die API-Aufrufe kapselt. Auch hier gibt es eine abgeleitete Testklasse, mit der ich Erfolg oder Misserfolg des API-Aufrufs simulieren kann, ohne dieses wirklich aufzurufen.

Was das User Interface angeht, sollte normalerweise keine weitere Indirektion nötig sein. Das User Interface ist ja, wenn man vernünftig programmiert, ein eigener Layer, der von der Geschäftslogik (Problem Domain, PD) unabhängig ist. Das User Interface sollte man bei Modultests aussparen und sich stattdessen auf die Programmteile beschränken, die die Geschäftslogik enthalten. Das zeigt nebenbei, dass die Modultests unbedingt von Anwendungstests ergänzt werden müssen, da es ja gerade der Zweck von Modultests ist, gezielt Teile wie das User Interface aussparen. Für den Entwicklungsprozess scheinen mir jedoch die Modultests wichtiger zu sein.

Unit Tests lassen sich zu Suiten zusammenfassen

Mein ganzes Wissen, wie meine Klasse funktionieren soll, kann ich in Form von Modultestfällen formulieren. Wenn neue Funktionalität hinzugefügt werden soll, wächst meine Testsuite um weitere Testfälle. Ebenso wächst sie, wenn ein Fehler gemeldet wird, der offenbar zuvor durch die Testsuite nicht abgedeckt war. Es entsteht so eine Testsuite, die ich auf Knopfdruck aufrufen kann, um jederzeit während des Entwicklungsvorgangs zu prüfen, ob bestehende Tests etwa auf "NICHT OK" gelaufen sind. Anders als im oben beschriebenen herkömmlichen Entwicklungsprozess kann ich so nicht nur einen Testfall, sondern einen ganzen Haufen davon als Referenz gegen meinen neuen Code laufen lassen.

JavaScript: Vorsicht mit Methodenreferenzen

Es ist ein empfohlenes Vorgehen, Funktionen in JavaScript nicht über ihren globalen Namen aufzurufen, sondern eine lokale Funktionsreferenz im aktuellen Kontext zu erzeugen. Das gilt vor allem für mehrfache Aufrufe. Hier ein unschönes Beispiel:
function buchen(iString) {
sendRecord( 1 );
sendRecord( 17 );
sendRecord( 21 );
...
}

Hier wird für jeden Funktionsaufruf ein globaler Lookup für das Symbol sendRecord durchgeführt. Es wird also in den lokalen Variablen, danach im "Scope" der Funktion buchen(), danach im übergeordneten "Scope" geschaut, bis das Symbol schliesslich im globalen Scope gefunden wird. Das ist zwar schnell, kann aber bei sehr vielen Funktionsaufrufen Kosten verursachen - vor allem wenn die Funktion im Browser läuft und die Suche sich daher auch auf das window-Objekt erstrecken muss.

Besser ist es hier, mit einer lokalen Funktionsreferenz zu arbeiten, wie in folgendem Beispiel:
function buchen(iString) {
var lSendRecord = sendRecord;
lSendRecord( 1 );
lSendRecord( 17 );
lSendRecord( 21 );
...
}

Nun ist lSendRecord eine Referenz auf die Funktion sendRecord, lebt aber als lokale Variable im "Stack". Das ist der Ort, wo der Lookup als erstes ausgeführt wird. Der globalen Lookup muss hier nur einmal durchgeführt werden, nämlich bei Evaluierung des Wertes von lSendRecord. Die aktuellen Aufrufe der Funktion laufen dann alle über das lokale Symbol. Das macht die JavaScript-Ausführung effizienter.

Vorsicht ist jedoch geboten, wenn man Methoden referenziert. Stellen wir uns der Einfachheit folgendes Singleton-Objekt s vor:
s = {
data:[1,2,3],
getData:function(iIndex) {
return this.data[iIndex];
}
};

Wenn wir nun wie oben einen lokale Methodenreferenz für die Funktion getData erzeugen wollen, bekommen wir ein Problem:
function buchen() {
var lGetData = s.getData;
var lValue = lGetData(1);
...
}

Dieser Code wird nicht wie gewünscht funktionieren. Grund ist die Verwendung des Schlüsselworts this im Rumpf der Methode getData(). Die Methodenreferenz ist lediglich ein Zeiger auf die Implementierung der Methode getData(). Das heisst, der Aufruf lGetData(1) führt den Code einfach genau so aus, wie er in dieser Implementierung angegeben ist. Das Schlüsselwort this wird dabei nicht auf das Objekt umgesetzt. Die Methodenreferenz ist ja eigentlich bloss eine Funktionsreferenz. Sie merkt sich nicht den Context, in dem sie aufgerufen wird, weiss also auch nicht, dass sie Instanzmethode des Singleton-Objekts s ist. Also bekommen wir ein Problem: this.data kann nicht korrekt aufgelöst werden, wenn this nicht auf s zeigt. Die Funktion bricht mit einem JavaScript-Fehler ab. Es ist einer dieser leidigen Fehler, von denen man nicht weiss, was eigentlich los ist (welches Objekt, welche Methode?)

Das Objekt unterstützt diese Eigenschaft oder Methode nicht.

Wie kann man obiges Beispiel also korrigieren? Indem man eine Objektreferenz statt einer Methodenreferenz verwendet.
function buchen() {
var lS = s;
var lValue = lS.getData(1);
...
}

Das ergibt zwar einen zweistufigen Lookup, aber beide sind schnell: der erste ist die Auflösung der lokalen Variablen lS, der zweite sucht die Komponente getData im Singleton s. Da s technisch nichts anderes als ein Hash ist, ist auch dieser zweite Lookup sehr schnell. Und, das wichtigste: Die Methode hat nun überhaupt erst die Chance, korrekt ausgeführt zu werden!

Noch eine Anmerkung zm Thema Indirektion. Noch besser wäre es, sich die Singleton-Instanz über eine get-Methode zu beschaffen, zum Beispiel so:
function buchen() {
var lS = getS();
var lValue = lS.getData(1);
...
}

Warum ist das besser? Weil man Implementierungen austauschen kann. Man kann "mocken" und "stubben", wenn man Tests für die Methode schreibt: Wenn man im Testkontext die getS()-Funktion so überschreibt, dass sie statt s eine Testinstanz derselben Signatur (also mit denselben Komponenten wie s) zurückgibt, kann man den aufrufenden Code isoliert testen (indem die Testimplementierung von s das zurückgibt, was man eben so von s erwartet).

Samstag, 9. Juni 2007

Web-Anwendungen versus Web-Anwendungs-Frameworks

Das neue Java Spektrum Juni/Juli 07 hat "Frameworks für Web-Anwendungen" als Schwerpunktthema. Wenn ich mir die Beiträge so durchlese, werde ich den Verdacht nicht los, dass sich hier die Frameworks selbst in den Mittelpunkt stellen. Die Web-Anwendung dient dabei nur noch als Strohmann, als Sinngeber für die Framework-Entwickler: Schaut her, mit unserem Framework wird die Entwicklung dieser Web-Anwendungen vereinfacht.

Aber wird sie das wirklich? Wo die Vorgehensweise einmal in allen Details vorgeführt wird, wie in dem sehr gut nachvollziehbaren Artikel von Klaus P. Berg über das Google Web Toolkit, hat man eher den gegenteiligen Eindruck. Eine steile Lernkurve scheint nötig zu sein, der Artikel verheisst lange Nächte des Bastelns. Man wird sich auf eine neue Technologie einlassen, die sicher interessant kennenzulernen ist. Man wird im Endeffekt eine Web-Anwendung erhalten, die man mit konventionellen Mitteln mit weniger Aufwand ebenfalls hinbekommen hätte. Hier fände ich Ockhhams Rasiermesser einmal angebracht! Der Weg, auf dem man mit weniger Mitteln zum selben Ziel kommt, ist der vorzuziehende. Es sind ja meine Nächte, die ich mir um die Ohren hauen muss! :-)

Darüberhinaus habe ich ein tief sitzendes Misstrauen gegen alle Arten von HTML-Code-Generierung. Möglicherweise ist dies in einem frühen FrontPage-Trauma begründet, als ich erleben musste, wie mir mein ganzer schöner HTML-Code durch irgendwelche von der Maschine hingeschmierten Konstrukte überschrieben wurde. Aber auch mit anderen Tools wie DreamWeaver habe ich Ähnliches erlebt, bis ich vor vielen Jahren zu Plain Text Editoren für alle meine HTML-Seiten überging (und übrigens auch für meine Java-Programme: ich brauche keine "Builder"-Monstren, sondern schreibe mir je Projekt ein kleines Script mit den nötigen Anweisungen zum Compilieren). Klaus P. Berg berichtet, dass auch das Google Web Toolkit HTML-Code generiert - wie alle "Top of the Art" Frameworks für Web-Anwendungen, aber gnädigerweise eine Art "Native Interface" vorsieht, damit man doch noch hier und dort ein paar HTML-Tags einbauen darf.

Den Vergleich eines solchen HTML-Exits mit Suns Java Native Interface (JNI) finde ich allerdings deplaziert. Das Java Native Interface bietet Absprünge für maschinenspezifischen Code. Aber HTML, CSS und JavaScript sind nicht maschinenspezifisch. Für alle drei Sprachen gibt es Standards; wenn man diese einhält, hat man mit den Browsern dieser Welt keine grösseren Probleme. Es ist nicht korrekt, HTML, CSS und JavaScript als "Low Level"-Konstrukte hinzustellen, auf die man von seiner Framework-Warte hinabblicken kann.

An Stelle derartiger Frameworks wäre eine umgekehrte Perspektive die richtige, wenn man wirklich die Arbeit der Web-Entwickler unterstützen möchte: Der Web-Entwickler entwirft die Views, die Steuerung und die Logik seiner Anwendungen. Für Unterstützung bei der Entwicklung der Views - also der HTML-Fragmente, die zu einer vollständigen Webseite zusammengesetzt werden, ist jeder Web-Entwickler dankbar. Dazu gehören Tag Libraries, um den HTML-Code einzelner graphischer Elemente wie Buttons oder Inputfelder übergreifend und einheitlich herzustellen. Dazu gehören auch schlanke JavaScript-Bibliotheken wie Sarissa oder Prototype, die ihm die Arbeit abnehmen, browserübergreifend Ajax-Requests zu versenden oder Seiteninhalte dynamisch zu verändern. Ein HTML-Validator wie Tidy ist unverzichtbar. Letztlich behält der Web-Entwickler aber die Kontrolle über den in die Welt gesandten HTML-Code. Wenn die Anwendungslogik unter Einhaltung des Model-View-Controller Archtitekturmusters angebunden ist, ergibt sich insgesamt eine saubere Web-Anwendung, deren Wartung oder Weiterentwicklung ich jederzeit gern von einem Kollegen übernehmen würde!

Donnerstag, 7. Juni 2007

Lokale Klassen

Lokale Klassen (das ABAP-Analogon zu inneren Klassen) kommen durch ABAP Unit wieder neu zu Ehren. Tests werden in lokalen Klassen des zu testenden Programms hinterlegt. Diese lokalen Klassen sind durch den Zusatz for testing als Testklassen ausgezeichnet. Die Unit Test Laufzeit erfasst alle mit dem Zusatz for testing gekennzeichneten Methoden und führt sie aus.

Während lokale Klassen für das Testen ideal geeignet sind (denn ein Test ist spezifisch für ein bestimmtes Programm), sollten sie für den Entwurf des produktiven Codes normalerweise vermieden werden. Es ist besser, sich eine passende Schnittstelle zu überlegen und eine globale Klasse mit dem Class Builder zu erstellen. Das erfordert natürlich die Arbeit am Entwurf: Was soll mein Objekt können, wie redet es mit anderen Objekten, was ist öffentlich, was privat. Wenn man mit lokalen Klassen arbeitet, kann man sich um all diese Fragen drücken. Denn "öffentlich" bedeutet für lokale Klassen im allgemeinen nur Sichtbarkeit im einbettenden Programm.

Vor allem drückt man sich mit lokalen Klassen um die Frage: Welche Teile meiner Software sind von anderen wiederverwendbar. Man hat keine Abstraktionslayer, kein "Oben-Unten", um allgemeinen Code von spezifischem Code zu unterscheiden, sondern man erklärt einfach alles als spezifisch. So bekommt man keine lästigen Anfragen oder Supportfälle von Kollegen, die an den eigenen Funktionen interessiert sind und sie in ihrem Code verwenden wollen. Im Extrem sieht man das in Funktionsgruppen wie der MEGUI. Das ist eine Funktionsgruppe mit nicht weniger als 88 lokalen Klassen im Bauch.

Man könnte einwenden: "Was geht es dich an, wenn ich meine Funktionsgruppe intern mit lokalen Klassen strukturiere? Kümmere dich um deine eigenen Angelegenheiten!" Zugegeben, Teilfunktionalität in lokale Klassen zu verlagern ist immer noch besser als mit Unterprogrammen zu arbeiten. Aber kann es sein, dass von dem gesamten Code von 88 lokalen Klassen keine einzige Zeile wiederverwendbar ist? Egal, wie man diese Frage beantwortet: Auf jeden Fall stimmt etwas mit dem Software-Design nicht. Wenn es wiederverwendbare Teile gibt, sollten diese von anderen Programmen aufgerufen werden können. Wenn es aber in 88 Klassen keine einzige wiederverwendbare Programmzeile gibt, muss leider der ganze Entwurf als misslungen betrachtet werden.

Man kann übrigens durchaus einen Abstraktionslayer für lokale Klassen einführen, indem man sie von globalen Klassen erben lässt:


class lcl_special definition
inheriting from zcl_general.
...


Dabei steht zcl_general für die im Class Builder erstellte Klasse. Das kann beispielsweise eine abstrakte Klasse sein. Wiederverwendbaren Code verschiebt man in zcl_general, programmspezifischen Code belässt man in der lokalen Klasse.

Wenn das einbettende Programm, die einbettende Funktionsgruppe oder einbettende globale Klasse eine get_instance Funktion anbietet, kann sogar die lokale Instanz in fremden Kontexten verwendet werden. Hier das Beispiel eines Unterprogramms, das extern gerufen werden kann, um die Instanz zu besorgen.


...
data: go_special type ref to lcl_special.
...
form get_special changing eo_special type ref to zcl_general.
if go_special is not bound.
create object go_special.
endif.
eo_special ?= go_special.
endform.


Aber wenn man derartige Konstruktionen macht, stellt sich die Frage: Warum dann nicht gleich die ganze lokale Klasse in den Class Builder schieben und als Kind von zcl_general deklarieren?

Dennoch will ich lokale Klassen nicht völlig ablehnen. In manchen Fällen ist ihr Gebrauch geradezu ideal - nämlich immer dann, wenn man spezifischen Code hat und diesen innerhalb des bestehenden Objektes noch besser kapseln möchte. Ich sehe folgende Einsatzgebiete für lokale Klassen:

  • Testklassen sind lokal, denn sie sind Reflexionen oder Kontrollinstanzen des aktuellen Programms.
  • Ereignisbehandler sind oft nötig, um eine Kopplung, eine Transmission zwischen auslösendem und behandelndem Programmteil zu ermöglichen. Wenn das behandelnde Programmteil keine Methode ist (dann könnte man sie gleich in der Workbench als Behandler deklarieren), sondern etwa ein Funktionsbaustein oder ein Unterprogramm, so bedarf es einer Hilfsklasse, die das Ereignis dispatcht. Diese Hilfsklasse hat eine so spezifische Aufgabe, dass sich eine Verallgemeinerung nicht lohnt, man wird sie in der Regel als lokale Klasse implementieren.
  • Automatisch generierte Klassen. Wie eine globale Klasse automatisch zu generieren ist, entzieht sich meiner Kenntnis. Vermutlich ist es kompliziert. Die "automatische Generierung des kleinen Mannes" ist dagegen immer noch die ABAP-Anweisung insert report lv_repid from table lt_code., wobei lt_code die Codezeilen enthält. lt_code kann einen Subroutinenpool mit einer lokalen Klasse und einem Unterprogramm zur Instanzbeschaffung enthalten, das wie oben dargestellt gestaltet ist. Externe Aufrufer beziehen sich auf einen im Class Builder definierten Objekttyp, auf eine Klasse oder ein Interface. Das vom Unterprogramm zurückgegebene Objekt ist dann die jeweilige Spezialisierung. Ich habe dieses Konzept in meinem "BSP-Praxisbuch" im Kapitel 18 am Beispiel des "Generischen Tabellenhandlers" beschrieben.
  • Auch in Beispielprogrammen, in denen man z.B. einen Vererbungsbaum simuliert und gewisse OO-Features ausprobieren möchte, sind lokale Klassen ideal.

In allen anderen Fällen ist es aus den oben angeführten Gründen meist besser, gleich mit dem Class Builder eine globale Klasse anzulegen.

Mittwoch, 6. Juni 2007

ABAP Unit

Nun geht es in grossen Schritten weiter mit ABAP Unit, dem neuen Tool für Modultests von SAP, verfügbar ab Basisrelease 7.0. Hier ein erstes Beispiel, ich werde meine Erfahrungen noch genauer dokumentieren. Das Beispiel soll nur zeigen, wie es funktioniert.

Nämlich so wie alle Unit Tests: Eine Sammlung von Testmethoden wird bei Wahl des Menüpunkts "Modultest" (an manchen Stellen geht auch die Kombination Strg+Shift+F10) automatisch abgearbeitet.

Die folgenden drei Screenshots - mit einem Klick können sie in Originalgrösse betrachtet werden - geben einen ersten Eindruck. Ich habe in einer Klasse einige Unit Tests eingebaut. Um zu zeigen, wie es funktioniert, habe ich nun absichtlich einen Fehler eingebaut, indem ich eine Zeile in der Methode modify_aufi() auskommentiert habe:



Nach Druck auf "Modultest" kommt ein umfassendes hierarchisches Protokoll. Zwei Methoden namens test_assign() (in zwei verschiedenen Testklassen gelegen, falls sich jemand wegen der gleichen Namen wundert) sind auf Fehler gelaufen Das Detailprotokoll meldet einen "Kritischen Prüffehler: AUFI wurde nicht aktualisiert". Das Fenster recht unten gibt die Abweichungen von erwartetem und tatsächlichem Wert an. Ausserdem kann man direkt an die Codestelle verzweigen, um
den Fehler zu analysieren. Die Fehlerursache liegt natürlich nicht in der Testmethode, sondern an einem anderen Ort. Da aber eine meiner letzten Änderungen den Fehler verursacht haben muss, weiss ich, wo ich den Breakpoint setzen muss, um den Fehler zu finden. Meist finde ich ihn bereits ohne Debugger, durch blosse Betrachtung meiner letzten Änderungen.



Nun mache ich die auskommentierte Zeile wieder aktiv. Wenn ich wieder "Modultest" wähle, wird das Protokoll ganz unscheinbar als Success-Meldung in der Statuszeile angezeigt. Wozu braucht man auch in diesem Fall ein detailliertes Protokoll.



Meine Erfahrungen mit ABAP Unit werden in den nächsten Tagen hier veröffentlicht werden.

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.