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円の範囲か」「メールアドレスのフォーマットが正しいか」のような基本的な制約も書きづらく、すぐ手詰まりになります。最初のプロジェクトから導入しておくと、後から書き直す手間がなくなります。

▼ このトピックをポチポチ学ぶ

dbtを深く使いこなす

関連記事を順序立てて読みながら、ステップごとに4択クイズで理解を確認できる学習パスです。登録不要・進捗自動保存。

学習パスを始める →