Blog

Der fehlende Teil der DBT-Softwareentwicklung: Unit-Tests

Cor Zuurmond

Aktualisiert Oktober 16, 2025
8 Minuten

Wir sprechen ziemlich... viel über dbt. Wir haben darüber gesprochen, wie wir dbt für Datentransformationen, für Datentests oder für den Wissensaustausch mit Datendokumenten verwenden. Aber wir haben noch nicht darüber gesprochen, was in dbt fehlt! Und zwar fehlt ein wichtiger Teil der Softwareentwicklung: Unit-Tests.

Einheitstests

Wenn wir Datenumwandlungspipelines ohne dbt schreiben, schreiben wir Unit-Tests, um zu überprüfen, ob unser Code wie erwartet funktioniert. Wie die meisten Programmierer wissen, sind Unit-Tests eine wichtige Best Practice in der Softwareentwicklung. Eine kurze Auffrischung für Sie alle:

Unit-Tests sind (kleine) Testfälle, die in der Regel aus vier Schritten bestehen: (i) Definition der erwarteten Ausgabe, (ii, optional) Definition der Eingabe, (iii) Ausführung des zu testenden Codes und (iv) Überprüfung, ob die Ausgabe mit der erwarteten Ausgabe übereinstimmt. Wenn die Ausgabe nicht mit der erwarteten Ausgabe übereinstimmt, ist Ihr Test fehlgeschlagen! Wenn sie gleich ist, hat Ihr Test bestanden!

Manchmal fühlt es sich mühsam an, Unit-Tests zu schreiben, aber die meisten Programmierer erkennen an, dass Tests auf lange Sicht Zeit sparen. Tests werden immer wichtiger, wenn Ihre Codebasis wächst. Je größer das Projekt ist, desto schwieriger ist es, die Auswirkungen einer Änderung zu erkennen. Unit-Tests überprüfen, ob Ihr Code das tut, was Sie von ihm erwarten.

Darüber hinaus haben Unit-Tests noch weitere Vorteile. So sind Unit-Tests eine Form der Dokumentation und ermöglichen Ihnen ein sicheres Refactoring des Codes.

Produktion ohne Unit-Tests?!

Warten Sie was?! Will ich damit sagen, dass wir dbt in der Produktion ohne Unit-Tests einsetzen?! Ähm ... ja, das tun wir ... aber wir testen unsere Daten!

Dbt macht Datentests einfach. Und Datentests sind der Integrationstest für Ihre Datenanwendung. Sie sind eine Stufe höher in der Testpyramide. Sie sind eine vollständigere, abschließende Prüfung als Unit-Tests. Eine Unit-Test-Suite kann erfolgreich sein, während Sie Datenprobleme haben.

Aber Unit-Tests beschleunigen den Entwicklungsprozess. Gute Unit-Tests sind isoliert, was sie vorhersehbar macht und es somit einfacher macht, Ihre Codeänderungen zu testen und zu überprüfen als bei Datentests.

Aus Erfahrung weiß ich, dass die Überprüfung von PRs mit kompliziertem SQL + Jinja-Anweisungen schwierig ist. Ich habe PRs in einem dbt-Projekt genehmigt, die in der Produktion Probleme verursachten, die mit Unit-Tests hätten abgefangen werden können.

Glücklicherweise müssen wir uns nicht zwischen Unit- und Datentests entscheiden, wir können beides haben!

Einheitstest dbt mit pytest

Ich habe ein Pytest-Plugin für Unit-Tests eines dbt-Projekts erstellt. Die Benutzerdokumentation finden Sie auf read the docs. Sie installieren das Paket von PyPI mit pip:

pip install pytest-dbt-core

Das Projekt fügt eine Fixture macro_generator hinzu, die ein Makro aus Ihrem Projekt abruft und dabei die internen Klassen von dbt verwendet. Dann definieren Sie einen Test für Ihr Makro: (i) Definition der erwarteten Ausgabe, (ii, optional) Definition der Eingabe, (iii) Ausführung des Makros mit der optionalen Eingabe und (iv) Vergleich der Ausgabe mit der erwarteten Ausgabe. Ein Beispiel:

import pytest
from dbt.clients.jinja import MacroGenerator
from pyspark.sql import SparkSession

