データベース

dbt testの書き方:generic・singular・unit testの使い分けとCIへの組み込み【1.12対応】

dbt testの書き方:generic・singular・unit testの使い分けとCIへの組み込み【1.12対応】

dbt testは、モデルを作ったあとのデータを検査するデータテストと、作る前の変換ロジックを固定データで確かめるユニットテストを、1つのコマンドでまとめて実行します。この記事では、2026年11月時点の公式ドキュメントをもとに、YAMLで書くgeneric test、SQLで書くsingular test、given・expectで書くunit testの書き方を、コピーして動かせる形で整理しました。severityとwhereの設定、失敗行の保存、プルリクエストで変更分だけを検査するCIの組み方まで扱います。

まとめ:dbt testはデータとロジックを分けて書き、CIで変更分だけを回す

主キーと結合キーにはuniqueとnot_null、参照先がある列にはrelationshipsをYAMLで付けます。ここまではどのプロジェクトでも書いておく最低線で、1列あたり数行で済みます。

業務ルールの検査は、SQLで失敗行を返すsingular testに書きます。同じ形の検査が3か所以上に出てきたら、{% test %}ブロックのgeneric testへ切り出す、という順番が無理のない進め方です。

CASE式や日付計算のように間違えやすい変換は、unit testで入力と期待値を固定します。unit testは開発とCIでだけ回し、本番のジョブからは除外してください。

プルリクエストではstate:modified+で変更したモデルと下流だけをテストし、警告で済む検査はseverity: warnに落とします。止めるべき失敗と眺めるだけの警告を分けておくと、テストが鳴りっぱなしで誰も見なくなる事態を防げます。

dbt testの4種類:generic・singular・unitとパッケージ

dbt testで書けるテストは、generic・singular・unit・パッケージ提供の4種類に分かれます。

書き方に入る前に、それぞれが何を検査するのかを押さえておきます。手順やインストールがまだの場合は、先にdbt Coreの使い方でdbt buildが通る状態を作ってください。

データテストはモデル作成後、ユニットテストは作成前にロジックを検査する

公式のデータテストの解説によれば、データテストはモデルやソースに対してselect文を実行し、失敗した行を返すものです。返った行が0件なら合格になります。YAMLで宣言するgeneric testと、SQLファイルで書くsingular testの2つがこれに当たります。

一方のユニットテストは1.8以降で使える機能で、静的な入力行をモデルのSQLに流し込み、出力が期待どおりかを比べます。本番テーブルを作る前に変換ロジックの誤りを見つけられる点が、データテストとの違いです。

種類 書く場所 検査の対象
generic test models配下のYAML 作成済みの列の値
singular test tests配下のSQL モデルをまたぐ業務ルール
unit test models配下のYAML モデルのSQLの変換ロジック
dbt_utilsのテスト models配下のYAML 式・複合キー・値の範囲

test_typeセレクタで種類ごとに実行範囲を絞るコマンド

dbt testコマンドは、何も指定しなければデータテストとユニットテストを両方実行します。種類ごとに分けたいときはtest_typeセレクタを使います。

dbt test                                          # データテストとユニットテストを両方
dbt test --select "test_type:data"                # データテストだけ
dbt test --select "test_type:unit"                # ユニットテストだけ
dbt test --select "test_type:singular"            # SQLで書いたテストだけ
dbt test --select "test_type:generic"             # YAMLで宣言したテストだけ
dbt test --select "fct_orders,test_type:data"     # 1モデルのデータテストだけ

test_typeセレクタはv1とv2の両方で動く書き方として案内されています。v1系だけで使える--resource-type testより、この形で手順書を書いておく方が移行時の書き換えが要りません。なおdbt testはテストだけを流すコマンドで、対象のモデルは先に作られている必要があります。

generic testの書き方:組み込み4種とarguments・configの指定

generic testは、YAMLに数行足すだけで列の値を検査できる、最初に書くべきテストです。

組み込み4種のgeneric testを列ごとに指定するYAMLの書き方

組み込みのgeneric testはunique・not_null・accepted_values・relationshipsの4つです。テストに渡す値をarguments:の下に置く書き方は1.10.5以降で使え、v2では必須になりました。新しく書くなら最初からこの形にします。

# models/marts/schema.yml
version: 2
models:
  - name: fct_orders
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null
      - name: status
        data_tests:
          - accepted_values:
              arguments:
                values: ['placed', 'shipped', 'completed', 'returned']
      - name: customer_id
        data_tests:
          - relationships:
              arguments:
                to: ref('dim_customers')
                field: customer_id

