В прошлом уроке мы изучили четыре встроенных теста. Теперь — стратегия их применения: где какие тесты ставить, чтобы не утонуть в ложных срабатываниях и не пропустить реальные проблемы.
Главный принцип: тесты должны отражать смысловые гарантии модели. Не “повешу все тесты на все колонки”, а “что должно быть истинным для этой колонки в этой модели, и без чего downstream развалится”.
Стратегия по слоям
В стандартной dbt-структуре staging -> intermediate -> marts тесты распределяются по разному.
Staging — фундаментальные проверки на keys и NOT NULL критичных полей. Marts — детальные тесты на бизнес-логику (status enums, foreign keys). Intermediate обычно — без отдельных тестов, доверяем upstream и downstream.
Staging-слой: фундаментальные тесты
Staging — это первая модель над raw. Минимальные тесты:
version: 2
models:
- name: stg_customers
columns:
- name: customer_id
tests:
- not_null
- unique
- name: email
tests:
- not_null # если бизнес гарантирует email
Логика:
- PK всегда: not_null + unique. Если customer_id NULL или дубль — у тебя проблема, которую обязательно нужно отловить максимально рано.
- Критичные NOT NULL. Поля, без которых downstream не работает (email для рассылки, country для биллинга).
- Без accepted_values и relationships. На уровне staging часто ещё нет данных для FK-проверок (другие staging могут идти параллельно). accepted_values имеет смысл, только если в raw гарантированы значения (редко на staging).
Зачем тестировать staging: ловить плохие данные до того, как они попадут в дорогие marts-вычисления. Сломанный loader или ошибка в источнике обнаружится сразу.
Marts-слой: максимальное покрытие
Marts — финальные модели для дашбордов. Тестов больше, проверяем business logic:
version: 2
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- not_null
- unique
- name: country
tests:
- accepted_values:
values: ['RU', 'US', 'DE', 'FR', 'GB', 'JP']
- name: status
tests:
- accepted_values:
values: ['active', 'inactive', 'banned']
- not_null
- name: fact_orders
columns:
- name: order_id
tests:
- not_null
- unique
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: status
tests:
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
- name: amount
tests:
- not_null
Логика:
- Все PK: not_null + unique. То же, что и в staging.
- Foreign keys: not_null + relationships. Гарантируем целостность ссылок.
- Enum-like поля: accepted_values. Status, country, type — везде, где set значений ограничен.
- Measures (amount, count): not_null. Если измерение должно быть числом, NULL ломает агрегации.
Keys vs Measures
Это другая дихотомия — по природе колонки.
Keys — идентификаторы строк или связи между таблицами:
- Primary key — уникален в модели. Тесты: not_null + unique.
- Surrogate key — генерированный (хэш, uuid). Тесты: not_null + unique.
- Foreign key — ссылается на родительскую модель. Тесты: not_null + relationships.
Measures — числовые или enum-значения, которые агрегируются или фильтруются:
- Numeric (amount, count, duration) — обычно not_null. Иногда accepted_range (через dbt_utils).
- Status (enum) — accepted_values.
- Boolean — not_null + accepted_values: [true, false].
- Date/time — not_null. Иногда accepted_range для разумных границ.
Dimensions — характеристики, не агрегируются:
- Categorical (country, type) — accepted_values, если список ограничен.
- Free-form (name, description) — обычно без accepted_values.
Keys — обязательная пара not_null + unique (или not_null + relationships для FK). Measures — accepted_values для enum, not_null для numbers. Dimensions — accepted_values по обстоятельствам.
Антипаттерны: какие тесты НЕ ставить
1. unique на foreign key. Customer_id в orders — у одного клиента много заказов, customer_id повторяется. unique будет провалится каждый раз.
2. not_null на optional поле. deleted_at — есть только у удалённых записей, у активных NULL. not_null будет ругаться на все активные строки.
3. accepted_values на free-form text. Имя клиента, описание товара, address — список не ограничен. accepted_values тут невозможен.
4. relationships между несвязанными таблицами. Если customer_id_alt — это альтернативный ID, который иногда есть, иногда нет — относится к другой таблице, проверка через relationships даст много ложных срабатываний.
5. unique на дублирующихся denormalized моделях. Wide-tables, где одна сущность представлена в нескольких строках (для разных дат снимков), не уникальны по сущности — только по композиции.
Когда тесты на intermediate
Intermediate-слой обычно тестируется минимально:
- Если intermediate — ephemeral, тесты не работают вообще (см. модуль 6).
- Если view/table — можно повесить 1-2 теста на ключевые трансформации, но обычно достаточно тестов на staging (вход) и marts (выход).
Исключение: если intermediate содержит критичную business-логику (например, дедупликация late events), тогда тесты имеют смысл — гарантия, что логика работает.
Тесты на staging для sources
Тесты можно вешать прямо на sources, не дожидаясь staging:
sources:
- name: jaffle_shop
tables:
- name: raw_customers
columns:
- name: id
tests:
- not_null
- unique
dbt test --select source:jaffle_shop запустит. Это полезно, когда хочешь раннее обнаружение проблем в источнике, до построения staging.
Минусы:
- Тесты на raw часто шумные — loader может временно дать неконсистентные данные.
- Логически правильнее тестировать после “очистки” в staging.
Практика: 1-2 критичных теста на source (например, not_null на id), остальное — на staging.
Severity и WARN
Чуть подробнее на это в следующем уроке, но для контекста: каждый тест может быть error (default — блокирует pipeline) или warn (предупреждение, не блокирует).
- name: status
tests:
- accepted_values:
values: ['active', 'inactive']
config:
severity: warn
Используется, когда тест не критичен — например, новое значение status может быть OK, но хочется получить уведомление.
Полный пример: staging + mart
Реалистичный YAML для проекта Jaffle Shop:
version: 2
models:
- name: stg_customers
columns:
- name: customer_id
description: "Unique customer ID from source"
tests:
- not_null
- unique
- name: email
description: "Email address"
tests:
- not_null
- name: stg_orders
columns:
- name: order_id
tests:
- not_null
- unique
- name: customer_id
tests:
- not_null
# без relationships здесь — stg_customers может быть не построен
- name: dim_customers
columns:
- name: customer_id
tests:
- not_null
- unique
- name: fact_orders
columns:
- name: order_id
tests:
- not_null
- unique
- name: customer_id
tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: status
tests:
- not_null
- accepted_values:
values: ['pending', 'shipped', 'delivered', 'cancelled']
- name: amount
tests:
- not_null
Структура понятна: staging — минимальный фундамент, marts — полное покрытие.
Эмпирическое правило: каждая модель должна иметь как минимум один тест на PK (not_null + unique). Это базовый sanity check. Дальше — по обстоятельствам: критичные NOT NULL, foreign keys через relationships, status enums через accepted_values.
Команды для отладки
# Тесты только для staging
dbt test --select staging
# Тесты для одной модели
dbt test --select fact_orders
# Тесты по тэгу (если расставил tags на тесты)
dbt test --select tag:critical
Если тест провалился — открой target/compiled/<project>/...<test_name>.sql, выполни в DuckDB, посмотри что вернулось — это нарушители.
Попробуй сам
Создай _models.yml для своего проекта с приоритетом покрытия:
- PK на каждой модели — not_null + unique.
- FK на каждой fact-модели — not_null + relationships.
- Status на каждой fact — accepted_values.
- Critical NOT NULL — email, amount, дата.
Запусти dbt test. Посмотри время выполнения. Если упадёт — открой compiled-файл теста, разберись, что нарушает.
Что мы поняли
Стратегия применения тестов: staging — фундамент (not_null + unique на PK), marts — максимальное покрытие (все 4 типа). По типу колонки: keys — not_null + unique (PK) или not_null + relationships (FK); measures — accepted_values для enum, not_null для numbers; free-form — обычно без тестов. Главное — тесты должны отражать смысловые гарантии, не be everywhere. Антипаттерны: unique на FK, not_null на optional, accepted_values на free-form.
В следующем уроке разберём severity — как блокировать или предупреждать через тесты.
Шесть измерений data quality