dbt Unit Testsの実装: 複雑なビジネスロジックを安全に管理する

データ分析

こんにちは。エンジニアチームです。

今回は、dbt Core 1.8 で導入された Unit Tests (ユニットテスト) 機能について紹介します。
KIYONOのデータ分析基盤において、Dataformからdbtへの移行や並行運用を検討する中で、この機能がどのようにフィットし、どのような課題を解決したかについて、実際のケースを交えて解説します。

1. ユニットテスト導入の背景とメリット

データ基盤運用において、「実装したロジックが意図通りか」を検証するコストは常に課題です。
特に、顧客ごとのステータス判定や、契約・解約に伴う期間計算など、ビジネスロジックが複雑になるほど、以下の問題が顕在化します。

  • 本番データへの依存: 開発環境に本番相当のデータがないと、エッジケース(異常値や発生頻度の低いパターン)の検証ができない。
  • 検証コスト: テストのためにデータを物理化(テーブル作成)する必要があり、時間とコンピュートリソースを消費する。

dbt Unit Tests は、これらの課題に対して「モデルを物理化せずにロジックを検証する」というアプローチで解決策を提示しました。

本番データが存在しない環境での再現性

最大のメリットは、本番データにアクセスできない環境や、データがまだ蓄積されていない初期フェーズでも、「想定されるデータ」を定義することでロジックを完全に検証できる点です。
「未来の日付が入ってきたら」「NULLだった場合は」といった、本番データでは再現しにくいケースも、テストコード上で明示的に作成して検証可能です。これにより、リリースの安全性が飛躍的に向上します。

2. 実践: ビジネスロジックの検証ケース

ここでは、私たちが扱うことの多い「期間計算」や「ステータス判定」を例に、具体的な検証フローを紹介します。
例えば、最終訪問日からの経過日数計算において、「未来日付の考慮漏れ」や「NULL処理の誤り」はよくあるバグです。

検証対象のロジック (dbt SQL)

(簡略化した例です)

-- l1_customer_master.sql
WITH visits AS (
    SELECT
        customer_code,
        -- 未来の日付は異常値として現在日付でクリップする
        LEAST(visit_date, CURRENT_DATE('Asia/Tokyo')) as valid_visit_date
    FROM {{ source('raw', 'visits') }}
)
SELECT
    customer_code,
    DATE_DIFF(CURRENT_DATE('Asia/Tokyo'), MAX(valid_visit_date), DAY) as days_since_last_visit
FROM visits
GROUP BY customer_code

dbt Unit Test による定義

このロジックに対し、unit_test.yml で「入力(given)」と「期待値(expect)」を定義します。SQLを書く必要はありません。
以下のようなテストパターンを想定します(起点を2023-11-01とした場合)。

テストケース名 入力 (visit_date) 期待値 (経過日数) 備考
通常ケース 2023-10-01 30日 正常に過去の日付として計算される
エッジケース 2099-01-01 0日 未来の日付は当日(0日経過)として補正される

 

unit_tests:
  - name: test_last_visit_logic
    model: l1_customer_master
    given:
      - input: source('raw', 'visits')
        rows:
          - {customer_code: 'A', visit_date: '2023-10-01'}  # 通常ケース
          - {customer_code: 'B', visit_date: '2099-01-01'}  # エッジケース: 未来日付
    expect:
      rows:
        - {customer_code: 'A', days_since_last_visit: 30} # 起点が2023-11-01の場合
        - {customer_code: 'B', days_since_last_visit: 0}  # 未来日付は当日扱い(0日)となること

このように、テストケースそのものが「仕様書」として機能します。

3. Dataform運用からのFit & Gap

私たちのチームではDataformも活用しています。
今回のdbt Unit Tests導入において、既存の運用フローにどのようにフィットさせたか、構成上の違いを整理しました。

構成上の比較とdbtのアプローチ

項目 Dataform (従来) dbt Unit Tests
テストデータ生成 動的 (JavaScriptで生成可能) 静的 (YAMLで定義)
強み 複雑な計算ロジックによるデータ生成 境界値・エッジケースの明示的なドキュメント化
運用ポリシー コード内でロジックを組んで生成 テストすべきデータパターン(境界値)を静的に定義

 

  1. 静的なテストケース管理の徹底
    Dataformでは動的に日付を生成してテストすることもありましたが、dbtでは「固定されたシナリオ」としてYAMLに記述するスタイルが基本です。
    これ一見不便に見えますが、「ビジネス要件として担保すべき境界値は不変である」という考えに基づくと、むしろ仕様が明確になるメリットがありました。
  2. モックデータのスコープ
    私たちの環境では、l1_, l2_ といったレイヤー構造を採用しています。
    dbt Unit Tests は ref()source()
    をピンポイントでモック(上書き)できるため、「テストしたいロジックが依存している直近のモデルだけ」をモック化することで、テスト記述量を最小限に抑えられました。
    複雑に依存し合うパイプラインの中で、「このモデルのロジックだけ」を切り出して検証するのに適しています。

4. 導入の効果とまとめ

導入の結果、単純な作業効率化以上に、「ロジック変更に対する心理的ハードルの低下」という効果が大きかったです。

変更を加える際、「このエッジケースはテスト済みだから大丈夫」という確証が手元にあることで、検証作業の手戻りが減り、レビューもスムーズになります。
特に、データ基盤が大規模化し、関係者が増えるにつれて、こうした「コード化された仕様書」としてのテストの価値は高まると感じています。

これからdbt Unit Testsを触る方は、まずは「過去にバグを出したロジック」一つに対してテストを書いてみることをお勧めします。

コメント

PAGE TOP
タイトルとURLをコピーしました