Im Rahmen einer Verlosung von Softwarequalität in PHP-Projekten durch den Hanser-Verlag konnten Leser im Blog Fragen rund um die Themen und Inhalte des Buches stellen. Sebastian Bergmann und ich werden diese Fragen in loser Folge in Form von Blogeinträgen beantworten.

Maximilian Berghoff fragte uns nach Datenbankinteraktionstests. Seine interessante Frage berührt ein sehr wichtiges Teilgebiet der Testautomation, das eigentlich nicht allzu viel mit klassischen Unit-Tests gemeinsam hat. Dass dennoch ein xUnit-Testframework zum Einsatz kommt, führt nicht selten zu Verwirrung.

Zunächst einmal ist es wichtig, zu wissen, dass im Sinne einer nachhaltigen, wartbaren und vor allem testbaren Software immer die Geschäftslogik vom Datenzugriff getrennt sein muss. In einer klassischen Dreischichtenarchitektur repräsentieren die Geschäftslogik und der Datenbankzugriff die beiden unteren Schichten, darüber liegt die Präsentationsschicht. Kommt ein Entwurfsmuster wie Active Record zum Einsatz, das Geschäftslogik und Datenzugriff verzahnt, bezahlt man einen hohen Preis für den schnellen Erfolg, den dieses Muster verspricht, da der Datenzugriff nicht mehr unabhängig von der Geschäftslogik getestet werden kann. Das führt oft zu einer Verzahnung von Tests für die Geschäftslogik mit Tests für die Persistenz, insbesondere mit Tests der Interaktion mit einer Datenbank. Die oben angedeutete Verwirrung ist damit perfekt.

Die Organisation der Anwendung in Schichten fördert die Trennung unterschiedlicher Belange im Code. Mit anderen Worten: die Geschäftslogik muss vom Datenzugriff völlig unabhängig sein. Geschäftsobjekte dürfen nicht wissen, dass es eine Datenbank gibt. Diese strikte Trennung ermöglicht es, durch Austausch der Datenzugriffsschicht eine völlig andere Art der Datenspeicherung einzuführen, ohne Geschäftslogik oder Präsentation anpassen zu müssen.

Genau diese Flexibilität erweist sich beim Testen einer Anwendung als extrem wertvoll: zu Testzwecken kann nämlich nun die gesamte Datenzugriffsschicht durch eine Atrappe (Englisch: Stub) ausgetauscht werden. Ein Stub ist eine Komponente, die keine wirkliche Arbeit erledigt, sondern lediglich vorab hinterlegte Ergebnisse zurückliefert. Ein Stub der Datenzugriffsschicht greift gar nicht auf eine echte Datenbank zu, sondern liefert auf die (in Form von Methodenaufrufen) gestellten Anfragen einfach vorbereitete und fest hinterlegte Ergebnisse beziehungsweise vermeldet einfach immer Erfolg, wenn er angewiesen wird, Daten zu speichern.

Auf diese Weise lässt sich das Zusammenspiel der Geschäftslogik mit der Datenzugriffsschicht testen, ohne dass eine echte Datenbank zum Einsatz kommt. Die Geschäftslogik an sich kann dank sauberer Trennung der einzelnen Schichten unabhängig von der Präsentation und dem Datenzugriff getestet werden. Weitere externe Dienste, auf die eine Anwendung möglicherweise zugreift, lassen sich auf die gleiche Weise testen. Dabei werden die Clients, durch die der Zugriff erfolgt, durch Stubs ersetzt, die ebenfalls hinterlegte Ergebnisse liefern, ohne dass dazu Kommunikation mit einem entfernten System nötig ist.

Beim Testen der Datenzugriffsschicht selbst steht man nun vor einem interessanten Problem. Eine SQL-Abfrage ist nämlich ein Programm, das von der getesteten Anwendung (oft dynamisch) erzeugt wurde. In einfachen Anwendungsfällen genügt es, eine fest hinterlegte SQL-Abfrage geeignet zu parametrisieren. Komplexere SQL-Statements werden durch String-Konkatenation zusammengesetzt und zusätzlich parametrisiert.

Obwohl SQL-Statements relativ kurze Programme sind, ist es für Entwickler alles andere als einfach, durch bloßes Lesen des Codes zu entscheiden, ob ein SQL-Statement gültig ist, geschweige denn zu beurteilen, ob das Statement die gewünschte Wirkung entfaltet. Der einfachste Weg, dies zu tun, ist, das SQL-Statement an die Datenbank zu senden und tatsächlich auszuführen. Da jede Datenbank eine etwas eigene Vorstellung von SQL hat, sollte man für solche Tests immer die Datenbank-Engine verwenden, die später im Livebetrieb eingesetzt werden soll.

