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 nicht-standard 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

BigQuery 2026-03-02aDb2 (LUW) 12.1.4bDuckDB 1.5.0cH2 2.4.240MariaDB 12.2.2acMySQL 9.6.0dOracle DB 23.26.1bPostgreSQL 18cSQL Server 2025SQLite 3.52.0cwith 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')

20 Jahre SQL-Evolution kann man nicht an einem Tag nachholen. Abonniere den Newsletter via E-Mail, Bluesky oder RSS, um sukzessive aufzuholen und modern-sql.com am Radar zu behalten.

Ü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.

Neuigkeiten Abonnieren

Markus Winands MailinglistenMarkus Winands RSS-FeedMarkus Winand auf LinkedInMarkus Winand auf XINGMarkus Winand auf MastodonMarkus Winand auf Bluesky
Copyright 2015-2026 Markus Winand. Alle Rechte vorbehalten.
Impressum | Kontakt | KEINE GEWÄHR | Handelsmarken | Datenschutz und DSGVO