Unit Testing on dbt

## dbtにユニットテストがクルー！
dbtではこれまでも[Singular tests](https://docs.getdbt.com/docs/build/tests#singular-tests)や[Generic tests](https://docs.getdbt.com/docs/build/tests#generic-tests)といったdbtが扱うデータモデルの品質をチェックすることができる機能を提供していましたがここに新たにデータモデルに対するユニットテストを行う機能が追加されることになりました。

これまでもhttps://github.com/EqualExperts/dbt-unit-testingやhttps://github.com/AgeOfLearning/dbt-unit-test などといったdbtのユニットテストを行うフレームワークがコミュニティ主導で開発され提供されていましたが `1.8` よりdbtのネイティブ機能として提供されることになりました。

## What is this?
https://github.com/dbt-labs/dbt-core/discussions/8275 よりProposalを眺めてみます。
モデルとして以下のような `my_model` を定義しています。モデルの中ではそれぞれのユーザーのメールアドレスが有効かどうかを判定するビジネスロジックが組み込まれています。

``` sql
select
  users.user_id,
  users.email,
  case
    when regexp_like(users.email,'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') = true
      and accepted_email_domains.tld is not null
        then true
     else false
  end as is_valid_email_address
from {{ ref('users')}} users
left join {{ ref('top_level_domains') }} accepted_email_domains
on users.email_top_level_domain = lower(accepted_email_domains.tld)
```

このモデルのロジックの検証を行う入力と期待する出力のデータをyamlとして書いていきます。

``` yaml
unit:
  - model: my_model # name of the model I'm unit testing
    tests:
     - name: test_is_valid_email_address # this is the unique name of the test
       given: # optional: list of inputs to provide as fixtures
        - input: ref('users')
          rows:
           - {user_id: 1, email: cool@example.com,     email_top_level_domain: example.com}
           - {user_id: 2, email: cool@unknown.com,     email_top_level_domain: unknown.com}
           - {user_id: 3, email: badgmail.com,         email_top_level_domain: gmail.com}
           - {user_id: 4, email: missingdot@gmailcom,  email_top_level_domain: gmail.com}
         - input: ref('top_level_domains')
           rows:
            - {tld: example.com}
            - {tld: gmail.com}
       expect: # required: the expected output given the inputs above
        - {user_id: 1, is_valid_email_address: true}
        - {user_id: 2, is_valid_email_address: false}
        - {user_id: 3, is_valid_email_address: false}
        - {user_id: 4, is_valid_email_address: false}
    - name: test_another_column
      ...
```

- 入力データ
ref('users') からの入力としてモックするデータを定義します。それぞれに user_id, email, email_top_level_domain が指定されています。
ref('top_level_domains') からの入力は、受け入れられるトップレベルドメイン（tld）を定義しています。

- 期待される出力:
このテストでは、各ユーザーのレコードが保持するメールアドレスが有効であるかどうかを判定するロジックが正しいかをチェックしています。
各ユーザーのメールアドレスの有効性が true または false で評価されます。
- cool@example.comは有効なドメインかつメールアドレスのフォーマットとして成り立っているので有効
- cool@unknown.comは未知のドメインであるため無効
- badgmail.comは@マークが欠けているため無効
- missingdot@gmailcomは.が欠けているため無効
であることが期待される出力となります。

このように実際のデータを使うことなくエッジケースとなるような少数のデータを使ってデータモデル内のビジネスロジックの品質を担保できるのがdbtのユニットテストの特徴と言えそうです。

さらにユニットテスト機能のロードマップは以下のようになると書かれています。

#### MVPとしての機能 [P0s]
- 新しいdbtコマンド: dbt unit が提案され、オプションの --select メソッドを受け入れ、関連するユニットテストを実行します。
- モデルのユニットテスト実行: 各ユニットテストは、本番データセットではなくモックされた入力データを解決する ref または source マクロを使用してモデルを実行します。
- テストの独立性: ユニットテストは互いに独立しており、任意の順序で、利用可能なスレッド数で実行できます。
- 個別のテスト実行: モデルに定義されているすべてのユニットテストを実行することなく、単一のユニットテストを実行できるべきです。
- 入力と期待値の柔軟性: テストケースに関連するカラムのみを明示的に指定する必要があります。これにより、簡潔かつ特定のユニットテストの記述が可能になります。
- モック値のデータ型指定不要: ユニットテストを簡潔にし、アダプター間での移植性を保持するため、モック値にデータ型を指定する必要はありません。
- マクロ、変数、環境変数のオーバーライド: ユニットテストごとにマクロ、変数、環境変数のオーバーライドを設定できる機能が提供されます。
- 明確なエラーメッセージを伴う等値アサーション。
- 入力フォーマットの設定可能性: 入力データの形式（例：CSVやCSVファイル）を設定可能にします。
#### MVP+α [P1s]
- インラインフィクスチャの再利用。
- スレッド引数を超える並行性: --groups <n> や --split <n> などの引数を検討し、ユニットテストのコレクションを分割して実行する機能。
- マクロのシンタックスシュガー。
- 単一ユニットテストのYAML生成の簡素化: コード生成やdbt Cloud IDEを活用した簡素化。
- Pythonモデルのユニットテスト。

## 個人的な感想
データモデル作成時、複雑なSQLロジックを扱うことは一般的です。
そこで、本番データを使わずにこれらのロジックをテストできるユニットテストが非常に役立つと考えられます。これまでのdbtのテストでは、カラムの値が一意かどうか、NULL値が含まれていないかなど、データの品質に関するチェックが主でした。
しかし、ユニットテストの導入により、データモデルに含まれるビジネスロジックの正確性も保証できるようになります。
つまり、従来のテストがデータセットの内容の品質を評価していたのに対し、ユニットテストはdbtによって構築されるデータモデルそのものの品質を評価することが可能になるのです。

dbtがさらに多機能な「ELT界の十徳ナイフ」としての地位を盤石にするものではないでしょうか！