Man kann nun ein Testframework wie PHPUnit verwenden, um in mehreren voneinander isolierten automatisierten Tests zu testen, ob die Datenbank alle SQL-Statements der Anwendung fehlerfrei verarbeiten kann. Damit dies funktioniert, benötigt man als Testumgebung (auch Testinventar oder Fixture genannt) zumindest jeweils die Tabellen, auf die das gerade getestete SQL-Statement zugreift. Um neben Einfüge-Operationen auch Abfrage-, Änderungs- und Löschoperationen zu testen, benötigt man darüber hinaus auch geeignete Testdatensätze.

Auf diese Weise lässt sich durch automatisierte Tests nicht nur prüfen, ob SQL-Statements fehlerfrei ausgeführt werden können, sondern auch sicherstellen, dass nach der Ausführung auch tatsächlich die richtigen Daten in der Datenbank stehen. Für PHPUnit existiert mit DBUnit eine Erweiterung, die einem nicht nur das Aufsetzen einer Testdatenbank erleichtert, sondern auch Zusicherungen bereitstellt, mit denen geprüft werden kann, ob nach Ausführung des SQL-Statements die Inhalte aller relevanten Datenbanktabellen den Erwartungen entsprechen. Softwarequalität in PHP-Projekten hat dem Testen von Datenbankinteraktionen übrigens ein ganzes Buchkapitel gewidmet, in dem der Einsatz von DBUnit im Detail beschrieben ist.

Ein SQL-Statement kann durch eine veränderte Parametrisierung durchaus ungültig werden, wie das folgende Beispiel zeigt. Wir nehmen dabei an, dass wir einen Benutzer aus der Tabelle Users lesen wollen, der durch eine ganzzahlige ID identifiert wird. Das SQL-Statement dazu könnte wie folgt erzeugt werden:

$sql = ‚SELECT … FROM Users WHERE id=‘ . $id;

Hat die Variable $id den Wert 42, ergibt sich das folgende Statement:

SELECT … FROM Users WHERE id=42

Wird allerdings für die Variable $id kein ganzzahliger Wert eingesetzt, ergibt sich:

SELECT … FROM Users WHERE id=not-an-integer

Dieses SQL-Statement ist syntaktisch nicht mehr korrekt, da Hochkommata fehlen. Menschen mit ausreichend krimineller Energie (oder übermäßigem Spieltrieb) versuchen nun, durch Ausprobieren verschiedener Eingaben ein SQL-Statement zu erzeugen, dass gänzlich unerwünschte Dinge tut. Wie wäre es beispielsweise mit dem Wert „0 OR 1=1“?

Man sieht, dass SQL-Injections (denn über nichts anderes sprechen wir hier) eine Folge von nicht ausreichend getestetem Datenzugriffscode zur Erzeugung von SQL-Statements ist. Offenbar reicht es aber auch nicht aus, den Code nur mit einer Eingabe zu testen. Auf der anderen Seite gibt es bereits für einige wenige Parameter eine solch große kombinatorische Vielfalt von Möglichkeiten, dass es nicht möglich ist, alle Kombinationen von Parametern zu testen.

Im obigen Beispiel könnte man in der Theorie für jeden denkbaren Integer-Wert einen eigenen Test schreiben. Sieht man sich den Code an, wird jedoch deutlich, dass das Verhalten für jeden Integer-Wert gleich sein wird. Alle Integer-Werte bilden für das obigen Beispiel damit eine so genannte Äquivalenzklasse. In der Praxis bedeutet dies, dass man nur einen einzigen Testfall schreiben wird, in der ein möglichst repräsentativer Wert (42 bietet sich hierfür natürlich immer an) verwendet wird, um den Code sozusagen stellvertretend für alle Integer-Werte zu testen.

Es empfiehlt sich, im ersten Schritt immer den so genannten Happy Path, also den erwarteten Alles-geht-gut-Fall, zu testen. Danach sollte man für eine Weile den Angreifer-Hut aufsetzen und überlegen, mit welchen Eingaben man den Code möglicherweise zu Fall bringen könnte. Auch hier werden wieder Äquivalenzklassen gebildet. Würde man im obigen Beispiel etwa durch einen Aufruf von is_int() in einer Guard Clause sicherstellen, dass $id auch tatsächlich eine ganze Zahl ist, dann wären alle nicht-Integer-Werte eine weitere Äquivalenzklasse und damit Eingabe für einen weiteren Testfall, der vermutlich eine Exception im Produktionscode auslösen würde, auf die man dank der von PHPUnit unterstützen Annotation @expectedException einfach testen kann.

Wenn Sie weitere Fragen haben, lassen Sie es uns wissen. Wir werden wir diese in weiteren Blogposts beantworten, sowie es unsere Zeit erlaubt.

Softwarequalität in PHP-Projekten

Softwarequalität in PHP-Projekten

Mehr Informationen zum Thema Softwarequalität in PHP-Projekten im Allgemeinen finden Sie in der aktuellen Ausgabe unseres gleichnamigen Buchs.