Timee Product Team Blog

タイミー開発者ブログ

dbt 1.8のUnit Tests 実施とその知見(時間ロックとSQLの分割について)

株式会社タイミーのkatsumiです!

dbtのバージョン1.8以上を利用することで、unit testsが利用可能になります。今までもSingular テスト(単一テスト)やGeneric テスト(汎用テスト)は可能でしたが、テストデータを利用した単体テストも行うことができます。

導入準備

dbt-coreの場合

dbt v1.8 以上を利用してください。

dbt-cloudの場合

2024/06/12時点では dbt「Keep on latest version」を選択することで利用できます。

弊社ではunit-test用の環境のみlatest versionを利用しています。

Unit Testの基本

# run data and unit tests
dbt test

# run only data tests
dbt test --select test_type:data

# run only unit tests
dbt test --select test_type:unit

# run tests for one_specific_model
dbt test --select "one_specific_model"

# run data tests limited to one_specific_model
dbt test --select "one_specific_model,test_type:data"

# run unit tests limited to one_specific_model
dbt test --select "one_specific_model,test_type:unit"

unit-testに関係する新しいコマンドが追加されました。このコマンドは、以前のデータテストで使用していたselect機能と同様に、特定のテストケースを選択して実行することができます。

ymlによるテストレコードの書き方

  - name: test_name
    description: "テストの説明"
    model: my_model
    given:
      - input: ref('users')
        rows:
          - {id: 1, user_email: example@example.com}
          
    expect:
      rows:
        - {id: 1, domain: example.com}

name: test_name

これはテストの名前です。この名前はテストケースを識別するために使用します。

description: “テストの説明”

これはテストケースの説明です。この説明には、テストが何を意図しているのか、テストの目的や背景について記載します。

model: my_model

これはテスト対象となるモデルの名前です。ここでは「my_model」がテスト対象のモデルとして指定されています。

given

データの内容です。ここでは「id: 1」で「user_email」「example@example.com」のユーザーを指定しています。このデータがテストの入力として使用されます。

expect

これは期待される結果を指定します。テストが成功するためには、モデルが「id: 1」のユーザーに対して「domain」が「example.com」として返される必要があります。期待される結果と実際の結果が一致するかどうかを検証します。

ファイルによるテストレコードの書き方

unit_tests:
  - name: test_my_model
    model: my_model
    given:
        - input: ref('users')
            format: csv
            fixture: users

プロジェクトのtests/fixturesディレクトリにあるCSVファイル名を指定することで利用できます。test-pathsオプションを使用することで、ディレクトリ構成を柔軟に指定することもできます。

未定義のカラムの挙動

未入力のカラムに関しては、safe_cast(null as INT64)のように型が定義されたnullのデータで補完されます。リレーションが必要なものや、ロジックに影響を与えるカラムの記入が必要になります。

実施における知見

大規模なクエリは”ephemeral”で細かいテスト行う。

  • with句が複数ありテストケースが複雑で見通しが悪くなるケースがあります。弊社ではSQLのテスト単位のロジックを”ephemeral”で分けて個別のmodelにてテストを書く実装を試しています。
  • 通常のモデルと同じ書き方でテストを実施することが可能です。
WITH 処理1_cte AS (
    SELECT * FROM {{ ref('処理1のephemeral') }}
)

, 処理2_cte AS (
    SELECT * FROM {{ ref('処理2のephemeral') }}
)

, 処理3_cte AS (
    SELECT * FROM {{ ref('処理3のephemeral') }}
)

時系列系の時間の停止をマクロで行う。

  • テストしたいケースにはcurrent_datetimeなど現在の時刻を利用するものがあります。その場合、テストを書く際に時間を固定する必要があります。
  • dbtのユニットテストでは、YAMLファイル上でdbtのマクロを置き換える機能があります。この機能を利用して、時間を固定する実装を行っています。
  - name: test_case
    model: my_model
    overrides:
      macros:
        current_datetime_jst: "date('2024-01-01')"
{{ config(materialized='ephemeral') }}

SELECT 
-- ここにロジックを書く
FROM {{ ref('users') }} AS users
WHERE DATETIME_TRUNC(created_at, MONTH) = DATE_TRUNC({{ current_datetime_jst() }}, MONTH)

Testに関するSQLの確認ができる。

  • 実際の仕組みとしてはテスト用のSQLが生成され、フィクスチャ(テストデータ)も含めたSQLが実行されます。debugコマンドやコンパイルされたSQLを確認することで、テストの挙動をチェックできます。
  • テストケースの問題が起きた時にSQLにて要因分析を行いました。

まとめ

重要指標の計算や複雑な時系列処理、プロダクトのロジックを再現する箇所では、テストケースを用意していこうと考えています。またテストケースを先に定義したのちにクエリを書くことも簡単にできるようになったように感じます。信頼性の高いモデルにするために、重要な機能になっていきそうです。

以上、unit-testsを試した時に得られた知見のまとめでした。この情報が役立てば幸いです!

We’re Hired

タイミーでは、一緒に働くメンバーを募集しています!!

参考資料

  • Unit tests | dbt Developer Hub

https://docs.getdbt.com/docs/build/unit-tests

  • Unit Testing

https://github.com/dbt-labs/dbt-core/discussions/8275