dbt Unit Tests について調べてみました

2025-03-27

お久しぶりです。株式会社 Belong にて Data Platform チームに所属する Shuhei です。

弊社は dbt-core を用いて BigQuery にてデータ分析基盤を構築しています。

先日、dbt Unit Tests を試す機会があったため、その機能や使い方などをご紹介します。

dbt Unit Tests について

dbt の Unit Tests により、SQL モデルのロジックの静的テストを実施することができます。

Unit Tests は dbt Core v1.8 から利用可能になった機能です。まるでソフトウェア開発の単体テストのようなことが dbt の model に対して実施できるとのことだったので、早速試してみました!

Unit Tests と Data Tests の違い

その前に、Unit Tests と Data Tests の違いについて整理してみましょう。用語として似てはいますが、チェックする観点が少し異なるため、確認してみましょう。

ざっくり下記の表のように整理できるかと思います。

項目Unit TestsData Tests
目的モデルのロジックの正確性を検証するデータの品質や整合性を検証する
対象モデルのクエリロジックや計算式実際のデータセットの値
実行タイミングモデルの開発時やビルド前モデルのビルド後

Unit Tests では、とあるモデルについて、ある入力値に対する期待出力値が得られるかどうかを確認することができます。

例えば、売上(revenue) を計算するモデルを考えます。このモデルでは、pricequantity を掛け合わせて revenue を計算します。

このとき、入力データが price: 100, quantity: 2 のとき、出力データが revenue: 200 であることを確認したいとき、Unit Tests が役立ちます。

一方、Data Tests ではとあるモデルが保持する値について、期待するデータ品質および整合性が保たれているかを確認することができます。

例えば、出力データの revenueNUMERIC 型であることや NOT NULL であることなどを確認したいとき、Data Tests が役立ちます。

Unit Tests のやり方

それでは実際に、Unit Tests を試してみましょう!必要なファイルは大きく 2 つです。

  1. Unit Tests を実行する対象の dbt model の SQL ファイル
  2. Unit Tests を定義した yaml ファイル

再掲となりますが、前述の 売上(revenue) を計算するモデルを例に、Unit Tests を実装してみましょう。

SQL ファイル

当然ですが、Unit Tests 対象のモデルファイルが必要です。下記のように revenue モデルを作成したと考えてみます。

WITH fct_sales AS (
    SELECT
        id,
        price,
        quantity
    FROM {{ ref('fct_sales') }}
)
SELECT
    id,
    price,
    quantity,
    price * quantity AS revenue
FROM fct_sales

Unit Tests を行うために、dbt runrevenue モデルを作成しておきましょう。

dbt run --select revenue

yaml ファイル

続いて、revenue モデルのロジックをテストすべく、yaml ファイルを作成します。

入力データが price: 100, quantity: 2 のとき、出力データが revenue: 200 であることを確認したいとき、下記のように記述できます。

unit_tests:
  - name: test_revenue_equals_price_times_quantity
    description: revenue  price  quantity を掛け合わせた値であることを確認するテスト
    model: revenue
    given:
      - input: ref('fct_sales')
        rows:
          - { price: 100, quantity: 2 }
          # - 必要に応じて入力を追加
    expect:
      rows:
        - { revenue: 200 }
        # - 必要に応じて期待結果を追加

地味にうれしいポイントとして、rows には確認したいロジックに関わるカラムのみ指定することができます。この特徴は多くのカラムを持つモデルに対して非常に有効ですし、Unit Tests 1 つ 1 つの見通しもよくなりますね。

主要なパラメータについて簡単に説明しておくと、下記のように整理できます。

パラメータ名説明
nameテストの名称
descriptionテストの説明。テストの目的や意図を簡潔に記述します。
modelテスト対象のモデル名。テストが適用される dbt モデルを指定します。
givenテスト対象モデルに渡す入力データを定義します。データセットは、 input 配下に複数の要素を設定可能です。
inputモデルに渡す入力として参照される dbt モデルを指定し、受け取るデータを定義します。
rows指定した input モデルに対して実際に使用する行データを記述します。
expectモデルの期待される出力データを定義します。テストが成功するためには、実行結果と一致する必要があります。
format行データのフォーマットを指定します。csv や sql を選択できます。

以上のファイルが作成できたら dbt test コマンドで Unit Tests を実行することができます!

