CUBE SUGAR CONTAINER

技術系のこと書きます。

dbt (data build tool) を使ってデータをテストする

ソフトウェアエンジニアリングの世界では、自動化されたテストを使ってコードの振る舞いを検証するのが当たり前になっている。 同じように、データエンジニアリングの世界でも、自動化されたテストを使ってデータの振る舞いを検証するのが望ましい。

データをテストするのに使える OSS のフレームワークも、いくつか存在する。 今回は、その中でも dbt (data build tool) を使ってデータをテストする方法について見ていく。 dbt 自体はデータのテストを主目的としたツールではないものの、テストに関する機能も備えている。

また、dbt には WebUI を備えたマネージドサービスとしての dbt Cloud と、CLI で操作するスタンドアロン版の dbt Core がある。 今回扱うのは後者の dbt Core になる。

使った環境は次のとおり。

$ sw_vers
ProductName:    macOS
ProductVersion: 12.2
BuildVersion:   21D49
$ uname -srm                       
Darwin 21.3.0 arm64
$ python -V          
Python 3.9.10
$ pip list | grep dbt
dbt-core                 1.0.1
dbt-extractor            0.4.0
dbt-postgres             1.0.1

もくじ

下準備

今回は、公開されているデータセットをローカルの PostgreSQL に取り込んで、それを dbt でテストしていく。

PostgreSQL をセットアップする

まずは Homebrew を使って PostgreSQL をインストールしよう。 また、データセットをダウンロードするために wget も入れておく。

$ brew install postgresql wget

PostgreSQL のサービスを起動する。

$ brew services start postgresql
 brew services list            
Name       Status  User    File
postgresql started amedama ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

データセットとしては seaborn が利用している taxis データセットを使う。 ダウンロードして /tmp に保存する。

$ wget https://raw.githubusercontent.com/mwaskom/seaborn-data/master/taxis.csv -P /tmp

上記のデータセットに合う形でテーブルの定義を作る。

$ cat << 'EOF' | psql -d postgres
CREATE TABLE IF NOT EXISTS public.taxis (
  id SERIAL NOT NULL,
  pickup TIMESTAMP NOT NULL,
  dropoff TIMESTAMP NOT NULL,
  passengers INT NOT NULL,
  distance FLOAT NOT NULL,
  fare FLOAT NOT NULL,
  tip FLOAT NOT NULL,
  tolls FLOAT NOT NULL,
  total FLOAT NOT NULL,
  color TEXT NOT NULL,
  payment TEXT,
  pickup_zone TEXT,
  dropoff_zone TEXT,
  pickup_borough TEXT,
  dropoff_borough TEXT
);
EOF

先ほどダウンロードした CSV ファイルの内容を上記のテーブルに取り込む。

$ cat << 'EOF' | psql -d postgres
COPY public.taxis (
  pickup,
  dropoff,
  passengers,
  distance,
  fare,
  tip,
  tolls,
  total,
  color,
  payment,
  pickup_zone,
  dropoff_zone,
  pickup_borough,
  dropoff_borough
)
FROM '/tmp/taxis.csv'
WITH (
  FORMAT csv,
  HEADER true
)
EOF

また、データベースを操作するためのユーザ (ROLE) を alice という名前で追加しておく。

$ cat << 'EOF' | psql -d postgres
CREATE ROLE
  alice
WITH
  LOGIN
  PASSWORD 'wonderland'
EOF

ちなみに、ユーザにパスワードは設定してるけど、実は無くても問題はない。 Homebrew でインストールした場合、デフォルトでローカルからの接続が trust になっているため。 パスワードなしでも接続できる。

$ cat /opt/homebrew/var/postgres/pg_hba.conf | sed -e "/^#/d" -e "/^$/d"
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
local   replication     all                                     trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust

追加したユーザにテーブルを操作する権限をつけておく。

$ cat << 'EOF' | psql -d postgres
GRANT
  ALL PRIVILEGES
ON
  TABLE public.taxis
TO
  alice
EOF

