В прошлом уроке мы разобрали, какие тесты вешать где. Теперь — как контролировать поведение при провале. Не каждый failed test одинаково критичен. Один обязательно блокирует production-deploy, другой — просто шум, на который стоит обратить внимание.
Этим управляет параметр severity.
Базовая идея
Каждый тест в dbt может быть одного из двух уровней:
error(default) — провал блокирует pipeline. Exit code != 0, downstream не выполняется вdbt build. CI-job падает.warn— провал показывается как warning, но не останавливает. Exit code остаётся 0.
error — твёрдая граница, провал блокирует. warn — мягкое предупреждение, информирует, но не останавливает. Выбор зависит от того, насколько критичен инвариант.
Синтаксис
В YAML severity указывается через config:
models:
- name: fact_orders
columns:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
config:
severity: warn
Или явно error (это default):
- name: customer_id
tests:
- not_null:
config:
severity: error
Когда severity: warn
Несколько типичных сценариев:
1. Новые enum-значения. Если в status может появиться новое значение (например, добавили ‘refunded’), и ты пока не уверен, что добавлять — severity: warn позволяет узнать о появлении нового, не блокируя pipeline.
2. Постепенная миграция. Когда добавляешь тест, который сейчас провалится из-за legacy данных. Сначала severity: warn — фикси и постепенно убирай нарушители. Потом меняешь на error.
3. Soft business rules. “Хорошо бы amount был > 0, но иногда returns/refunds могут давать отрицательные”. Warn информирует, error блокирует ошибочно.
4. Информационные проверки. “Проверь, что customer_segment не NULL” — было бы хорошо, но не критично для downstream.
Когда severity: error (всегда default)
- PK тесты — not_null + unique. Если они провалились, у тебя сломанные данные.
- Foreign keys — relationships. Сломанный FK ломает JOIN в дашбордах.
- Критичные NOT NULL — без email клиента уведомления не отправятся.
error_if и warn_if: условные пороги
dbt позволяет настроить порог нарушителей, при котором тест меняет severity. По умолчанию любая строка-нарушитель = FAIL. С error_if можно сказать: “если меньше 100 нарушителей — WARN, если больше 100 — ERROR”.
Синтаксис:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
config:
severity: warn
error_if: ">100" # ERROR если 100+ нарушителей
warn_if: ">0" # WARN если хоть один (это default для warn)
Логика порогов:
- dbt выполняет тест -> получает count нарушителей.
- Если count > error_if -> ERROR.
- Иначе если count > warn_if -> WARN.
- Иначе -> PASS.
error_if/warn_if полезны для тестов на больших таблицах, где небольшое количество нарушителей — terpимо (например, race condition в loader даёт 1-2 строки с invalid status). Жёсткий error на каждое сломанное событие — overkill. Threshold позволяет получать алерт только при больших проблемах.
Конкретный пример порогов
Сценарий: у тебя fact_events с миллиардом строк. Иногда (1-2 раза в сутки) loader даёт строку с UNKNOWN status. Не критично, но хочется мониторить тренд.
- name: status
tests:
- accepted_values:
values: ['login', 'logout', 'click', 'view']
config:
severity: error
error_if: ">1000" # критично, если 1000+
warn_if: ">10" # info, если 10+
Поведение:
- 0-10 нарушителей: PASS — нормальная погрешность.
- 11-1000: WARN — обрати внимание, но pipeline идёт.
- 1000+: ERROR — что-то серьёзное сломалось, останавливаем.
Severity в зависимости от среды
Иногда нужно жёстче в prod, мягче в dev. dbt поддерживает условные конфиги через target:
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_id
config:
severity: '{{ "error" if target.name == "prod" else "warn" }}'
В prod — error (блокирует). В dev — warn (не блокирует, чтобы не мешать разработке).
Реализуется через target.name и Jinja-условие. Подробнее в модуле про variables/env.
Output dbt test с разной severity
Когда запустишь dbt test, увидишь разные статусы:
14:23:01 1 of 4 START test not_null_fact_orders_order_id ........ [RUN]
14:23:01 2 of 4 START test unique_fact_orders_order_id .......... [RUN]
14:23:01 3 of 4 START test relationships_fact_orders_customer_id . [RUN]
14:23:01 4 of 4 START test accepted_values_fact_orders_status ... [RUN]
14:23:02 1 of 4 PASS not_null_fact_orders_order_id ............... [PASS in 0.18s]
14:23:02 2 of 4 PASS unique_fact_orders_order_id ................. [PASS in 0.22s]
14:23:02 3 of 4 WARN 3 relationships_fact_orders_customer_id ..... [WARN 3 in 0.31s]
14:23:02 4 of 4 FAIL 1 accepted_values_fact_orders_status ........ [FAIL 1 in 0.18s]
Done. PASS=2 WARN=1 ERROR=1 SKIP=0 TOTAL=4
- PASS — тест прошёл (0 нарушителей).
- WARN 3 — тест warn-уровня нашёл 3 нарушителя, pipeline продолжается.
- FAIL 1 — тест error-уровня нашёл 1 нарушителя, pipeline останавливается.
Exit code: при наличии хотя бы одного FAIL = 1. При наличии только WARN = 0.
Severity на уровне модели/проекта
Можно задать дефолтный severity для всего проекта через dbt_project.yml:
tests:
+severity: warn
Или для тестов конкретной модели:
models:
- name: experimental_mart
config:
tests:
+severity: warn
Используется реже, но полезно когда есть экспериментальная модель и не хочешь, чтобы её тесты блокировали main pipeline.
Custom thresholds через config
В dbt-core 1.6+ можно использовать warn_after и error_after (не путать с freshness). Синтаксис:
- name: status
tests:
- accepted_values:
values: ['active', 'inactive']
config:
warn_if: ">1"
error_if: ">100"
Это equivalent error_if/warn_if. Главное — string-comparison, чтобы dbt мог парсить число.
Best practices
1. По умолчанию — error. На PK, FK, критичных NOT NULL всегда error.
2. Warn для миграционных моментов. Когда внедряешь новый тест на legacy данных — сначала warn, постепенно фикси и переключай на error.
3. Threshold для шумных таблиц. На больших fact-таблицах с миллиардом событий и редкими аномалиями — error_if: “>N”.
4. Не злоупотребляй warn. “Все тесты warn, чтобы не мешали разработке” — это плохо. Через месяц никто не смотрит на warnings, и реальные проблемы прячутся.
5. Reviewing warns. В каждом sprint смотри dbt test output, разбирайся с warnings. Либо фикси, либо упрощай тест.
DuckDB-специфика для severity
Severity никак не зависит от warehouse — это чисто dbt-механика. На DuckDB работает так же, как везде. Exit code от dbt test одинаков на любом адаптере.
Команды для проверки severity
# Список тестов с конфигом
dbt list --resource-type test --output json | jq '.[] | {name, config: .config}'
# Тесты только определённого severity
dbt test --select "config.severity:warn"
Попробуй сам
Возьми существующий тест, например на accepted_values:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped']
Запусти dbt test — если data содержит ‘cancelled’, тест FAIL’нет.
Добавь severity: warn:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped']
config:
severity: warn
Снова dbt test. Теперь будет WARN, exit code 0.
Затем добавь threshold:
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped']
config:
severity: error
warn_if: ">0"
error_if: ">10"
Если нарушителей 5 — WARN. Если 15 — ERROR. Проверь, добавив тестовые данные.
Что мы поняли
severity управляет поведением проваленного теста: error (default) — блокирует pipeline, exit 1; warn — предупреждение, не блокирует. error_if/warn_if задают пороги, при которых severity меняется (например, error только если 100+ нарушителей). Использовать warn для миграционных моментов, мягких правил, информационных проверок. По умолчанию — error на критичных тестах. Не злоупотреблять warn, иначе они станут невидимыми.
В следующем уроке разберём store_failures — как dbt сохраняет failed строки для дебага.
Severity и thresholds: продвинутые настройки