dbt macro のロジックをユニットテストしたい
はじめに
generate_schema_name
をはじめ、さまざまな dbt マクロを利用している方は多いと思いますが、「Web 開発と同様にロジックをユニットテストすることで安定性を高められないか?」と考え、実際にユニットテストを導入してみました。その実装内容を本記事で共有します。
Web 開発における CI(継続的インテグレーション)では、主に以下のようなことを自動化するのが一般的です。
- コードの Lint・フォーマットチェック
- 静的解析・型チェック
- ユニットテストの実行 ← 今回の対象
dbt-unittest について
dbt-unittest
は、dbt マクロ向けのユニットテストを簡単に書けるようにするためのパッケージです。以下のようなアサーション用マクロが提供されています。
📚 ドキュメント
使用例
assert_true
: True であることをテストします。
{{ dbt_unittest.assert_true(true) }}
{{ dbt_unittest.assert_true(1 == 1) }}
assert_equals
: 2つの値が等しいことを検証します。
{{ dbt_unittest.assert_equals("foo", "bar") }}
導入事例
dbt-unittestという便利なライブラリがあるので、こちらを使ってマクロをユニットテストできるようにしています。全てのマクロにテスト追加させるようなルールはなく、適宜追加するスタイルでやっています。仮にテストしないと不安になるような場合、マクロが複雑すぎる可能性があるのでシンプルな方向に落とすように促しています。
実装について
インストール手順
インストール方法は、以下の dbt hub ページをご確認ください。
テストコードの書き方
generate_name/generate_schema_name.sql
)
マクロ本体(例として generate_schema_name
マクロのテストを書いてみます。
サンプル
{% 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
を引数として受け取れるようリファクタリングします(機能はそのままです)
{% 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" |
{% 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
にまとめて確認したいテストを追記していきます
{% 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_equals
やassert_in
などの便利な検証ができる。
マクロを共通化していくプロジェクトでは、このようなテスト戦略を早めに取り入れておくと、将来的な変更に強くなります。
おわりに
これまで作成した dbt マクロの管理が煩雑だったり、検証不足によるバグが不安だったりした部分を、ユニットテストの導入によって改善できました。今後も継続的に運用していきたいと思います。
参考
Discussion