次のように、ユーザとパスワードを指定してデータを見られることを確認しておく。

$ echo "SELECT * FROM public.taxis LIMIT 5" | psql -d postgres --user alice --password             
Password: 
 id |       pickup        |       dropoff       | passengers | distance | fare | tip  | tolls | total | color  |   payment   |      pickup_zone      |     dropoff_zone      | pickup_borough | dropoff_borough 
----+---------------------+---------------------+------------+----------+------+------+-------+-------+--------+-------------+-----------------------+-----------------------+----------------+-----------------
  1 | 2019-03-23 20:21:09 | 2019-03-23 20:27:24 |          1 |      1.6 |    7 | 2.15 |     0 | 12.95 | yellow | credit card | Lenox Hill West       | UN/Turtle Bay South   | Manhattan      | Manhattan
  2 | 2019-03-04 16:11:55 | 2019-03-04 16:19:00 |          1 |     0.79 |    5 |    0 |     0 |   9.3 | yellow | cash        | Upper West Side South | Upper West Side South | Manhattan      | Manhattan
  3 | 2019-03-27 17:53:01 | 2019-03-27 18:00:25 |          1 |     1.37 |  7.5 | 2.36 |     0 | 14.16 | yellow | credit card | Alphabet City         | West Village          | Manhattan      | Manhattan
  4 | 2019-03-10 01:23:59 | 2019-03-10 01:49:51 |          1 |      7.7 |   27 | 6.15 |     0 | 36.95 | yellow | credit card | Hudson Sq             | Yorkville West        | Manhattan      | Manhattan
  5 | 2019-03-30 13:27:42 | 2019-03-30 13:37:14 |          3 |     2.16 |    9 |  1.1 |     0 |  13.4 | yellow | credit card | Midtown East          | Yorkville West        | Manhattan      | Manhattan
(5 rows)

これでデータベースの準備は整った。

dbt をインストールする

続いては肝心の dbt をインストールする。 dbt は Python で開発されているので、pip を使ってインストールできる。

dbt では、接続するデータベースごとにアダプタと呼ばれるパッケージを切り替えて対応する。 たとえば PostgreSQL なら dbt-postgres というアダプタを使えば良い。 これが、もしバックエンドに BigQuery を使うなら dbt-bigquery を使うことになる。 いずれも、依存関係に本体の dbt-core が入っているので一緒にインストールできる。

$ pip install dbt-postgres

インストールすると dbt コマンドが使えるようになる。

$ dbt --version                                  
installed version: 1.0.1
   latest version: 1.0.1

Up to date!

Plugins:
  - postgres: 1.0.1

これで必要な下準備がすべて整った。

dbt からデータベースに接続する

まずは dbt からデータベースに接続する部分を確認していく。

dbt では dbt_project.yml という設定ファイルが必要になるので、まずは作る。 name はプロジェクト名、version はプロジェクトのバージョン番号、config-version は YAML のコンフィグ形式のバージョン番号を表している。 profile というのは、dbt がデータベースに接続するやり方のことをプロファイルと呼んでいて、それにつけた名前のこと。

$ cat << 'EOF' >> dbt_project.yml
name: taxis
version: 0.0.1
config-version: 2
profile: postgres_taxis
EOF

ちなみに dbt init コマンドを使えば一通りの設定ファイルとディレクトリ構造の入ったボイラープレートを展開することもできる。 ここらへんは好みで。

$ dbt init

次は、上記の設定ファイルで指定した postgres_taxis という名前のプロファイルを用意しよう。 データベースへの接続方法は、パスワードなど秘匿しておきたい情報も多い。 そのため、デフォルトではプロジェクトのディレクトリとは分離して、ホームディレクトリ以下を読むことになっている 1

