🍊

dbt macro のロジックをユニットテストしたい

に公開

はじめに

generate_schema_name をはじめ、さまざまな dbt マクロを利用している方は多いと思いますが、「Web 開発と同様にロジックをユニットテストすることで安定性を高められないか?」と考え、実際にユニットテストを導入してみました。その実装内容を本記事で共有します。

Web 開発における CI(継続的インテグレーション)では、主に以下のようなことを自動化するのが一般的です。

  1. コードの Lint・フォーマットチェック
  2. 静的解析・型チェック
  3. ユニットテストの実行 ← 今回の対象

dbt-unittest について

dbt-unittest は、dbt マクロ向けのユニットテストを簡単に書けるようにするためのパッケージです。以下のようなアサーション用マクロが提供されています。

📚 ドキュメント

https://github.com/yu-iskw/dbt-unittest?tab=readme-ov-file
https://docs.getdbt.com/blog/unit-testing-dbt-packages

使用例

assert_true: True であることをテストします。

  {{ dbt_unittest.assert_true(true) }}
  {{ dbt_unittest.assert_true(1 == 1) }}

assert_equals: 2つの値が等しいことを検証します。

  {{ dbt_unittest.assert_equals("foo", "bar") }}

導入事例

https://techblog.cartaholdings.co.jp/entry/snowflake-dbt-data-platform-vision

dbt-unittestという便利なライブラリがあるので、こちらを使ってマクロをユニットテストできるようにしています。全てのマクロにテスト追加させるようなルールはなく、適宜追加するスタイルでやっています。仮にテストしないと不安になるような場合、マクロが複雑すぎる可能性があるのでシンプルな方向に落とすように促しています。

実装について

インストール手順

インストール方法は、以下の dbt hub ページをご確認ください。
https://hub.getdbt.com/yu-iskw/dbt_unittest/latest/

テストコードの書き方

マクロ本体(generate_name/generate_schema_name.sql

例として generate_schema_name マクロのテストを書いてみます。
https://docs.getdbt.com/docs/build/custom-schemas
サンプル

{% macro generate_schema_name(custom_schema_name, node) -%}

    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}

        {{ default_schema }}

    {%- else -%}

        {{ default_schema }}_{{ custom_schema_name | trim }}

    {%- endif -%}

{%- endmacro %}

このままだとテストが書きづらいため、test_target を引数として受け取れるようリファクタリングします(機能はそのままです)

dbt_project/macros/generate_name/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node, test_target=none) -%}
    {%- set effective_target = test_target if test_target is not none else target -%}

    {%- if custom_schema_name is none -%} {{ effective_target.schema }}
    {%- else -%} {{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}

テストコード(generate_name/__tests__/test__generate_schema_name.sql

以下の2ケースを検証します。

No. custom_schema_name の値 test_target["schema"] 期待される戻り値
1 "custom_schema" "test_target_schema" "custom_schema"
2 None "test_target_schema" "test_target_schema"
dbt_project/macros/generate_name/__tests__/test__generate_schema_name.sql
{% macro test__generate_schema_name() %}

    {# カスタムスキーマが指定されている場合、それがそのまま使用される #}
    {% set custom_schema_name = "custom_schema" %}
    {% set node = none %}
    {% set test_target = {"schema": "test_target_schema"} %}
    {{
        dbt_unittest.assert_equals(
            dbt_project.generate_schema_name(custom_schema_name, node, test_target),
            "custom_schema",
        )
    }}

    {# カスタムスキーマが指定されていない場合、target.schema を使用する #}
    {% set custom_schema_name = none %}
    {% set node = none %}
    {% set test_target = {"schema": "test_target_schema"} %}
    {{
        dbt_unittest.assert_equals(
            dbt_project.generate_schema_name(custom_schema_name, node, test_target),
            "test_target_schema",
        )
    }}

{% endmacro %}

実行方法

テスト成功時には、特に何も表示しないので必要あればログを自分で仕込んでください。

dbt run-operation test__generate_schema_name

テスト失敗時の出力例

失敗時は下記のように明確にエラー箇所がログに表示されます(テストケースを書き換えてわざとエラー出しています)

Encountered an error while running operation: Compilation Error in macro test__generate_schema_name (macros/generate_name/__tests__/test__generate_schema_name.sql)
  FAILED: test_target_schema does not equal test_target_schema2.
  
  > in macro assert_equals (macros/assert_equals.sql)
  > called by macro test__generate_schema_name (macros/generate_name/__tests__/test__generate_schema_name.sql)
  > called by macro test__generate_schema_name (macros/generate_name/__tests__/test__generate_schema_name.sql)

複数のテストを管理する

JavaScript のテストフレームワーク「Jest」を参考にして、各マクロディレクトリに __tests__ フォルダを作り、テストを分離して管理することにしました(あくまで一例です)

ディレクトリ構成

以下のように、各マクロディレクトリに __tests__ フォルダを設けてテストを分離します

dbt_project/
├─ macros/
│  ├─ __tests__/
│  │ └─ test_macros.sql             # 全体のエントリーポイント
│  ├─ generate_name/
│  │  ├─ __tests__/
│  │  │  ├─ test__generate_alias_name.sql
│  │  │  └─ test__generate_schema_name.sql
│  │  ├─ generate_alias_name.sql
│  │  └─ generate_schema_name.sql

テストマクロのエントリーポイント

すべてのテストをまとめて実行するためのエントリーポイントを作成します
test_macros.sqlにまとめて確認したいテストを追記していきます

dbt_project/macros/__tests__/test_macros.sql
{% macro test_macros() %}
    {% do test__generate_alias_name() %}
    {% do test__generate_schema_name() %}
    {{ log("✅ All macro unit tests passed", info=True) }}
{% endmacro %}

下記は、成功した場合何も表示しないのは寂しいので追記、、、

{{ log("✅ All macro unit tests passed", info=True) }}

全テストを実行

GitHub Actions などで Pull Request 作成時に以下のコマンドを実行することで、自動テストを走らせる運用もおすすめです。

dbt run-operation test_macros

テスト失敗時の出力例

Encountered an error while running operation: Compilation Error in macro test_macros (macros/__tests__/test_macros.sql)
  FAILED: test_target_schema does not equal test_target_schema2.

  > in macro assert_equals (macros/assert_equals.sql)
  > called by macro test__generate_schema_name (macros/generate_name/__tests__/test__generate_schema_name.sql)
  > called by macro test_macros (macros/__tests__/test_macros.sql)

まとめ

  • Jestライクに __tests__ フォルダでマクロごとのテストを管理すると見通しが良くなる。
  • dbt_unittest を使えば、assert_equalsassert_in などの便利な検証ができる。

マクロを共通化していくプロジェクトでは、このようなテスト戦略を早めに取り入れておくと、将来的な変更に強くなります。

おわりに

これまで作成した dbt マクロの管理が煩雑だったり、検証不足によるバグが不安だったりした部分を、ユニットテストの導入によって改善できました。今後も継続的に運用していきたいと思います。

参考

https://zenn.dev/pixiv/articles/8b4fc2b870b8f9
https://github.com/yu-iskw/dbt-unittest?tab=readme-ov-file
https://docs.getdbt.com/blog/unit-testing-dbt-packages
https://techblog.cartaholdings.co.jp/entry/snowflake-dbt-data-platform-vision

Discussion