dbt-Tests auf Azure Databricks: Welche Tests wirklich sinnvoll sind

Azure Databricks10 Min. LesezeitStand: August 2026
dbt-Tests auf Azure Databricks Datenqualität als Code im Repository, im selben Deployment-Prozess Data Tests prüfen die Daten in der Tabelle nach dem Build Standard-Tests unique, not_null, relationships, accepted_values Packages dbt_expectations, dbt_utils Eigene Tests Singular Tests, Custom Generic Tests Unit-Tests prüfen die Transformationslogik vor dem Build, in der CI Typische Fälle Regex, Datumslogik, Fensterfunktionen Testdaten als dict, CSV oder SQL, inline oder als Datei Grenzen nur SQL-Modelle, ab dbt 1.8

Ein Test in dbt ist im Kern nichts anderes als eine SQL-Abfrage, die fehlerhafte Zeilen zurückgibt. Kommen null Zeilen zurück, gilt der Test als bestanden. Das klingt unspektakulär, hat aber eine wichtige Konsequenz: Tests liegen versioniert im Repository neben den Modellen, laufen im selben Deployment-Prozess mit und brauchen kein separates Werkzeug. Datenqualität wird damit Teil der Pipeline, statt eine nachgelagerte Kontrolle zu sein, die irgendwann niemand mehr durchführt.

Die Frage ist deshalb selten, ob getestet wird, sondern welche der vielen möglichen Tests sich wirklich lohnen. Aus unseren dbt-Projekten auf Azure Databricks hat sich eine überschaubare Kombination bewährt, und die lässt sich in wenigen Tagen aufsetzen.

Data Tests und Unit-Tests

dbt kennt zwei Arten von Tests, und die Unterscheidung lohnt sich, bevor man das erste YAML schreibt.

Data Tests prüfen Daten, die schon in der Tabelle liegen, laufen also nach dem Build. Innerhalb dieser Kategorie gibt es Generic Tests, die auf Spalten oder Modelle konfiguriert und wiederverwendet werden, und Singular Tests, also einzelne SQL-Dateien für einen konkreten Einzelfall. Zu den Generic Tests gehören die vier eingebauten Tests, alle Tests aus Packages wie dbt_expectations und selbst geschriebene Custom Generic Tests.

Unit-Tests prüfen die Transformationslogik, und zwar mit Mock-Daten, ohne dass echte Tabellen im Spiel sind. Sie laufen vor dem Build und beantworten eine andere Frage: nicht, ob die Daten in Ordnung sind, sondern ob das SQL richtig rechnet.

Die vier Standard-Tests als Basis

Die vier eingebauten Tests unique, not_null, relationships und accepted_values sind in wenigen YAML-Zeilen konfiguriert und sollten auf jedem Modell die Grundlage bilden. Am Beispiel eines Lieferkopfs und der zugehörigen Positionen:

YAML
models:
  - name: deliveries
    columns:
      - name: delivery_id
        data_tests:
          - unique
          - not_null

  - name: delivery_items
    columns:
      - name: delivery_id
        data_tests:
          - relationships:
              to: ref('deliveries')
              field: delivery_id
              config:
                severity: error
      - name: item_status
        data_tests:
          - accepted_values:
              values: ["open", "picked", "shipped"]

Zwei Stolpersteine, die in der Praxis regelmäßig auftauchen:

relationships prüft nur die Richtung von n zu 1. Positionen ohne Lieferkopf findet der Test, Lieferköpfe ohne Positionen nicht. Wenn das fachlich relevant ist, ergänzt man einen Singular Test. Das ist eine einzelne SQL-Datei unter tests/, die die fehlerhaften Zeilen zurückgibt. Eine YAML-Konfiguration ist nicht nötig, dbt findet und führt die Datei automatisch aus:

SQL
-- tests/deliveries_without_items.sql
select d.delivery_id
from {{ ref('deliveries') }} d
left join {{ ref('delivery_items') }} i
  on i.delivery_id = d.delivery_id
where i.delivery_id is null

Der zweite Stolperstein betrifft unique auf historisierten Rohtabellen. Liegen dort mehrere Ladestände je Beleg, schlägt der Test zwangsläufig fehl. Er gehört in diesem Fall auf das bereinigte Modell, oder der Ladezeitstempel wird Teil der Schlüsselkombination. Für zusammengesetzte Schlüssel greift man dabei zu dbt_utils:

YAML
models:
  - name: delivery_items
    data_tests:
      - dbt_utils.unique_combination_of_columns:
          combination_of_columns:
            - delivery_id
            - item_number