~/.dbt というディレクトリに profiles.yml という名前で YAML ファイルを作る。 そして、postgres_taxis というプロファイルを定義する。 その下にある target では、デフォルトで使う接続先の環境を指定している。 これは、同じ用途のデータベースであっても、一般的には役割によって複数の環境を用意することになるため。 たとえば、本番 (prod)、ステージング (stg)、開発 (dev) といったように。 その中で、デフォルトで使用するものを指定している。 なお、接続先はコマンドラインオプションの --target で切り替えることができる。 target に指定した名前は、outputs 以下にある設定と対応する。 ここでは local という名前で設定した。 typepostgres を指定することで、アダプタとして dbt-postgres の実装が使われることになる。

$ mkdir -p ~/.dbt
$ cat << 'EOF' > ~/.dbt/profiles.yml                                             
postgres_taxis:
  target: local
  outputs:
    local:
      type: postgres
      threads: 1
      host: localhost
      port: 5432
      user: alice
      pass: wonderland
      dbname: postgres
      schema: dbt_alice
EOF

ちなみに、PostgreSQL ではテーブルの階層構造が <database>.<schema>.<table> という 3 層構造になっている。 上記で dbnameschema を指定しているのは、このため。 ただし、上記で指定している schema は、このプロファイルで使う作業用のスキーマの名前になっている。 つまり、アクセスできるスキーマがこれに限られるわけではない。

さて、プロファイルができたら、まずはデータベースへの接続が上手くいくことを確認しよう。 これには dbt debug コマンドを使う。

$ dbt debug
15:14:54  Running with dbt=1.0.1
dbt version: 1.0.1
python version: 3.9.10
python path: /Users/amedama/.virtualenvs/py39/bin/python
os info: macOS-12.2-arm64-arm-64bit
Using profiles.yml file at /Users/amedama/.dbt/profiles.yml
Using dbt_project.yml file at /Users/amedama/Documents/temporary/dbt-example/dbt_project.yml

Configuration:
  profiles.yml file [OK found and valid]
  dbt_project.yml file [OK found and valid]

Required dependencies:
 - git [OK found]

Connection:
  host: localhost
  port: 5432
  user: alice
  database: postgres
  schema: dbt_alice
  search_path: None
  keepalives_idle: 0
  sslmode: None
  Connection test: [OK connection ok]

All checks passed!

どうやら、無事に接続できたようだ。 データベースに接続できることを確認するのも、ある意味で健全性のテストと言えるかもしれない。

source freshness をテストする

さて、データベースに接続できることが分かった。 次の一手としては、すでにデータベースに取り込まれているテーブルをソース (source) として定義する。 ソースを定義しておくと、別の場所から参照できたり、それに対してテストが書けたりする。

テストの観点は色々とあるけど、まずは source freshness を確認してみよう。 これは、ソースの特定のカラムに含まれる最新のタイムスタンプが、現在時刻からどれくらい離れているかを検証するもの。 たとえば DWH へのデータの取り込みが何らかの理由で遅延したり、あるいは停止しているのを見つけるのに利用できる。

ソースを定義するには models というディレクトリを作って、そこに YAML の設定ファイルを追加する。 さっきも似たような作業があったと思うけど、基本的に dbt はユーザから見える部分のほとんどが YAML と SQL で成り立っている。 sources 以下に name でスキーマを指定して、その下に tables でテーブルを指定する。 以下であれば postgres.public.taxis という階層構造のテーブルを定義していることになる。 そして、その下にある freshnessloaded_at_field という項目で source freshness の設定をする。

$ mkdir -p models
$ cat << 'EOF' > models/taxis.yml
version: 2

sources:
  - name: public
    tables:
      - name: taxis
        freshness:
          warn_after:
            count: 1
            period: hour
          error_after:
            count: 1
            period: day
        loaded_at_field: pickup::timestamp
EOF

上記の設定では、pickup というカラムの最新の時刻が現在時刻から 1h 以上離れると警告、1d 以上離れるとエラーになる。 カラムの時刻は UTC を基準にする点に注意が必要。 つまり、JST を使っている場合には、UTC に変換する必要がある 2

設定できたら dbt source freshness コマンドを実行しよう。 これで、ソースに含まれる最新のタイムスタンプと現在時刻が比較される。

