Freitag, 28. Dezember 2007

Stubs: Sich selbst statt andere redefinieren!

Die wesentliche Idee der Unit Tests ist, die einzelne Software-Unit isoliert vom Rest der Welt zu testen. Das zu testende Objekte tritt mit diesem "Rest der Welt" beispielsweise in Form von Methodenaufrufen anderer Objekte in Wechselwirkung.

Der Standardtrick, um diese Methodenaufrufe im Unit Test abzuklemmen, ist die Verwendung eines Stubs: Ich definiere für Testzwecke eine Subklasse des fremden Objekts und schiebe dem zu testenden Objekt diese Instanz an Stelle des produktiven Objekts unter. Diese Subklasse kann entweder leere Methodenimplementierungen enthalten, oder so tun, als ob es die Aufgabe des Fremdobjekts erledigen würde. Es kann auch prüfen, ob die Aufrufparameter die erwarteten sind.

Die Wechselwirkung mit dem produktiven Code wird durch Stubs minimiert. Ich verwende in der produktiven Klasse lediglich eine Klassenmethode get_instance_for_testing( ) an Stelle der produktiven get_instance() Methode. Diese erzeugt eine ganz normale Instanz, ersetzt aber dann die Instanzvariablen, die auf Delegationsobjekte verweisen, durch Referenzen auf die Stubs.

Leider ist dieses Vorgehen problematisch. Denn es erzeugt Abhängigkeiten von den fremden Objekten. Wenn ich eine Klasse redefinieren will, darf diese zunächst mal nicht final sein. Wenn die Klasse abstrakte Methoden enthält, muss ich all diese abstrakten Methoden redefinieren, um selbst eine konkrete Instanz bilden zu können. Damit hänge ich von Designentscheidungen meines Kollegen ab. Wenn er sich entscheidet, gewisse Methoden als abstrakt zu kennzeichnen, die es vorher nicht waren, bekommt meine Klasse einen Syntaxfehler. Wenn er geschützte Methoden löscht oder umbenennt, weil ihm ein besseres Design eingefallen ist, bekomme ich ebenfalls Syntaxfehler.

All diese Abhängigkeiten sind unschön - und vermeidbar! Ich kann nämlich eine eigene Indirektionsebene einführen, die ganz allein meiner Klasse gehört, indem ich alle API-Aufrufe - seien dies Funktionsbausteinaufrufe, Methodenaufrufe oder gar externe Performs - in einer lokalen Klasse lcl_api kapsele. Das hat darüberhinaus den Vorteil, dass ich ein etwaiges Schnittstellenmapping noch in diese Klasse auslagern kann. Dadurch wird der Code der Hauptklasse besser lesbar, denn er konzentriert sich auf das Wesentliche. Statt die Methode eines fremden Objekts aufzurufen, rufe ich nun eine Methode meines API-Objekts go_api auf. Und statt fremde Klassen zu redefinieren, die das vielleicht gar nicht gerne mögen, redefiniere ich meine eigene lokale Klasse lcl_api durch einen lcl_api_stub. Diese Redefinition ist stabil gegenüber Klassenänderungen meines Kollegen. Denn die Klasse lcl_api ruft lediglich die von mir benötigte öffentliche Methode der fremden Klasse auf. Das ist die einzige Abhängigkeit. Ich bekomme also nur noch Syntaxfehler, wenn die fremde Klasse fehlerhaft wird oder wenn die öffentliche Methode inkompatibel geändert wird. Das ist klar und unvermeidbar: Wer die Schnittstellen seiner öffentlichen Methoden inkompatibel ändert, ist selbstverständlich in der Pflicht, alle Aufrufer anzupassen.

Keine Kommentare :