キー名はdata_tests:が正式で、tests:は互換のために残された別名です。1つのモデルに両方を書くとエラーになります。arguments:にはテスト本体へ渡す値、config:には後述のseverityやwhereのような動作の設定を書く、という分担です。

severity・error_if・warn_ifで警告と失敗のしきい値を分ける

severityの設定は既定がerrorで、error_ifとwarn_ifはどちらも既定で!=0、つまり1行でも失敗すればエラーです。外部から取り込むデータのように多少の欠損を許容する列では、件数でしきい値を分けます。

      - name: email
        data_tests:
          - not_null:
              config:
                severity: error
                error_if: ">1000"
                warn_if: ">10"

この例では、NULLが10件以下なら合格、11〜1000件なら警告、1000件を超えたらエラーになります。severity: errorのときは先にerror_ifを評価し、満たさなければwarn_ifを見る、という順番です。severity: warnにするとerror_ifは評価されず、--warn-errorを付けない限りジョブは止まりません。

whereで直近のデータだけを検査し、テストの実行時間を抑える

何億行もあるファクトテーブルに毎回uniqueを掛けると、テストだけで数十分かかることがあります。whereの設定を使うと、検査する行を絞り込めます。

      - name: order_id
        data_tests:
          - unique:
              config:
                where: "order_date >= current_date - 7"

dbtはテストのSQL内の{{ model }}を(select * from fct_orders where ...) dbt_subqueryという副問い合わせに置き換えて実行します。注意点として、whereはsingular testには効かず無視されます。SQLで書くテストでは、絞り込みの条件を自分でSQLに書いてください。

singular testとカスタムgeneric testをSQLで書く手順

組み込みの4種で表せない業務ルールは、SQLで書くテストに移します。

tests直下に書くSQLは失敗行を返し、結果が0行なら合格になる

singular testは、tests/フォルダにselect文を1つだけ書いた.sqlファイルです。ファイル名がそのままテスト名になり、dbt testで自動的に実行されます。返金を含めても注文ごとの支払合計がマイナスにならない、というルールを検査する例です。スナップショットの現行行がキーごとに1件だけかを検査する例はdbt snapshotの使い方:timestamp・check戦略でSCD Type2の履歴を作る手順で紹介しています。

-- tests/assert_order_payment_total_is_not_negative.sql
select
    order_id,
    sum(amount) as total_amount
from {{ ref('fct_payments') }}
group by order_id
having sum(amount) < 0

書くときの決まりは3つあります。1つ目は、正しいデータなら0行になるように、失敗する行を返すSQLにすることです。2つ目は、末尾にセミコロンを付けないことで、付けるとテストが誤って失敗する場合があります。3つ目は、このファイルをモデルのYAMLのdata_tests:から参照しないことです。参照するとエラーになるためです。説明文を付けたい場合はtests/schema.ymlにdata_tests:キーで書きます。

test ブロックで再利用できるgeneric testを自作する

同じ形のsingular testが増えてきたら、カスタムgeneric testへ切り出します。tests/generic/かmacros/にSQLファイルを置き、{% test %}ブロックで定義します。金額列が0以上かを検査する、引数付きのテストの例です。

-- tests/generic/test_is_not_negative.sql
{% test is_not_negative(model, column_name, allow_null=false) %}
select *
from {{ model }}
where {{ column_name }} < 0
{% if not allow_null %}
   or {{ column_name }} is null
{% endif %}
{% endtest %}

modelとcolumn_nameはdbtが自動で渡すので、YAMLに書くのは追加の引数だけです。

      - name: amount
        data_tests:
          - is_not_negative:
              arguments:
                allow_null: true

テストの既定の動作は、{% test %}ブロックの中で{{ config(severity = 'warn') }}のように指定できます。利用側のYAMLのconfig:で上書きできるため、普段は警告にしておき、重要な列でだけエラーにする、といった使い分けが可能です。

dbt_utils 1.4.1のexpression_is_trueと複合キーの一意性

自作する前に、dbt-utilsに同じテストが無いかを確かめます。expression_is_true・unique_combination_of_columns・accepted_range・recencyなど16種のgeneric testが入っており、dbt Hubで確認した2026年11月時点の最新版は1.4.1、対応するdbtの範囲は1.3.0以上3.0.0未満です。

# packages.yml(追記後に dbt deps を実行する)
packages:
  - package: dbt-labs/dbt_utils
    version: 1.4.1

# models/marts/schema.yml
models:
  - name: fct_order_items
    data_tests:
      - dbt_utils.unique_combination_of_columns:
          arguments:
            combination_of_columns: ['order_id', 'line_no']
      - dbt_utils.expression_is_true:
          arguments:
            expression: "quantity * unit_price = line_amount"

