dbtを導入した直後は、テストを書こうと意気込みます。`unique`と`not_null`を一通り並べて満足する、というところまでは多くのチームが進みます。けれども半年後、ダッシュボードの数字が合わないクレームを受けて初めて「いやテストは通ってたんだけど」と気づきます。テストは通っていたのに、本当に守りたいビジネスルールは検証されていなかった、というパターンです。
効くdbtテストは、generic(汎用)・singular(個別)・パッケージ(dbt-expectations等)を使い分けて組みます。それぞれの役割と、現場で効く書き方を整理します。
dbtテストの3層
| 種類 | 書き方 | 得意領域 |
|---|---|---|
| generic tests(汎用) | schema.ymlに宣言的に書く | unique・not_null・accepted_values・relationships |
| singular tests(個別) | SQLファイルとして書く | そのモデル固有のビジネスルール |
| パッケージ(dbt-expectations等) | マクロを呼び出す | 統計的な検証、複雑な制約 |
3層は補完関係にあります。「genericで基本骨格、singularで業務ルール、パッケージで深い検証」と分担すると、テストが厚くなりすぎず、欲しい安心も得られます。
generic tests:宣言的に書く基本
generic testsは、schema.ymlにモデルとカラムの制約を宣言的に書きます。dbt標準で4つ用意されています。
# models/silver/schema.yml
version: 2
models:
- name: fct_orders
columns:
- name: order_id
tests:
- unique
- not_null
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: order_status
tests:
- accepted_values:
values: ['pending', 'paid', 'shipped', 'cancelled']
これだけで「主キーが一意か」「外部キーが対応するディメンションに存在するか」「列挙値が想定の範囲内か」が毎回チェックされます。最初に書くのはここまでです。
singular tests:業務ルールをSQLで書く
genericテストでは、業務固有のルールは表現できません。「キャンセル済みの注文に売上が計上されていないこと」「商品の単価×数量が注文金額と一致すること」のようなルールは、singular testsで書きます。
-- tests/assert_cancelled_orders_have_zero_revenue.sql
-- このSQLが1行でも返したらテスト失敗
SELECT
order_id,
order_status,
revenue
FROM {{ ref('fct_orders') }}
WHERE order_status = 'cancelled'
AND revenue > 0
singular testsは「失敗するべきパターンを検出するSELECT文」を書く、というシンプルな約束です。テストフォルダに`.sql`ファイルとして置けば、`dbt test`で自動的に実行されます。業務ルールの数だけ書いていきます。
singularは強力ですが、増えすぎるとメンテが重くなります。「3回以上同じパターンが出てきたらgeneric化を検討する」のが現場のリズムです。
独自generic testを作る
同じsingularテストを複数モデルで書きたくなったら、独自のgeneric testに切り出します。`tests/generic/`配下にマクロとして定義します。
-- tests/generic/test_positive_value.sql
{% test positive_value(model, column_name) %}
SELECT * FROM {{ model }}
WHERE {{ column_name }} <= 0
{% endtest %}
# schema.yml で使う
columns:
- name: order_total
tests:
- positive_value
これで「正の値であること」を宣言的に書けるようになります。ビジネス用語に近いテスト名(`is_business_day`、`matches_invoice_total`等)を増やしていくと、schema.ymlがそのまま仕様書として機能します。
dbt-expectations:表現力を借りる
毎回独自macroを書くのは大変です。dbt-expectations(Calogica社)は、Great Expectationsの考え方をdbtに移植した、表現力豊富なテストパッケージです。代表的なテストを挙げます。
| テスト | 検査内容 |
|---|---|
| expect_column_values_to_be_between | 値が指定範囲内 |
| expect_column_values_to_match_regex | 正規表現に一致 |
| expect_row_values_to_have_data_for_every_n_datepart | 日付の欠落がない |
| expect_table_row_count_to_be_between | 行数が想定範囲 |
| expect_column_value_lengths_to_be_between | 文字列長の範囲 |
# schema.yml で dbt-expectations を使う
columns:
- name: order_total
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
models:
- name: fct_orders
tests:
- dbt_expectations.expect_table_row_count_to_be_between:
min_value: 100
max_value: 1000000
severity と store_failures:通知と調査を楽にする
テストは失敗しただけでは何が起きたか分かりません。dbtにはテストの深刻度を変えたり、失敗行を保存する仕組みがあります。
| 設定 | 動き |
|---|---|
| severity: error(既定) | 失敗時に処理を止める |
| severity: warn | 失敗を警告にして処理続行 |
| warn_if / error_if | 「N行までwarn、M行超えたらerror」のような閾値制御 |
| store_failures: true | 失敗行を別テーブルに保存。原因調査が楽 |
columns:
- name: order_total
tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1000000
config:
severity: warn
warn_if: '>10'
error_if: '>100'
store_failures: true
「軽微なずれは警告で済ませ、大きく外れたらエラーで止める」運用が組めます。`store_failures`を有効にすると、失敗した行が`dbt_test__audit`スキーマに保存されるので、調査が圧倒的に楽になります。
どこまでテストを書くか:割り切りの目安
テストは増やせるだけ増やしたくなりますが、実行時間とメンテコストが上がります。最低限のラインは次の通りです。
- 必須:全モデルの主キー(unique + not_null)、ファクト→ディメンションの参照整合性(relationships)
- 強く推奨:列挙値(accepted_values)、金額や数量の符号・範囲
- 業務ルール:「キャンセルなら売上ゼロ」のような業務固有制約をsingularで
- 余裕があれば:行数の想定範囲、日付の欠落検査、文字列のフォーマット
incrementalモデル運用時は、テストが効きやすい設計になります。新しく入ったデータだけを検証すれば済むからです。incremental modelsの設計とテストはセットで考えると整合性が取れます。
まとめ
- generic(汎用)・singular(業務ルール)・パッケージ(dbt-expectations)を使い分ける。
- genericで主キーと参照整合性、singularで業務固有ルール、パッケージで統計的検査と覚える。
- 同じsingularが3回出てきたら独自generic化する。
- severityとstore_failuresで「警告と停止」「原因調査」を運用しやすくする。
- 必須は主キー+参照整合性。あとは痛みが出たところから足す。
関連:dbt incremental modelsの設計、dbt + Snowflakeのコスト最適化、dbt snapshotsでSCD Type 2。テスト設計や運用改善の壁打ちは、DE-STKの初回相談(30分・無料)もご利用ください。
よくある質問(FAQ)
Q. テストが多すぎて遅いです。どう減らせばいいですか?
A. まず、テストごとの実行時間を`dbt test –select test_name`で測ります。遅いテストは大きなテーブルへの全件スキャンが原因のことが多いです。limitを噛ませる、特定パーティションだけ検査する、severityをwarnに落として頻度を減らす、といった調整で実行時間を抑えられます。「全部errorで毎日全件検査」を疑い、優先度の高いテストにフォーカスします。
Q. テストとデータ品質ツール(Great Expectations、Soda等)は両方必要ですか?
A. 役割が違うので、規模が大きくなると両方使います。dbt testsは「変換ロジックが正しく動いているか」を確かめる、コードの隣にあるテストです。Great ExpectationsやSodaは「データ自体の品質」を独立して監視する、データ専用の品質ツールです。小規模ならdbt tests+dbt-expectationsで十分です。複数のデータ基盤やBIまで監視範囲を広げたい場合に、専用ツールを足します。
Q. テストが失敗したら何が起きますか?
A. severityがerrorなら、そのモデル以降のビルドが止まります。CI/CDで実行している場合、ジョブが赤くなって通知が飛びます。severityがwarnなら、警告だけ出して処理は続行します。失敗時の挙動は、ビジネスへの影響度で決めます。「止めると業務が困るが、間違ったデータが出るともっと困る」場合は、errorで止めるのが適切です。逆に「軽微なゆらぎは許容、データ提供を優先」ならwarnが妥当です。
Q. dbt-expectationsは導入すべきですか?
A. ほぼ常に導入する価値があります。`packages.yml`に1行足すだけで使え、独自にmacroを書く手間が大幅に減ります。dbt標準の4テストだけだと「金額が0〜1,000,000円の範囲か」「メールアドレスのフォーマットが正しいか」のような基本的な制約も書きづらく、すぐ手詰まりになります。最初のプロジェクトから導入しておくと、後から書き直す手間がなくなります。