dbt_expectations für alles, was die Standard-Tests nicht abdecken

Die Standard-Tests prüfen Schlüssel, Referenzen und Wertelisten. Was sie nicht abdecken, sind Wertebereiche, String-Längen, Datentypen, Mittelwerte, Zeilenzahlen und Schema-Prüfungen. Genau das bringt dbt_expectations mit, orientiert an Great Expectations, und zwar rein per YAML ohne eigenen SQL-Code.

YAML
models:
  - name: deliveries
    columns:
      - name: warehouse_code        # immer dreistellig
        data_tests:
          - dbt_expectations.expect_column_value_lengths_to_equal:
              value: 3
          - dbt_expectations.expect_column_values_to_be_of_type:
              column_type: string
      - name: gross_weight_kg       # üblicherweise um 100 kg
        data_tests:
          - dbt_expectations.expect_column_mean_to_be_between:
              min_value: 50
              max_value: 200
              config:
                severity: warn

Der Mittelwert-Test ist unspektakulär und trotzdem einer der nützlichsten überhaupt. Er erkennt das Szenario, in dem das durchschnittliche Gewicht jahrelang bei etwa 100 Kilogramm liegt und nach einer Quelländerung plötzlich bei 10.000 steht, weil jemand Gramm statt Kilogramm liefert. Kein Schlüssel ist verletzt, kein Pflichtfeld leer, technisch ist alles in Ordnung, und die Auswertung ist trotzdem falsch.

Zwei Details, die für dbt auf Azure Databricks wichtig sind. Erstens: bei Typprüfungen heißt der Typ string, nicht varchar. Beispiele aus der Snowflake- oder Postgres-Welt lassen sich also nicht eins zu eins kopieren. Wer eine Spalte bewusst als VARCHAR(n) angelegt hat, bekommt vom Test trotzdem string zurück, weil VARCHAR in Databricks nur im Tabellenschema existiert und darunter als String mit Längenprüfung abgelegt wird. Zweitens bringt dbt_expectations das Package dbt-date mit und setzt eine definierte Zeitzone voraus. Ohne diesen Eintrag scheitern die Läufe:

YAML
# dbt_project.yml
vars:
  'dbt_date:time_zone': 'Europe/Berlin'

Für Dateiimporte lohnt sich außerdem die Schema-Prüfung auf der Zieltabelle. Sie beantwortet die drei Fragen, die sich bei jedem Import stellen, in einem Test: Sind alle Attribute mitgeliefert, sind neue dazugekommen, sind alle eindeutig benannt?

YAML
models:
  - name: stg_supplier_import
    data_tests:
      - dbt_expectations.expect_table_columns_to_match_ordered_list:
          ordered_column_list: ["delivery_id", "quantity", "weight", "load_date"]
          config:
            severity: error

Ändert die Quelle ihr Schema, fällt es hier auf, bevor nachgelagerte Modelle brechen.

Wenn Standard und Package nicht reichen

Für wiederkehrende fachliche Regeln schreibt man einen eigenen Custom Generic Test. Ein Beispiel, nach dem in Projekten regelmäßig gefragt wird, ist der Vergleich des aktuellen Datenvolumens mit dem historischen Mittelwert. Als parametrisierbarer Test wird daraus ein Baustein, der auf jedem Modell einsetzbar ist:

SQL
-- tests/generic/volume_vs_average.sql
{% test volume_vs_average(model, date_column,
    datepart='month', min_pct=50, max_pct=200) %}

with per_period as (
    select
        date_trunc('{{ datepart }}', {{ date_column }}) as period,
        count(*) as cnt
    from {{ model }}
    group by 1
),
avg_cnt as (
    select avg(cnt) as avg_cnt from per_period
)
select
    p.period,
    p.cnt,
    round(100 * p.cnt / a.avg_cnt, 1) as pct_of_avg
from per_period p
cross join avg_cnt a
where p.period = date_trunc('{{ datepart }}', current_date)
  and (100 * p.cnt / a.avg_cnt < {{ min_pct }}
       or 100 * p.cnt / a.avg_cnt > {{ max_pct }})

{% endtest %}

Konfiguriert wird er wie jeder andere Generic Test:

YAML
models:
  - name: deliveries
    data_tests:
      - volume_vs_average:
          date_column: load_datetime
          datepart: month
          min_pct: 50
          max_pct: 200
          config:
            severity: warn
            store_failures: true

