Unit-Tests mit transienten Daten


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

BigQueryaeDb2 (LUW)bMariaDBaceMySQLdeOracle DBPostgreSQLceSQL ServeraeSQLitecewith auf oberster Ebenewith in UnterabfragenWith t as (values …)select ohne from
  1. Es scheint, als können CTEs in Unterabfragen globale CTEs nicht sehen
  2. Erfordert Spaltennamen in der With-Klausel: with x (c1,c2) as (values …) • Nur ohne Schlüsselwort row
  3. Nur ohne Schlüsselwort row
  4. Nur mit Schlüsselwort row: values row('r1c1','r1c2'), row('r2c1', 'r2c2')
  5. Proprietäre Erweiterung (nicht Standard!)

Über den Autor

Foto von Markus Winand

Markus Winand gibt auf modern-sql.com Einblick in SQL und zeigt, wie es von verschiedenen Systemen unterstützt wird. Zuvor machte er use-the-index-luke.com, was er noch immer wartet. Markus kann als Trainer, Sprecher und Berater auf winand.at engagiert werden.

Sein Buch kaufen

Titelbild von „SQL Performance Explained“: Eichhörnchen läuft durchs Grass

Die Essenz: SQL-Tuning auf 200 Seiten

Jetzt Kaufen
(Taschenbuch und/oder PDF)

Sein Training

Markus verwandelt veraltetes SQL-92-Wissen in solides und zeitgemäßes SQL-Know-how

Erfahren Sie mehr»

Fußnoten

  1. Vorausgesetzt der Transaktions-Isolations-Level ist nicht auf READ_UNCOMMITTED gesetzt.

Mit Markus Winand verbinden

Markus Winand auf LinkedInMarkus Winand auf XINGMarkus Winand auf Twitter
„modern SQL“ von Markus Winand ist unter einer Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License lizenziert.
Impressum | Kontakt | KEINE GEWÄHR | Handelsmarken | Datenschutz und DSGVO | CC-BY-NC-ND 3.0 Lizenz