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.

Keine Kommentare :