데이터 테스트

데이터 테스트 (Data tests)

**데이터 테스트(data test)**는 모델·소스·시드·스냅샷 같은 dbt 리소스에 대해 세우는 단언(assertion)이에요. dbt test를 실행하면 프로젝트 안의 각 테스트가 통과했는지 실패했는지를 알려주죠. 테스트라는 이름이 단위 테스트와 구분되도록 이제 데이터 테스트로 불러요.

데이터 테스트는 본질적으로 SQL 쿼리예요. 특히 '실패하는 레코드'를 찾는 select 문이죠. 특정 컬럼이 유일하다고 단언하면 중복을 찾고, null이 아니다고 단언하면 null을 찾아요. 그리고 그 테스트가 실패 행을 0개 돌려주면 통과, 즉 단언이 검증된 거예요.

출처: Add data tests to your DAG

개요

데이터 테스트는 각 모델의 SQL 무결성을 높이는 데 써요. 기본적으로 특정 컬럼이 null이 아닌 값만 가지는지, 유일한 값인지, 다른 모델의 값과 대응하는지(예: orderscustomer_idcustomers 모델의 id와 대응), 지정한 목록의 값인지 등을 검사할 수 있어요. 모델에 대해 select 쿼리 형태로 만들 수 있는 단언이라면 무엇이든 데이터 테스트로 바꿀 수 있답니다.

데이터 테스트를 정의하는 방법은 두 가지예요.

  • 단일(singular) 데이터 테스트: 실패 행을 반환하는 SQL 쿼리를 직접 작성해 테스트 디렉토리의 .sql 파일로 저장하는 방식이에요. 이제 그 자체가 데이터 테스트가 되어 dbt test 명령으로 실행돼요.
  • 일반(generic) 데이터 테스트: 인자를 받는 매개변수화된 쿼리로, 특별한 test 블록(매크로처럼)에 정의해요. 정의한 뒤 .yml 파일에서 이름으로 참조할 수 있고, 모델·컬럼·소스·스냅샷·시드에 적용할 수 있어요. dbt는 네 가지 일반 데이터 테스트를 기본 내장하고 있어요.

단일 데이터 테스트

가장 단순한 정의는 실패 행을 반환하는 정확한 SQL을 작성하는 거예요. '단일' 테스트라고 부르는 건 한 가지 목적을 위한 일회성 단언이기 때문이에요. 보통 tests 디렉토리(test-paths 설정으로 정의)의 .sql 파일에 넣고, 파일 하나에 select 문 하나가 곧 테스트 하나예요. 테스트 정의 안에서 모델을 만들 때처럼 Jinja(ref·source 포함)를 쓸 수 있어요.

-- Refunds have a negative amount, so the total amount should always be >= 0.
-- Therefore return records where total_amount < 0 to make the test fail.
select
    order_id,
    sum(amount) as total_amount
from {{ ref('fct_payments') }}
group by 1
having total_amount < 0

테스트 이름은 파일 이름(assert_total_payment_amount_is_positive)이 돼요. 참고로 SQL 끝의 세미콜론(;)은 데이터 테스트 실패의 원인이 될 수 있으니 생략해야 하고, tests 디렉토리의 단일 데이터 테스트는 dbt test 실행 시 자동으로 실행돼요. model_name.yml에는 참조하지 말아요.

단일 데이터 테스트에 설명을 붙이려면 tests/schema.yml 같은 .yml 파일을 추가하면 돼요.

data_tests:
  - name: assert_total_payment_amount_is_positive
    description: >
      Refunds have a negative amount, so the total amount should always be >= 0.
      Therefore return records where total amount < 0 to make the test fail.

일반 데이터 테스트

일반 데이터 테스트는 재사용할 수 있어요. test 블록에 매개변수화된 쿼리를 정의하고 인자를 받죠. 예를 들면 다음과 같아요.

{% test not_null(model, column_name) %}

    select *
    from {{ model }}
    where {{ column_name }} is null