@pytest.mark.parametrize(
    "macro_generator", ["macro.spark_utils.get_tables"], indirect=True
)
def test_create_table(
    spark_session: SparkSession, macro_generator: MacroGenerator
) -> None:
    expected_table = "default.example"
    spark_session.sql(f"CREATE TABLE {expected_table} (id int) USING parquet")
    tables = macro_generator()
    assert tables == [expected_table]

Das Makro kann z.B. auch Teil einer Abfrage sein:

import pytest
from dbt.clients.jinja import MacroGenerator
from pyspark.sql import SparkSession

@pytest.mark.parametrize(
    "macro_generator",
    ["macro.my_project.to_cents"],
    indirect=True,
)
def test_dollar_to_cents(
    spark_session: SparkSession, macro_generator: MacroGenerator
) -> None:
    expected = spark_session.createDataFrame([{"cents": 1000}])
    to_cents = macro_generator("price")
    out = spark_session.sql(
        "with data AS (SELECT 10 AS price) "
        f"SELECT cast({to_cents} AS bigint) AS cents FROM data"
    )
    assert out.collect() == expected.collect()
Die Beispiele zeigen, wie Sie Tests schreiben, wenn Sie dbt mit Spark verwenden. Das Plugin ist so geschrieben, dass es mit jedem dbt-Adapter funktioniert. Sie benötigen eine Engine, gegen die Sie SQL ausführen können, z.B. eine Datenbank oder eine Warehouse-Sitzung.

Beschränkungen

Es gibt eine grundlegende Einschränkung bei diesem Aufbau: Python. Es widerspricht dem SQL-lastigen Ansatz von dbt. Und wir mögen dbt, weil es SQL-lastig ist! Eine Sprache, die ein breites Publikum anspricht. Warum also führen wir Python für ein Unit Testing Framework ein?

Zunächst würde ich gerne wissen, ob es eine gute Alternative zum Schreiben von Unit-Tests mit einem SQL-Framework gibt! Es gibt bereits einige Alternativen (siehe nächster Abschnitt), mit denen Sie Tests schreiben können, indem Sie (i) die erwarteten Ergebnisse definieren, (ii) Eingabedaten definieren, (iii) ein Modell ausführen und (iv) die Ausgabe mit dem erwarteten Ergebnis vergleichen. Ich empfand jedoch ein SQL-Framework für Tests als einschränkend. Ich möchte die volle Flexibilität haben, die pytest Ihnen bietet. Ein paar Beispiele:

Das erste Beispiel im obigen Abschnitt ist kein Makro, das ein typischer dbt-Benutzer schreibt. Fortgeschrittene Benutzer oder Betreuer von Adaptern könnten ein Makro schreiben, das alle Tabellen aus einem Warehouse abruft. Eine solche Abfrage passt nicht in ein SQL-Testframework, das nur zum Testen von Datentransformationen gedacht ist.

Das zweite Beispiel im obigen Abschnitt testet eine Unterabfrage, die ebenfalls nicht in einen solchen SQL-Testrahmen passt.

Und wenn Ihr dbt-Projekt wächst, möchten Sie Ihre Testbasis unter Kontrolle halten, indem Sie:

Alternativen

Es gibt eine Github-Diskussion über Unit-Tests für SQL in dbt. In der Diskussion finden Sie einen vollständigen Überblick, eine kurze Zusammenfassung:

Dbt-Seeds können verwendet werden, um Eingabe- und erwartete Ausgabedaten für Tests hinzuzufügen. Man war jedoch der Meinung, dass Seeds eine schlechte Entwicklererfahrung bieten, weil sie langsam sind. Daher werden SQL-Mocks bevorzugt.

Ein Framework, wie oben erwähnt, ist für verschiedene Entwickler sinnvoll, indem Sie beispielsweise neben dem Modell, das Sie testen möchten, eine ..._input.sql und ..._expected.sql Datei definieren. Dann führt Ihr Test-Framework (i) die erwartete Datei aus, (ii) dann die Eingabedatei, (iii) dann Ihr Modell und (iv) vergleicht schließlich die erwartete mit der tatsächlichen Ausgabe. Dieser Ansatz wurde durch das dbt-Paket von Equal Experts, mit dem Sie Unit-Tests mit Makros ausführen können, noch einen Schritt weitergeführt: mock_ref, mock_source, expect und test.