# 上述の Unit Tests にのみフィルタして実行する例
dbt test --select test_revenue_equals_price_times_quantity

※ソースのモデル fct_sales も事前に作成しておく必要がありますので注意しましょう。作成していないと下記のようなエラーとなります。

Compilation Error in model fct_sales (models/test_revenue.yaml)
  Not able to get columns for unit test 'fct_sales' from relation `XXX`.`YYY`.`fct_sales` because the relation doesn't exist
  > in macro get_fixture_sql (macros/unit_test_sql/get_fixture_sql.sql)
  > called by model fct_sales (models/test_revenue.yaml)

Unit Tests を使うことで、dbt model のロジックが要件を満たしているか確認しやすくなり、開発時やリファクタ時に重宝しそうだなと感じました!

仮に test に失敗したとしても、actual differs from expected として、実際の結果と期待結果との差分もわかりやすく表示してくれるため、非常に便利だなと思いました。

ephemeral model に対して Unit Tests する

私が Unit Tests を使用してみたいと感じたのは、intermediate layerephemeral model に対してでした。

dbt model の見通しを良くするために、intermediate の model にロジックを集約させることは多いかと思います。したがって、intermediate の各 model に対して Unit Tests を実装することは、効果が高いのではないかと考えました。

複数の ephemeral model に依存する model の例

ephemeral な model は DWH 上に構築されるわけではなく、Model Contracts など一部の機能に対して制限がありますが、Unit Tests は ephemeral な model に対しても利用することができます。 (この点が少し不安でしたが杞憂に終わりました 😌)

ephemeral model に対する Unit Tests も、前述の例とほとんど使用方法は変わらないため yaml の詳細は省略します。

unit_tests:
  - name: test_XXX_is_passed
    description: ephemeral_model_1  XXX ロジックのテスト
    model: ephemeral_model_1
    given:
      - ...

ephemeral model を Mock する

前述の main_model のように、 ephemeral model を参照するモデルはどのように Unit Tests すればよいでしょうか?少しクセがあるため、確認していきます。

revenue の例では dict で input データを Mock していました。

given:
    - input: ref('fct_sales')
    rows:
        - { id: 1, price: 100, quantity: 2 }

input データは format により dict や csv が指定可能ですが、 ephemeral model では format: sql を使用します。

unit_tests:
  - name: test_revenue_equals_price_times_quantity
    model: revenue
    given:
      - input: ref('ephemeral_model') # ephemeral model を Mock する
        format: sql
        rows: |
          select 1 as id, 100 as price, 2 as quantity
          # 必要に応じて UNION ALL 等で行を追加
    expect:
      rows:
        - { id: 1, revenue: 200 }

sql は inline で記述するほか、ファイルに切り出することもできます。下記は tests/fixtures/mock_ephemeral_model.sql に Mock データを SELECT する SQL ファイルを配置し、読み込ませている例となります。

unit_tests:
  - name: test_revenue_equals_price_times_quantity
    model: revenue
    given:
      - input: ref('ephemeral_model') # ephemeral model を Mock する
        format: sql
        fixture: mock_ephemeral_model # デフォルトでは tests/fixtures 配下を探索する
    expect:
      rows:
        - { id: 1, revenue: 200 }

本稿執筆現在、公式ドキュメントによると、ephemeral model の Mock は format: sql を指定しなければならないようです。私が確認した限りでは、format: dictformat: csv は使用できませんでした。

If you want to unit test a model that depends on an ephemeral model, you must use format: sql for that input.

まとめ

dbt の Unit Tests を活用することで、モデルのロジックの正確性を簡単にテストすることができることがわかりました。

Unit Tests を開発時に作っておけばロジックを検証しながら安心して開発が進められますし、リファクタ時にも役立ちそうだなと思いました!ロジックを構築したあと、model を dbt run してデータを確認し、ロジックが正しいか検算して... の大変な作業から解放してくれそうで、どんどんこれから使っていきたいです。

また、Unit Tests は ephemeral model 自身や、ephemeral model を参照する model にも活用することができるのも、とても使いやすいなと感じました!

現在、Unit Tests の input には dbt の model を指定可能ですが、CTEs の単位などで指定できるとより柔軟にテストできそうだなと思いました。

Belong ではこのように dbt を用いた DWH の構築を積極的に実施しています!Data Platform チームに興味を持っていただけた方は是非 Entrancebook をご覧ください!