明細テーブルのように主キーが複数列になるモデルでは、組み込みのuniqueは使えません。unique_combination_of_columnsをモデル単位に付けるのが定番の書き方です。

unit testの書き方:given・expectで変換ロジックを固定データで検証

unit testは、モデルのSQLに固定の入力行を与えて、出力が期待どおりかを確かめるテストです。

unit_testsのYAMLをmodels配下に置き、refをすべてinputで渡す

注文金額から会員ランクを判定するdim_customersのCASE式を検査する例です。境界値の9,999円と10,000円を入れておくと、不等号の向きの誤りをすぐに見つけられます。

# models/marts/unit_tests.yml
unit_tests:
  - name: test_customer_rank_boundary
    description: "累計購入額1万円以上をsilver、5万円以上をgoldにする境界を検査する"
    model: dim_customers
    given:
      - input: ref('stg_orders')
        format: dict
        rows:
          - {customer_id: 1, amount: 9999}
          - {customer_id: 2, amount: 10000}
          - {customer_id: 3, amount: 50000}
      - input: ref('stg_customers')
        format: dict
        rows:
          - {customer_id: 1}
          - {customer_id: 2}
          - {customer_id: 3}
    expect:
      format: dict
      rows:
        - {customer_id: 1, customer_rank: 'bronze'}
        - {customer_id: 2, customer_rank: 'silver'}
        - {customer_id: 3, customer_rank: 'gold'}

formatはdict・csv・sqlから選べ、dictとcsvでは検査に関係する列だけを書けば足ります。YAMLはtests/ではなくmodels/の配下に置いてください。tests/はデータテスト専用です。

つまずきやすいのは、モデルが参照するrefとsourceをすべてinputに並べる点です。1つでも抜けるとコンパイル時に「node not found」のエラーになります。SQLモデルだけが対象で、materialized view・再帰SQL・イントロスペクティブなクエリを使うモデルには書けません。

本番ジョブではunit testを除外:exclude-resource-typeの指定

unit testの入力は固定なので、本番で何度流しても結果は変わらず、DWHの計算資源を使うだけです。公式ドキュメントも、開発環境とCIでだけ実行するよう強く勧めています。本番ジョブではresource typeの指定で除外します。

# 本番ジョブ(1.12系):unit testを除いてビルドする
dbt build --exclude-resource-type unit_test --target prod

# 環境変数で指定する場合(1.11以降)
export DBT_ENGINE_EXCLUDE_RESOURCE_TYPES=unit_test
dbt build --target prod

CI側では逆にdbt test --select "test_type:unit"を先に流すと、ロジックの誤りをデータテストより前に検出できます。unit testの終了コードは、失敗した行数にかかわらずテストケース単位で0か1です。

失敗の調査とCIへの組み込み:store_failuresとstate:modified+

テストを書いたあとに必要になるのは、落ちた理由を調べる手段と、毎回流す仕組みです。

store_failuresで失敗行をdbt_test__auditスキーマに残す

テストが落ちたとき、ログに出るのは失敗件数だけです。store_failuresを有効にすると、失敗した行がテストと同じ名前のテーブルに保存されます。保存先は既定で<接続先のスキーマ名>_dbt_test__auditというスキーマです。

# dbt_project.yml:プロジェクト全体で失敗行を残す
data_tests:
  +store_failures: true

# 一度だけ残したいときはフラグで指定する
dbt test --select fct_orders --store-failures

保存されたテーブルは、同じテストを次に実行したときに置き換わります。失敗が0件になった場合も置き換わるので、履歴として残したいなら別のテーブルへ写す処理を自分で用意します。開発者ごとのスキーマではdbt_test__auditスキーマの作成権限が無く、「permission denied for schema」で落ちることがあるので、権限の付与も合わせて確認してください。

GitHub Actionsで変更したモデルのテストだけを回す設定

stateメソッドを使うと、本番で最後に作ったmanifest.jsonと比べて、変更のあったモデルとその下流だけを選べます。プルリクエストごとに全モデルをテストする必要はありません。

# .github/workflows/dbt-test.yml
name: dbt-test
on: pull_request
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install "dbt-core~=1.12.0" "dbt-duckdb~=1.11.0"
      - run: dbt deps
      # 本番ジョブが保存した manifest.json を prod-artifacts/ に取得しておく
      - run: dbt test --select "state:modified+,test_type:unit" --state prod-artifacts
      - run: dbt build --select "state:modified+" --exclude "test_type:unit" --state prod-artifacts --target ci