Ein Hinweis zum Tagesvergleich: Wer datepart auf day stellt, bekommt an Wochenenden und Feiertagen Fehlalarme, weil dort planmäßig keine Daten ankommen. Sauber löst man das über einen Join auf einen Betriebskalender, der sowohl den aktuellen Tag als auch die Mittelwert-Basis auf Arbeitstage filtert. Ohne Kalendertabelle dämpft schon der Vergleich gleicher Wochentage die Schwankung deutlich.

Unit-Tests: Logik statt Daten

Unit-Tests gibt es seit dbt 1.8. Sie validieren die Modelllogik anhand statischer Eingabedaten, bevor das Modell materialisiert wird. Sinnvoll sind sie überall dort, wo die Logik selbst das Risiko ist: Fensterfunktionen, Datumsarithmetik, Regex, Deduplizierung, case when mit vielen Zweigen. Für ein min() braucht niemand einen Unit-Test, das testet die Engine besser als wir.

YAML
unit_tests:
  - name: test_delivery_is_late
    description: "Verspätung nur, wenn tatsächliche Lieferung nach dem Zieldatum liegt"
    model: fct_deliveries
    given:
      - input: ref('stg_deliveries')
        format: dict
        rows:
          - {delivery_id: 1, promised_date: "2026-03-10", actual_date: "2026-03-12"}
          - {delivery_id: 2, promised_date: "2026-03-10", actual_date: "2026-03-10"}
          - {delivery_id: 3, promised_date: "2026-03-10", actual_date: null}
    expect:
      format: dict
      rows:
        - {delivery_id: 1, is_late: true}
        - {delivery_id: 2, is_late: false}
        - {delivery_id: 3, is_late: false}

Bei den Grenzen lohnt sich Ehrlichkeit, denn sie sind spürbar. Unit-Tests funktionieren ausschließlich mit SQL-Modellen, Python-Modelle sind nicht unterstützt. Ebenfalls nicht unterstützt sind Materialized Views, rekursives SQL, introspektive Queries und Modelle aus anderen Projekten oder Packages. Wer in seinem dbt-Projekt mit Streaming Tables oder Materialized Views arbeitet, kann diese Modelle also nicht unit-testen, und Python-Modelle testet man mit pytest außerhalb von dbt, was zwei Test-Frameworks im selben Projekt bedeutet. Adapterspezifische Sondereinschränkungen sind übrigens nur für BigQuery und Redshift dokumentiert, für Databricks gibt es keine.

Zwei praktische Punkte kommen dazu. Die direkten Eltern-Modelle müssen vor dem Unit-Test in der Datenbank existieren. Damit dafür kein unnötiges Compute anfällt, baut man sie leer:

Terminal
dbt run --select "stg_deliveries" --empty

Und inkrementelle Modelle sind testbar, brauchen aber ein Override. Erwartet wird dabei das Ergebnis der Materialisierung, also die Zeilen, die gemerged oder eingefügt werden, nicht der Endzustand der Tabelle:

YAML
unit_tests:
  - name: test_incremental_load
    model: fct_deliveries
    overrides:
      macros:
        is_incremental: true
    given:
      - input: ref('stg_deliveries')
        rows:
          - {delivery_id: 1, load_datetime: "2026-03-01"}
          - {delivery_id: 2, load_datetime: "2026-03-02"}
      - input: this
        rows:
          - {delivery_id: 1, load_datetime: "2026-03-01"}
    expect:
      rows:
        - {delivery_id: 2, load_datetime: "2026-03-02"}

Unit-Tests gehören in Entwicklung und CI, nicht in die Produktion. Die Inputs sind statisch, ein Produktionslauf verbrennt also nur Compute. dbt Labs empfiehlt das ausdrücklich, und technisch lässt es sich über den Ressourcentyp steuern:

Terminal
dbt test --select "test_type:unit"            # nur Unit-Tests, etwa in der CI
dbt build --exclude-resource-type unit_test   # Produktionslauf ohne Unit-Tests

Praktisch an dbt build: der Befehl führt in Lineage-Reihenfolge zuerst die Unit-Tests aus, materialisiert dann das Modell und lässt danach die Data Tests laufen. Genau die Reihenfolge, die man haben will.

Was beim Einsatz von dbt auf Azure Databricks zu beachten ist

Der Adapter dbt-databricks ist cloudneutral, die bisherigen Beispiele gelten also genauso außerhalb von Azure. Der Azure-Bezug liegt in der Umgebung: in Unity Catalog, in den Rechten und in den Kosten der Ausführung. Genau dort liegen auch die Punkte, die in Projekten regelmäßig aufhalten.