$ dbt source freshness
15:16:57  Running with dbt=1.0.1
15:16:57  Partial parse save file not found. Starting full parse.
15:16:57  Found 0 models, 0 tests, 0 snapshots, 0 analyses, 165 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
15:16:57  
15:16:57  Concurrency: 1 threads (target='local')
15:16:57  
15:16:57  1 of 1 START freshness of public.taxis.......................................... [RUN]
15:16:57  1 of 1 ERROR STALE freshness of public.taxis.................................... [ERROR STALE in 0.02s]
15:16:57  
15:16:57  Done.

当たり前だけど、実行は失敗する。 サンプルデータのタイムスタンプは、最新のレコードでも 2019 年になっている。 1h とか 1d なんて単位ではない離れ方をしている。

$ echo "SELECT MAX(pickup), NOW() FROM public.taxis" | psql -d postgres

         max         |              now              
---------------------+-------------------------------
 2019-03-31 23:43:45 | 2022-02-04 00:26:35.521179+09
(1 row)

試しに現在時刻との乖離が小さなデータを追加してみよう。 時刻の計算に GNU date を使いたいので Homebrew を使って coreutils をインストールする。

$ brew install coreutils

pickup が現在時刻から 2h 前のレコードを追加する。 これなら実行したときエラーではなく警告になるはずだ。 他のカラムについては適当な値で埋めた。

$ PICKUP=$(TZ=UTC gdate "+%Y-%m-%d %H:%M:%S" --date '2 hour ago')
$ DROPOFF=$(TZ=UTC gdate "+%Y-%m-%d %H:%M:%S" --date '1 hour ago')
$ cat << EOF | psql -d postgres
INSERT INTO taxis (
  pickup,
  dropoff,
  passengers,
  distance,
  fare,
  tip,
  tolls,
  total,
  color,
  payment,
  pickup_zone,
  dropoff_zone,
  pickup_borough,
  dropoff_borough
) VALUES (
  '${PICKUP}',
  '${DROPOFF}',
  1,
  1.5,
  7,
  2.0,
  0,
  12.5,
  'yellow',
  'credit card',
  'Lenox Hill West',
  'UN/Turtle Bay South',
  'Manhattan',
  'Manhattan'
);
EOF

先ほどと同じコマンドを実行すると、今度はたしかに警告 (WARN) に変わっている。

$ dbt source freshness
15:31:35  Running with dbt=1.0.1
15:31:35  Found 0 models, 0 tests, 0 snapshots, 0 analyses, 165 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
15:31:35  
15:31:35  Concurrency: 1 threads (target='local')
15:31:35  
15:31:35  1 of 1 START freshness of public.taxis.......................................... [RUN]
15:31:35  1 of 1 WARN freshness of public.taxis........................................... [WARN in 0.01s]
15:31:35  Done.

同じ要領で pickup と現在時刻の差が 1h 未満のデータを入れてみよう。

$ PICKUP=$(TZ=UTC gdate "+%Y-%m-%d %H:%M:%S" --date '15 min ago')
$ DROPOFF=$(TZ=UTC gdate "+%Y-%m-%d %H:%M:%S" --date '10 min ago')
$ cat << EOF | psql -d postgres
INSERT INTO taxis (
  pickup,
  dropoff,
  passengers,
  distance,
  fare,
  tip,
  tolls,
  total,
  color,
  payment,
  pickup_zone,
  dropoff_zone,
  pickup_borough,
  dropoff_borough
) VALUES (
  '${PICKUP}',
  '${DROPOFF}',
  3,
  2.16,
  9,
  1.1,
  0,
  13.4,
  'yellow',
  'cash',
  'Midtown East',
  'Yorkville West',
  'Manhattan',
  'Manhattan'
);
EOF

今度は実行が成功 (PASS) した。