Vor kurzem hat dbt als Teil der Version 1.1 eine neue Möglichkeit zum Testen von Adaptern veröffentlicht. Der Titel lautet zwar "Testen eines neuen Adapters", aber das Test-Framework kann für die Definition Ihrer eigenen Tests verwendet werden. Ich finde es sehr gut, dass dbt Unit-Testing-Funktionen mit seinem Kern ausliefert! Das ist ein großer Vorteil der Verwendung dieses Frameworks.

Der Ansatz von pytest-dbt-core ist eine Inspiration für die erste Implementierung der Verwendung von pytest built-ins zum Testen von dbt-Adaptern. Vor diesem neuen Framework werden dbt-Adapter mit der pytest-Erweiterung dbtspec getestet, die Tests von der Kommandozeile aus ausführt - sie ist nicht in den Code integriert.

Dbt Kerntest-Framework

Ich bevorzuge das Testframework pytest-dbt-core, weil ich die Indirektheit des Testframeworks von dbt verwirrend und daher schwer zu verwenden finde. Dies ist ein Meinungsunterschied in Bezug auf die Art und Weise, wie Sie Tests mit pytest schreiben sollten. Um meinen Standpunkt zu begründen:

In dbt core werden die Tests in Klassen statt in Funktionen definiert. Dieser Stil stammt aus der Unit-Test-Bibliothek, die von pytest abgelöst wurde. Innerhalb von pytest ermöglichen Klassen die Gruppierung von Tests und bieten Ihnen Vorteile wie die Organisation von Tests, die gemeinsame Nutzung von Fixtures und die Vergabe von Markierungen für eine Klasse statt für einen Test. Ich verwende nicht gerne Testklassen, weil sie durch (Mehrfach-)Vererbung und eine implizit ausgeführte Setup-/Teardown-Methode ein gewisses Maß an Indirektheit mit sich bringen.

Das dbt core tests framework lässt sich nicht reibungslos in Ihr Projekt integrieren. So müssen Sie beispielsweise Ihr Profil oder Ihre Konfiguration (neu) definieren, anstatt das Profil aus Ihrem Projekt wiederzuverwenden.

Das dbt core plugin lässt sich nicht reibungslos in pytest integrieren. Sie müssen zunächst eine globale Variable erstellen, die pytest_plugins auf die dbt.tests.fixtures.project verweist, bevor Sie Ihre Tests ausführen können.

Das Framework stützt sich auf run_dbt der ein High-Level-Einstiegspunkt für dbt ist, der es schwer macht, nachzuvollziehen, was genau Sie ausführen - und damit testen.

Beachten Sie, dass ich an der Testsuite von dbt mitgewirkt habe. Ich weiß aus Erfahrung, dass ihre Testsuite komplex ist. Ich habe Ratschläge gegeben, wie die Testsuite verbessert werden kann, und zwar auf hoher Ebene, z.B. bei der Verwendung von Testklassen anstelle von Funktionen, und bei kleineren Problemen, z.B. bei einem Dekorator mit Seiteneffekten. Es ist nicht einfach, ein großes Open-Source-Projekt wie dbt zu pflegen und zu ändern. Außerdem ist das Testen zwar wichtig, hat aber normalerweise nicht die höchste Priorität. pytest-dbt-core hat den Vorteil, dass er aus der Test-Suite von dbt gelernt hat und bei Null anfangen kann.

Fazit

Es ist noch zu früh, um zu sagen, ob pytest-dbt-core von der Gemeinschaft als Unit-Testing-Framework für dbt angenommen werden wird. Wie oben dargelegt, denke ich, dass dieses Framework im Vergleich zu den Alternativen einige Vorteile bietet. Allerdings gibt es auch einige klare Nachteile, wie z.B. dass es nicht SQL first ist und nicht zum Kern von dbt gehört.

Ich erwarte, dass Unit-Tests in diesem Jahr ein heißes Thema in der dbt-Community sein werden. Ich behalte dieses Thema genau im Auge. Mal sehen, ob wir uns auf eine Lösung einigen können!

Verfasst von

Cor Zuurmond

Contact

Let’s discuss how we can support your journey.