Fehlerhafte Zeilen sichtbar machen. Standardmäßig sind Testergebnisse flüchtig: pass oder fail, die betroffenen Zeilen sind danach nicht mehr einsehbar. Für das Debugging materialisiert store_failures sie als Tabelle:

YAML
# dbt_project.yml
data_tests:
  +store_failures: true

dbt legt die Ergebnisse in einem eigenen Schema mit dem Namensmuster <schema>_dbt_test__audit ab und verwaltet die Tabellen selbst, bei jedem Lauf werden sie überschrieben. Genau hier wartet der häufigste Stolperstein in Unity Catalog: Der Lauf scheitert mit einer Rechte-Meldung auf dieses Schema, weil das Service Principal zwar Owner des eigenen Zielschemas ist, aber kein CREATE SCHEMA auf dem Catalog hat. Das Recht muss also einmalig gesetzt werden, sonst funktioniert die Option nicht.

Kosten im Blick behalten. Jeder Test ist eine eigene Query. Bei zwanzig Tests fällt das nicht auf, bei mehreren Hundert auf einem SQL Warehouse Serverless schon. Zwei Hebel helfen: dbt build statt getrennter Läufe, damit Modelle und Tests eine Session teilen, und store_failures nur dort aktivieren, wo die Zeilen tatsächlich gebraucht werden, statt global.

Abgrenzung zu den Plattform-Mitteln. Neben dbt-Tests gibt es auf Azure Databricks Expectations in Lakeflow Spark Declarative Pipelines und Constraints auf Delta-Tabellen. Der Unterschied ist die Wirkung: Expectations und Constraints greifen beim Schreiben und können fehlerhafte Zeilen verwerfen oder den Lauf abbrechen, dbt-Tests prüfen danach und melden. Für Transformationsstrecken, die in dbt laufen, gehören die Prüfungen zu den Modellen, also in dbt. Wer Daten über SDP ingestiert, setzt die harten Regeln sinnvollerweise dort und testet in dbt die fachliche Logik weiter oben. Wann welches der beiden Werkzeuge die Transformationsstrecke tragen sollte, haben wir hier gegenübergestellt.

Severity: error oder warn

Die letzte Entscheidung ist die, die den Unterschied zwischen einem nützlichen und einem ignorierten Testsetup macht.

error gehört auf alles, was nachgelagerte Joins verfälscht: Schlüssel, Referenzen, Pflichtfelder, unerwartete Spaltenlisten. Hier soll die Pipeline stehen bleiben.

warn gehört auf statistische Prüfungen wie Mittelwerte, Zeilenzahlen und Volumenvergleiche. Ein verschobener Mittelwert ist ein Hinweis, kein Beweis für einen Fehler, und ein Fehlalarm um drei Uhr nachts kostet Vertrauen. Setzen Sie die Schwellen zu Beginn bewusst weit und schärfen Sie sie nach zwei bis drei Wochen Beobachtung nach. Ein Testsetup, das täglich grundlos anschlägt, wird nach zwei Wochen weggeklickt, und dann ist es wirkungslos.

Wenn irgendwann viele Tests regelmäßig laufen und niemand mehr jedes Log liest, ist elementary der nächste sinnvolle Schritt. Das Package speichert Testergebnisse dauerhaft, erkennt Anomalien im Verlauf und meldet nach Teams oder Slack. Solange nur wenige Tests laufen, reicht die Konsolenausgabe.

Fazit

Für den Einstieg in dbt-Tests auf Azure Databricks braucht es keine große Test-Architektur: die vier Standard-Tests als Basis auf jedem Modell, dbt_utils für zusammengesetzte Schlüssel, dbt_expectations für Wertebereiche, Typen und Schema-Prüfungen, einzelne Custom Generic Tests für wiederkehrende fachliche Regeln und Unit-Tests dort, wo die Logik selbst komplex ist. Das deckt den größten Teil dessen ab, wofür sonst ein separates Data-Quality-Tool angeschafft wird, und es bleibt vollständig im Repository und in der Plattform. Dass dbt auf Azure Databricks damit häufig auch ein separates ETL-Tool überflüssig macht, haben wir hier beschrieben.

Falls Sie gerade vor genau so einer Implementierung stehen, kommen Sie gerne auf uns zu. Wir unterstützen Sie beim Aufbau moderner Data- und AI-Plattformen auf Azure Databricks.

Erstgespräch vereinbaren

← Zurück zum Blog