$ dbt source freshness
15:33:36  Running with dbt=1.0.1
15:33:36  Found 0 models, 0 tests, 0 snapshots, 0 analyses, 165 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
15:33:36  
15:33:36  Concurrency: 1 threads (target='local')
15:33:36  
15:33:36  1 of 1 START freshness of public.taxis.......................................... [RUN]
15:33:36  1 of 1 PASS freshness of public.taxis........................................... [PASS in 0.01s]
15:33:36  Done.

これで source freshness の確認ができるようになった。

generic test を使ってテストを書く

さて、続いてはデータの中身を見るテストを書いていこう。 dbt を使ってテストを書くやり方には generic test と singular test の 2 つがある。 まずは、より汎用性の高い generic test から見ていこう。

generic test を使うには YAML の設定を追加するだけで良い。 次の設定では、ソースの taxis テーブルに含まれるいくつかのカラムに対してテストを用意している。 それぞれの名前から内容はなんとなく分かるはずだけど、念の為に書いておくと次のようなルールになっている。

  • id カラムは一意で NULL の値がないこと
  • pickup カラムは NULL の値がないこと
  • color カラムは yellowgreen の値だけあること
$ cat << 'EOF' > models/taxis.yml
version: 2

sources:
  - name: public
    tables:
      - name: taxis
        columns:
          - name: id
            tests:
              - unique
              - not_null
          - name: pickup
            tests:
              - not_null
          - name: color
            tests:
              - accepted_values:
                  values: ['yellow', 'green']
EOF

なお、先ほど確認した source freshness の設定は、簡単のために上記の設定ファイルからは省いた。

設定ファイルを作ったら dbt test コマンドでテストを実行する。

$ dbt test
09:38:01  Running with dbt=1.0.1
09:38:01  Found 0 models, 4 tests, 0 snapshots, 0 analyses, 165 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
09:38:01  
09:38:01  Concurrency: 1 threads (target='local')
09:38:01  
09:38:01  1 of 4 START test source_accepted_values_public_taxis_color__yellow__green...... [RUN]
09:38:01  1 of 4 PASS source_accepted_values_public_taxis_color__yellow__green............ [PASS in 0.03s]
09:38:01  2 of 4 START test source_not_null_public_taxis_id............................... [RUN]
09:38:01  2 of 4 PASS source_not_null_public_taxis_id..................................... [PASS in 0.01s]
09:38:01  3 of 4 START test source_not_null_public_taxis_pickup........................... [RUN]
09:38:01  3 of 4 PASS source_not_null_public_taxis_pickup................................. [PASS in 0.03s]
09:38:01  4 of 4 START test source_unique_public_taxis_id................................. [RUN]
09:38:01  4 of 4 PASS source_unique_public_taxis_id....................................... [PASS in 0.02s]
09:38:01  
09:38:01  Finished running 4 tests in 0.19s.
09:38:01  
09:38:01  Completed successfully
09:38:01  
09:38:01  Done. PASS=4 WARN=0 ERROR=0 SKIP=0 TOTAL=4

テストが実行されて成功 (PASS) した。

ちなみに、それぞれのテストケースは、いずれも SQL を使って実現されている。 先ほどの実行で、どのような SQL が発行されているかは、デフォルトで logs ディレクトリに生成されるログを読むと分かる。 テストは原則として「失敗するときに一致するレコードが出る SQL」になっている。 つまり、先ほどテストが成功したということは「発行した SQL に一致するレコードがなかった」ことを意味する。

また、上記ではソースに対してテストを書いたけど、モデルなど dbt に登場するその他のオブジェクトに対しても同じ要領でテストが書ける。 ただし、今回は簡単のためにソースに対してだけテストを書いていく。

テスト用マクロの入ったパッケージを利用する

先ほどはカラムの値が一意か NULL がないかといったテストを書いた。 ただ、dbt Core が組み込みで用意しているテスト用のマクロは多くない。 早々に「こういうテストが書きたいのに!」という場面が出てくる。 そんなときは、お目当てのテストに使えるマクロが入ったパッケージがないか探してみるのが良い。 dbt には dbt Hub というリポジトリに登録されているパッケージをインストールする仕組みがある。

