dbtのunit testを試してみた

# この記事は？
dbtのunit testを触ってみたメモです。

# 環境
- Cloud IDE
- version: versionless
    - unit test は1.7までは使えないため、versionless(1.8+)にしてください。（2024年12月9日現在）
- DWHはsnowflake

# unit testが動作するまでにやったこと
1. ソースmodelの準備
2. テスト対象modelの準備
3. テスト用ymlの作成
4. テストの実行
5. エラー修正

# ソースmodelの準備
テスト対象のmodelの元となるmodelの準備です。
社員番号と社員名が記載してある単純なmodelです。

```sql:my_test_raw.sql
select 
    emp_id,
    emp_name
from
    {{ ref("employee") }}
```

今回はテストを単純化するためにソース用のmodelを作成ました。
実際の環境ではソースmodelは作成しません（すでにあるmodelを使用）。

- 社員番号(emp_id)
    - 0埋めの数字4桁
    - スプレッドシートで取り込んでいるため、桁落ちしているレコードがある
- 社員名(emp_name)
    - 姓名は半角スペースで区切られている

# テスト対象modelの準備
テスト対象modelの準備です。
ソースmodelを変換して、実際に使用するmodelを作成します。

```sql:my_test.sql
select
    iff(len(emp_id) < 4, lpad(emp_id, '0', 4), emp_id) as emp_id,
    trim(split(emp_name, ' ')[0], '"') as family_name,
    trim(split(emp_name, ' ')[1], '"') as first_name
from
    {{ ref("my_test_raw") }}
```

- 社員番号(emp_id)
    - 4桁に満たない（0埋めが外れている）場合
        - 先頭に'0'を4文字になるまで埋める
    - 4桁以上の場合
        - emp_idをそのまま使用
- 姓(family_name)
    - 社員名を半角スペースで分割して、前半を姓として使用
    - splitの仕様で「"」（ダブルクォート）が付加されるため、trimeで「"」を除去
- 名(fisrt_name)
    - 社員名を半角スペースで分割して、後半を姓として使用
    - splitの仕様で「"」（ダブルクォート）が付加されるため、trimeで「"」を除去

# テスト用ymlの作成

テスト用ymlを作成します。

```yml:my_test.yml
version: 2
unit_tests:
  - name: test_my_test
    model: 
      my_test
    # givenは入力する値を定義する
    given:
      # ここにはmodelのinputと同じことを書く
      - input: ref("my_test_raw")
        # model: my_test_rawの値
        rows:
          - {emp_id: '3', emp_name: '山田 奈緒子'}
          - {emp_id: '100', emp_name: '上田 次郎'}
          - {emp_id: '9789', emp_name: '矢部 謙三'}
    # expectは期待する値を定義する
    expect:
        # model: my_testで変換した値
      rows:
        - {emp_id: '0003', family_name: '山田', first_name: '奈緒子'}
        - {emp_id: '0100', family_name: '上田', first_name: '次郎'}
        - {emp_id: '9789', family_name: '矢部', first_name: '謙三'}
```

#### 入力値の指定
givenセクションでは入力値を指定します。
- input
    - ソースmodelのfromに該当する値を使用します
- rows
    - テストデータを定義します

```yml
    given:
      # ここにはmodelのinputと同じことを書く
      - input: ref("my_test_raw")
        # model: my_test_rawの値
        rows:
          - {emp_id: '3', emp_name: '山田 奈緒子'}
          - {emp_id: '100', emp_name: '上田 次郎'}
          - {emp_id: '9789', emp_name: '矢部 謙三'}
```

#### 期待値の指定
expectセクションではテスト結果の期待値を指定します。
- rows
    - テスト対象のmodelで変換した後の値
    - 今回は以下の2点を変換しています
        - 社員番号→4桁0埋め
        - 社員名→姓・名の項目に分割

```yml
    # expectは期待する値を定義する
    expect:
        # model: my_testで変換した値
      rows:
        - {emp_id: '0003', family_name: '山田', first_name: '奈緒子'}
        - {emp_id: '0100', family_name: '上田', first_name: '次郎'}
        - {emp_id: '9789', family_name: '矢部', first_name: '謙三'}
```

# テストの実行
準備ができたところでテストをやってみます！
Cloud IDEのコンソールから、以下のコマンドを実行をします。

> dbt test --select my_test

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974516/36475879-3651-d5f3-417f-b8a8623c52a9.png)

エラーが出ました。
お・や・く・そ・く　です。

エラーになった原因を探っていきましょう。

# エラー修正

結果ログの「Details」をクリックすると、詳細なエラーメッセージが表示されます。
メッセージの最後の部分を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974516/cea78d61-4a61-e71d-1b1a-6381f5238d38.png)

>actual differs from expected:

期待値と実際の値が異なる箇所は赤字で強調されています。

snowflakeの公式ドキュメントによると、LPADの仕様は…

> LPAD(\<base>, \<length_expr> [, \<pad>])

| 第1引数 | 第2引数 | 第3引数 |
| -- | -- | -- |
| 文字列 | 埋める桁数 | 埋める文字 |

なので、SQLで書いたlpadの引数が誤っていたことになります。

```sql
    iff(len(emp_id) < 4, lpad(emp_id, '0', 4), emp_id) as emp_id,
```

次のように修正します。

```sql
    iff(len(emp_id) < 4, lpad(emp_id, 4, '0'), emp_id) as emp_id,
```

もう一度testコマンドを実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974516/b7e08e0a-94e7-c6ec-5b15-a892d1180d68.png)

無事テストが成功しました！

# おまけ
## JoinしたModelの場合

複数のModelをJoinして新たなModelを作成するケースはよく出てきます。

```sql:my_test_join.sql
select
    emp.emp_id,
    emp.emp_name,
    emp.org_code,
    org.org_name,
from 
    {{ ref("my_test_join_raw_emp") }} as emp
join {{ ref("my_test_join_raw_org") }} as org
    on emp.org_code = org.org_code
```

このケースでは、testのinputを複数指定します。

```yml:my_test_join.yml
version: 2
unit_tests:
  - name: test_my_test_join
    model: 
      my_test_join
    # givenは入力する値を定義する
    given:
      # ここにはmodelのinputと同じことを書く
      - input: ref("my_test_join_raw_emp")
        rows:
          - {emp_id: '0003', emp_name: '山田 奈緒子', org_code: '0001'}
      # 複数のテーブルをJoinしたModelはinputを複数羅列する
      - input: ref("my_test_join_raw_org")
        rows:
          - {org_code: '0001', org_name: 'マジシャン'}
    # expectは期待する値を定義する
    expect:
        # model: my_test_joinで変換した値
      rows:
          - {emp_id: '0003', emp_name: '山田 奈緒子', org_code: '0001', org_name: 'マジシャン'}
```

# 参考資料

この記事を作成するにあたり、以下の資料を参考にさせていただきました。
ありがとうございます。

- [Unit tests | dbt Developer Hub
](https://docs.getdbt.com/docs/build/unit-tests)
- [dbt v1.8で追加された単体テストを触ってみた](https://speakerdeck.com/k_data_analyst/dbt-v1-dot-8dezhui-jia-saretadan-ti-tesutowohong-tutemita)
- [dbtのUnit testsをプロダクトに導入してクエリのロジックをテストする](https://developers.cyberagent.co.jp/blog/archives/49438/)
- [[新機能]dbtでSQL上の1つ1つのロジックに対しテストを行える「Unit tests」を試してみた](https://dev.classmethod.jp/articles/dbt-try-unit-tests/)