{% endtest %}

modelcolumn_name 두 인자가 쿼리에 템플릿으로 들어가요. 그래서 이 테스트는 어떤 모델의 어떤 컬럼에도 얼마든지 정의할 수 있고, dbt가 그에 맞게 값을 전달하죠. 정의한 뒤에는 해당 리소스가 있는 디렉토리의 .yml 파일에서 속성(property)으로 추가하면 돼요.

dbt는 기본으로 unique, not_null, accepted_values, relationships 네 가지 일반 데이터 테스트를 제공해요. orders 모델에 적용한 전체 예시를 보면 다음과 같아요.


models:
  - name: orders
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null
      - name: status
        data_tests:
          - accepted_values:
              arguments: # available in v1.10.5 and higher. Older versions can set the <argument_name> as the top-level property.
                values: ['placed', 'shipped', 'completed', 'returned']
      - name: customer_id
        data_tests:
          - relationships:
              arguments:
                to: ref('customers')
                field: id

이를 말로 풀면 이렇게 돼요.

  • unique: orders 모델의 order_id 컬럼은 유일해야 한다.
  • not_null: orders 모델의 order_id 컬럼은 null을 포함하지 않아야 한다.
  • accepted_values: orders 모델의 status 컬럼은 'placed', 'shipped', 'completed', 'returned' 중 하나여야 한다.
  • relationships: orders의 각 customer_idcustomers 테이블의 id로 존재해야 한다(참조 무결성).

dbt는 내부적으로 각 데이터 테스트에 대해 select 쿼리를 만들고, 단언이 참이 아닌 행을 반환해요. 그래서 0행이 반환되면 단언이 통과하는 거예요.

모델 디렉토리에 models/schema.yml을 추가하고 dbt test를 실행하면 검사가 돌아가요.


models:
  - name: orders
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null
$ dbt test

Found 3 models, 2 tests, 0 snapshots, 0 analyses, 130 macros, 0 operations, 0 seed files, 0 sources

17:31:05 | Concurrency: 1 threads (target='learn')
17:31:05 |
17:31:05 | 1 of 2 START test not_null_order_order_id..................... [RUN]
17:31:06 | 1 of 2 PASS not_null_order_order_id........................... [PASS in 0.99s]
17:31:06 | 2 of 2 START test unique_order_order_id....................... [RUN]
17:31:07 | 2 of 2 PASS unique_order_order_id............................. [PASS in 0.79s]
17:31:07 |
17:31:07 | Finished running 2 tests in 7.17s.

Completed successfully

Done. PASS=2 WARN=0 ERROR=0 SKIP=0 TOTAL=2

실패 저장하기 (store failures)

보통 데이터 테스트 쿼리는 실행 중에 실패를 계산해요. 선택적으로 --store-failures 플래그나 store_failures, store_failures_as 설정을 지정하면, dbt가 테스트 쿼리 결과를 먼저 데이터베이스의 테이블에 저장한 뒤 그 테이블을 조회해 실패 개수를 계산해요. 개발 중에 실패 레코드를 훨씬 빠르게 조회하고 검토할 수 있게 돼요.

주의할 점이 몇 가지 있어요. 테스트 결과 테이블은 기본적으로 dbt_test__audit 접미사가 붙은 스키마에 만들어지고(스키마 설정으로 변경 가능), 한 테스트의 결과는 항상 같은 테스트의 이전 실패를 대체해요.

data_tests: 문법

데이터 테스트는 역사적으로 dbt에서 유일한 테스트 형태였기 때문에 '테스트(tests)'로 불렸어요. 유닛 테스트가 도입되면서 키가 tests:에서 data_tests:로 바뀌었죠. dbt는 여전히 호환성을 위해 YAML 설정 파일에서 tests:를 지원하지만, 같은 리소스에 testsdata_tests 키를 동시에 쓸 수는 없어요.

더 알아보기 (Learn more)