たとえば dbt-utils というパッケージをインストールしてみよう。 このパッケージはテスト専用ではないものの、テストに使えるマクロがいくつか用意されている。 インストールしたいパッケージは packages.yml という名前の設定ファイルに書く。

$ cat << 'EOF' > packages.yml                
packages:
  - package: dbt-labs/dbt_utils
    version: 0.8.0
EOF

バージョンの指定は必須なので dbt Hub を見て記述する。

hub.getdbt.com

設定ファイルの用意ができたら dbt deps コマンドを実行する。

$ dbt deps

すると、デフォルトで dbt_packages というディレクトリにパッケージがインストールされる。

$ ls dbt_packages 
dbt_utils

このディレクトリには、パッケージに対応するリポジトリの内容がそのままダウンロードされている。 実にシンプルな仕組み。

$ ls dbt_packages/dbt_utils 
CHANGELOG.md        README.md       dbt_project.yml     etc         macros
LICENSE         RELEASE.md      docker-compose.yml  integration_tests   run_test.sh

パッケージをダウンロードできたら、実際に dbt-utils に含まれるテスト用のマクロを使ってみよう。 たとえば accepted_range というマクロを使うとカラムが取りうる値の範囲を指定できる。 以下では tip カラムの値が 0.0 ~ 33.2 になることを確認している。

$ cat << 'EOF' > models/taxis.yml
version: 2

sources:
  - name: public
    tables:
      - name: taxis
        columns:
          - name: tip
            tests:
              - dbt_utils.accepted_range:
                  min_value: 0.0
                  max_value: 33.2
                  inclusive: true
EOF

次のとおり tip カラムの値は 0.0 ~ 33.2 になっている。

$ echo "SELECT MAX(tip) AS max_tip, MIN(tip) AS min_tip FROM public.taxis" | psql -d postgres
 max_tip | min_tip 
---------+---------
    33.2 |       0
(1 row)

テストを実行してみよう。

$ dbt test
14:56:08  Running with dbt=1.0.1
14:56:08  Unable to do partial parsing because a project dependency has been added
14:56:09  Found 0 models, 1 test, 0 snapshots, 0 analyses, 352 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
14:56:09  
14:56:09  Concurrency: 1 threads (target='local')
14:56:09  
14:56:09  1 of 1 START test dbt_utils_source_accepted_range_public_taxis_tip__True__33_2__0_0 [RUN]
14:56:09  1 of 1 PASS dbt_utils_source_accepted_range_public_taxis_tip__True__33_2__0_0... [PASS in 0.03s]
14:56:09  
14:56:09  Finished running 1 test in 0.08s.
14:56:09  
14:56:09  Completed successfully
14:56:09  
14:56:09  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

ちゃんと成功した。

ちなみに、テスト用のマクロが色々と入っているパッケージとしては dbt-expectations というのがある。

hub.getdbt.com

custom generic test を定義する (引数なし)

探しても使えそうなマクロが見つからないときは、独自の generic test を自分で書くこともできる。 公式のドキュメントでは custom generic test と呼んでいる。

試しに、カラムの値が正の値かをテストするマクロを定義してみよう。 前述したとおり、書くのは「失敗するときに一致するレコードが出る SQL」となる。 ただし、純粋な SQL ではなくて Jinja2 というテンプレートエンジンの構文を使って書いていく。

custom generic test を定義するには、tests/generic ディレクトリ以下に SQL を記述する。 以下では is_positive という名前で定義しており、カラムの値が 0 未満のレコードを抽出する。

$ mkdir -p tests/generic 
$ cat << 'EOF' > tests/generic/is_positive.sql 
{% test is_positive(model, column_name) %}

select
  *
from
  {{ model }}
where
  {{ column_name }} < 0

{% endtest %}
EOF

上記のテストを使ってみよう。 passengers カラムの内容をチェックする。

$ cat << 'EOF' > models/taxis.yml
version: 2

sources:
  - name: public
    tables:
      - name: taxis
        columns:
          - name: passengers
            tests:
              - is_positive
EOF