1段目でunit testだけを流し、2段目で変更分のモデルを作りながらデータテストを掛けます。--stateに渡すフォルダはtarget/と分けてください。dbtが解析時にmanifest.jsonを上書きし、比較が成り立たなくなります。本番の成果物を保管するジョブの組み方はデータパイプラインの設計判断で扱っています。

テストをどこまで書くかの判断:dbt testで足りる範囲と他の道具

テストは増やすほど安心に見えますが、実行時間と鳴りっぱなしの警告が増えると、誰も結果を見なくなります。

テストの対象は主キー・結合キー・計算の境界値に絞り、全列には書かない

必ず書くのは、主キーのuniqueとnot_null、結合に使う外部キーのrelationships、金額や日付の計算を含むモデルのunit testです。これらが崩れると、下流の集計が気づかれないまま倍になったり欠けたりします。

見送ってよいのは、表示用の名称列や自由記述の列にnot_nullを一律に付けることです。業務上NULLがありうる列に付けると警告が常態化し、本当に止めるべき失敗が埋もれます。stagingとmartのどちらに付けるかは、生データの不備はstagingで、集計ロジックの誤りはmartで捕まえる、と層で分けるのが目安です。層の分け方はdbtのmarts層と3層の分け方を参照してください。

行単位でテストを書く範囲と、分布の受け入れ検査をdbtの外に出す場面

dbt testで足りるのは、行単位・件数単位で判定できる検査です。列の型や必須項目を契約として固定し、上流の変更で壊れたらビルドを止めたい場合は、データ契約とdbtの実装で扱うモデル契約の方が向いています。値の分布や外れ値の比率まで見たい場合は、Great Expectationsの実装手順のような専用の道具を検討します。

dbtを入れたもののテストの設計が手つかずで、どの列に何を書くかを決めきれない段階であれば、取り込みからBIまでを通した品質設計として整理する方が早く進みます。一創のデータ分析基盤構築・MLOps構築支援では、dbtのモデル設計とテスト方針を含めたご相談を受けています。

よくある質問

dbt testを書き始めるときによく聞かれる質問に答えます。

dbt testとdbt buildはどちらを使えばよいですか?

本番のジョブにはdbt buildを使ってください。モデルとテストをDAGの順に処理し、テストが落ちたモデルの下流を作らないため、誤った集計が公開される前に止まります。dbt testが向くのは、モデルを作り直さずにテストだけを流したい開発中の場面や、CIでunit testだけを先に確かめる場面です。

tests:とdata_tests:はどちらで書けばよいですか?

新しく書くならdata_tests:です。ユニットテストと区別するためにキー名が変わり、tests:は互換のための別名として残っています。1つのモデルに両方を書くとエラーになるので、既存のプロジェクトはファイル単位でまとめて置き換えてください。あわせて引数をarguments:の下に移しておくと、v2への移行時にそのまま使えます。

テストが落ちた行を確認するにはどうすればよいですか?

手早く見るなら、dbt test --select <テスト名> --store-failuresで失敗行をdbt_test__auditスキーマに保存し、そのテーブルをクエリします。target/compiled/配下にはテストのSQLが出力されているので、それをDWHで直接実行して確かめる方法もあります。

dbt testの実行が遅いときはどうすればよいですか?

大きなファクトテーブルのテストにwhereを付けて直近の期間だけを検査し、CIではstate:modified+で変更分に絞ります。unit testを本番から除外するだけでも、本番ジョブの実行時間を短くすることが可能です。v2ではバッチテストを有効にすると、同じモデルのuniqueとnot_nullを種類ごとに1本のクエリへまとめられます。v2そのものの違いはdbt Fusionとはで解説しました。

テストの内容をドキュメントとして共有できますか?

ドキュメントとして共有可能です。YAMLに書いたテストは、dbt docs generateで作るドキュメントのモデルのページに一覧で表示されます。singular testもtests/schema.ymlに説明を書いておけば表示の対象です。社内への公開手順はdbt docsの使い方にまとめています。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.09 テックブログ エディオン不正アクセス第2報:カード番号の対象確認とログ・決済画面の改ざん対策
  2. 2026.11.09 テックブログ LANSCOPE不正アクセス第2報:製品への影響と偽連絡の確認、停止中のライセンス認証の窓口
  3. 2026.11.10 テックブログ Claude Fable 5.2とは:料金・破壊的変更4点とFable 5.1からの移行手順【2026年11月】
  4. 2026.11.10 テックブログ ニトリの情報漏洩:最大3,762万件の対象確認となりすまし連絡の見分け方
  5. 2025.04.30 テックブログ IMSIキャッチャー(偽基地局)とは?2Gフォールバックの手口と日本での対策

RELATED POSTS 関連記事

目次