こんにちは。エンジニアチームです。
今回は、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で定義) |
| 強み | 複雑な計算ロジックによるデータ生成 | 境界値・エッジケースの明示的なドキュメント化 |
| 運用ポリシー | コード内でロジックを組んで生成 | テストすべきデータパターン(境界値)を静的に定義 |
- 静的なテストケース管理の徹底
Dataformでは動的に日付を生成してテストすることもありましたが、dbtでは「固定されたシナリオ」としてYAMLに記述するスタイルが基本です。
これ一見不便に見えますが、「ビジネス要件として担保すべき境界値は不変である」という考えに基づくと、むしろ仕様が明確になるメリットがありました。 - モックデータのスコープ
私たちの環境では、l1_,l2_といったレイヤー構造を採用しています。
dbt Unit Tests はref()やsource()
をピンポイントでモック(上書き)できるため、「テストしたいロジックが依存している直近のモデルだけ」をモック化することで、テスト記述量を最小限に抑えられました。
複雑に依存し合うパイプラインの中で、「このモデルのロジックだけ」を切り出して検証するのに適しています。
4. 導入の効果とまとめ
導入の結果、単純な作業効率化以上に、「ロジック変更に対する心理的ハードルの低下」という効果が大きかったです。
変更を加える際、「このエッジケースはテスト済みだから大丈夫」という確証が手元にあることで、検証作業の手戻りが減り、レビューもスムーズになります。
特に、データ基盤が大規模化し、関係者が増えるにつれて、こうした「コード化された仕様書」としてのテストの価値は高まると感じています。
これからdbt Unit Testsを触る方は、まずは「過去にバグを出したロジック」一つに対してテストを書いてみることをお勧めします。

コメント