Wenn man SQL für mehr als nur CURD verwendet, können Unit-Tests für komplexe Abfragen Sinn machen. Solche Tests erfordern jedoch genau definierte Testdaten, damit die getestete Abfrage das erwartete Ergebnis liefert.
Die Testdaten werden oft in sogenannten Set-up-Funktionen angelegt und nach dem Test in einer Tear-down-Funktion wieder gelöscht. Bei diesem Vorgehen können sich die Testfälle jedoch gegenseitig beeinflussen: zum einen, wenn Tests parallel ausgeführt werden, aber auch wenn wenn die Tear-down-Funktion nicht ordentlich aufräumt.
Beide Probleme können vermieden werden, wenn man die Tests mit transienten Testdaten durchführt, die nach dem Test automatisch wieder verschwinden.
Option 1: Offene Transaktionen
Die einfachste und vor allem flexibelste Möglichkeit ist es, das Set-up und den Test in einer offenen Transaktion durchzuführen, die im Tear-down durch Rollback zurückgenommen wir
Diese Methode bietet mehrere Vorteile:
Verhältnismäßig einfach umzusetzen
Natürlich muss sichergestellt werden, dass Set-up, Test und Tear-down in derselben Datenbank-Session laufen. Weiters müssen ungewollte Commits unterbunden werden. Das betrifft vor allem Commits, die vom getesteten Code selbst ausgeführt werden. Diese könnte man notfalls durch eine Mock-Implementierung der Datenbankverbindung abfangen. Impliziten Commits kann man damit auch nicht verhindern (z. B. durch DDL-Anweisungen).
Komplette Abläufe können getestet werden
Das einzige Limit für die Komplexität des Testfalles ist, dass man mit einer einzigen Transaktion auskommen muss.
Paralleles testen ist möglich
Die Testdaten sind jeweils nur in der offenen Transaktion sichtbar0 und können daher andere Vorgänge in der Datenbank nicht beeinflussen. Constraints können jedoch Verzögerungen verursachen – wenn zum Beispiel zwei Testfälle denselben Primärschlüsselwert einfügen.
Die Parallele Ausführung kann sogar helfe, falsch gewählte Transaktions-Isolations-Level zu finden.
Performancetests sind möglich
Da die Testabfragen selbst unverändert ablaufen, können die Testfälle auch für Performanceanalysen und Stresstests herangezogen werden.
Option 2: with
und values
Diese Methode kombiniert zwei Elemente: (1) die Tabellenzugriffe einer Abfrage werden durch gleichnamige with
-Abfagen „gekapert“; (2) die with
-Abfagen erzeugten die Testdaten mit der values
-Klausel „aus dem nichts“.
WITH cart (product_id, qty) AS (
VALUES (1, 2)
)
SELECT ...
FROM cart
...
In diesem Beispiel wurde einer zu testenden Abfrage eine with
-Klausel vorangestellt. Essenziell ist, dass der Tabellenname cart
, auf den die Abfrage zugreift, in der with
-Klausel überlagert wird. Diese Überlagerung funktioniert jedoch nicht, wenn der Tabellenzugriff schemaqualifiziert, oder durch einen View erfolgt (Achtung: Kompatibilität).
Wenn die zu testende Abfrage selbst eine with
-Klausel hat – zum Beispiel weil es literatisches SQL ist –, kann man sie als Unterabfrage verwenden, und die Testdaten mit einer with
-Klausel in der Hauptabfrage zur Verfügung stellen:
WITH cart (product_id, qty) AS (
VALUES (1, 2)
)
SELECT *
FROM ( WITH ...
SELECT ...
FROM cart
...
) test_query
Die Testdaten werden mit der values
-Klausel erzeugt. Falls die Zieldatenbank values
ohne insert
nicht unterstützt, muss man auf die in select
ohne from
aufgezeigten konformen oder proprietären Alternativen zurückgreifen.
Die Geschwindigkeit ist der Hauptvorteil dieser Methode: Ein Datenbank-Aufruf kombiniert die Testdaten mit der Testabfrage. Die Testfälle können auch parallel ausgeführt werden, ohne sich gegenseitig zu beeinflussen – nicht einmal durch Wartezeiten auf Locks (lock contention).
Der schlagende Nachteil ist, dass diese Methode nur funktionale Tests einzelner Abfragen zulässt. Rückschlüsse auf nicht-funktionale Aspekte wie Performance sind nicht möglich. Auch die Umsetzung ist höchst ungewöhnlich: Dem SQL-String wird die with
-Klausel vorangestellt oder die Test-Abfrage wird überhaupt als Unterabfrage verwendet.
Kompatibilität
- Es scheint, als können CTEs in Unterabfragen globale CTEs nicht sehen
- Erfordert Spaltennamen in der
With
-Klausel:with x (c1,c2) as (values …)
• Nur ohne Schlüsselwortrow
- Nur ohne Schlüsselwort
row
- Nur mit Schlüsselwort
row
:values row('r1c1','r1c2'), row('r2c1', 'r2c2')
- Proprietäre Erweiterung (nicht Standard!)