$ dbt test

custom generic test を定義する (引数あり)

先ほどの custom generic test には、追加の引数がなかった。 次は追加の引数があるものを定義してみよう。

以下では、特定の値よりも大きな値がカラムに含まれないことを確認する custom generic test を定義している。 引数にはしきい値を表す value と、境界値を含むかを表したフラグの inclusive をつけている。

$ cat << 'EOF' > tests/generic/max.sql 
{% test max(model, column_name, value, inclusive=true) %}

select
  *
from
  {{ model }}
where
  {{ column_name }} > {{- "=" if inclusive }} {{ value }}

{% endtest %}
EOF

上記を使ってみよう。 以下では passengers の最大値が 6 であることを確認している。 なお、inclusive のデフォルト値は false で上書きしている。

$ cat << 'EOF' > models/taxis.yml
version: 2

sources:
  - name: public
    tables:
      - name: taxis
        columns:
          - name: passengers
            tests:
              - max:
                  value: 6
                  inclusive: false
EOF

テストを実行すると成功する。

$ dbt test
15:27:22  Running with dbt=1.0.1
15:27:22  Found 0 models, 1 test, 0 snapshots, 0 analyses, 354 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
15:27:22  
15:27:22  Concurrency: 1 threads (target='local')
15:27:22  
15:27:22  1 of 1 START test source_max_public_taxis_passengers__False__6.................. [RUN]
15:27:22  1 of 1 PASS source_max_public_taxis_passengers__False__6........................ [PASS in 0.02s]
15:27:22  
15:27:22  Finished running 1 test in 0.06s.
15:27:22  
15:27:22  Completed successfully
15:27:22  
15:27:22  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

ちなみに inclusive オプションを true にしたり、あるいは削ってデフォルト値にするとテストは失敗する。 これは passengers カラムの最大値が 6 のため。

$ echo "SELECT MAX(passengers) FROM public.taxis" | psql -d postgres 
 max 
-----
   6
(1 row)

singular test を使ってテストを書く

generic test は汎用的なテストをマクロとして定義した上で、それを色々な場所から YAML の設定で使うものだった。 一方で、特定の用途に特化したテストを書きたいときもあるはず。 そんなときは単発の SQL を実行するだけの singular test を使うと良い。

singular test を書くときは tests ディレクトリ以下に、直接 SQL のファイルを記述する。 以下では postgres.public.taxispassengers カラムに正の値しかないことを確認するテストを書いている。 ようするに、先ほど引数なしの custom generic test で検証した内容をベタ書きしているだけ。

$ cat << 'EOF' > tests/taxis_passengers_positive.sql 
select
  *
from
  {# sources に定義してあるテーブルの名前を取得できる #}
  {{ source('public', 'taxis') }}
where
  passengers < 0
EOF

純粋に singular test の結果だけ見たいので、先ほどの custom generic test は設定ファイルから削る。

$ cat << 'EOF' > models/taxis.yml                               
version: 2

sources:
  - name: public
    tables:
      - name: taxis
EOF

テストを実行してみよう。

$ dbt test
15:36:50  Running with dbt=1.0.1
15:36:50  Found 0 models, 1 test, 0 snapshots, 0 analyses, 354 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
15:36:50  
15:36:50  Concurrency: 1 threads (target='local')
15:36:50  
15:36:50  1 of 1 START test taxis_passengers_positive..................................... [RUN]
15:36:50  1 of 1 PASS taxis_passengers_positive........................................... [PASS in 0.02s]
15:36:50  
15:36:50  Finished running 1 test in 0.06s.
15:36:50  
15:36:50  Completed successfully
15:36:50  
15:36:50  Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1

ちゃんと実行されて成功したことがわかる。

まとめ

今回は dbt を使ってデータをテストする方法について書いた。

参考

docs.getdbt.com

docs.getdbt.com


  1. 変更したいときはコマンドラインオプションを使って場所を指定できる

  2. タイムゾーンを UTC に変換するやり方は公式ドキュメントでいくつか紹介されている