Тестирование данных
Помимо разработки, одной из важнейших задач при работе с хранилищами данных является обеспечение качества и целостности данных. Для этого проводится тестирование, при котором выявляют аномалии и ошибки.
Понятно, что данные можно проверять точечно и вручную, но автоматизированое тестирование позволяет сократить количество ошибок, укрепить доверие к данным и сократить трудозатраты разработчиков и аналитиков на эту операцию.
В данном разделе рассмотрим возможности dbt™ в части тестирования.
Тестирование в dbt™ – это выполнение определенных настроек в моделях, а также в других объектах проекта (источниках, CSV-файлах, «снэпшотах»), которые позволяют проверить целостность данных, а затем сообщить о результатах этих проверок.
В dbt™ тесты являются SQL-запросами, направленными на поиск «ошибочных» записей.
Например, если в настройках указано, что столбец (или поле) модели является уникальным, то тестовый запрос выбирает дубликаты. Если, согласно настройкам, столбец никогда не бывает нулевым, то тест ищет записи с нулевым значением этого столбца. Другими словами, если тестовый запрос не возвращает никаких записей, то тест считается успешно пройденным.
Данные в dbt™ можно протестировать с помощью встроенной функциональности, а также с применением дополнительных библиотек («пакетов»).
В dbt™ существует два типа тестов:
тесты данных (data tests), которые помогают обеспечить качество данных;
модульные тесты (unit tests, возможно, удобнее их называть просто юнит-тесты), которые проверяют заложенную в модели логику.
Рассмотрим каждый из них подробнее.
Тесты данных (data tests)
Тесты данных (data tests) предназначены для проверки целостности данных при каждом запуске проекта.
При этом тесты на уровне данных бывают единичными (singular tests) и универсальными (generic tests).
Единичные тесты (singular tests)
Возможно, по-русски звучит как-то криво, но смысл названия в том, что этот тип тестов используется для какой-то одной конкретной цели. Он достаточно прост и содержит один единственный SELECT, который возвращает «ошибочные» записи. В этих тестах можно использовать функциональность Jinja, как в dbt-моделях.
Единичные тесты располагаются в папке tests и выполняются автоматически при запуске команды dbt test.
Какие-то строгие правила наименования здесь не применяются. Главное, чтобы название теста отражало суть выполняемых в нем действий.
Итак, давайте напишем первый незамысловатый тест - проверим, что в данных отсутствуют отрицательные суммы по оплате.
Создайте в папке tests файл check_payment_total_amount_is_positive.sql и добавьте в него следующий код:
select payment_id, sum(amount) from {{ ref('fct_payments') }} group by 1 having sum(amount) < 0
Не ставьте точку с запятой (;) в конце select единичного теста, иначе он свалится в ошибку.
Так как с помощью тестов мы хотим упростить себе жизнь, то этих тестов может быть достаточно большое количество. Да, ранее было отмечено, что название теста должно отражать его суть, но дополнительные уточняющие комментарии не помешают, чтобы не запутаться. Для этого применяются настроечные YAML-файлы.
Создайте в папке tests файл description.yml и добавьте в него следующее содержимое:
Название настроечного YAML-файла с комментариями может быть совершенно любым. Здесь самое главное, чтобы наименования тестов соответствовали наименованиям SQL-файлов с логикой тестов.
version: 2 data_tests: - name: check_payment_total_amount_is_positive description: > Оплата за аренду автомобиля всегда имеет положительное значение (>= 0). Выборка записей, в которых общая сумма < 0, завершит тест с ошибкой.
Пройдемся по параметрам файла:
version: 2. Эта настройка была обязательной для версии dbt™ 1.4 и ниже. Сейчас она не обязательна, но все-таки разработчики рекомендуют оставить ее;data_testsговорит о том, что в файле перечислены единичные тесты с их описанием:name- наименование теста (должно совпадать с наименованием SQL-файла),description- описание тестового сценария.
Знак > в description применяется для разбиения текста на несколько строк. Если этот знак не указать, то текст описания будет отображаться в одну строку.
Осталось запустить тест и убедиться, что данные в порядке, а заодно и ознакомиться с новой командой.
Запустите команду тестирования:
dbt test
Подобные тесты конечно хороши, но неудобны тем, что написаны под конкретную модель и даже столбец. Но вполне может такое быть, что логика проверки одной модели аналогична и для другой. Разница будет только в названии этой самой модели и имени столбцов. В этом случае можно с уверенностью сказать, что такой тест уже не будет единичным. Для таких целей нужно использовать универсальные тесты данных (generic tests).
Универсальные тесты (generic tests)
Встроенные универсальные тесты (build-in generic tests)
Прямо из коробки dbt™ предоставляет возможность использования четырех встроенных универсальных тестов:
unique- проверка на отсутствие дубликатов. Как правило, эти тесты применяются к столбцам, которые являются идентификаторами записей;not_null- проверка на отсутствие нулевых значений, чтобы гарантировать наличие важной, с точки зрения аналитики, информации. Также может применяться в сочетании сuniqueк столбцам с идентификаторами;accepted_values- проверка допустимых значений по бизнес-логике в определенных столбцах для обеспечения согласованности данных и контроля ввода недопустимых вариантов;relationships- проверка связи внешних ключей между двумя таблицами.
Добавьте перечисленные универсальные тесты для таблиц фактов и измерений.
Сначала создайте в папке models/marts/core/ файл _dimensions_tests.yml и добавьте в него следующий код:
version: 2 models: - name: dim_customers columns: - name: customer_id tests: - unique - not_null - name: dim_cars columns: - name: car_id tests: - unique - not_null - name: category_text tests: - accepted_values: values: ['business', 'cargo', 'comfort', 'electro', 'standart'] - name: dim_calendar columns: - name: date_id tests: - unique - not_null
Теперь создайте в папке models/marts/finance/ файл _facts_tests.yml и добавьте в него следующий код:
version: 2 models: - name: fct_payments columns: - name: payment_id tests: - unique - not_null
Еще раз запустите тесты и убедитесь, что с данными всё в порядке:
dbt test
Пользовательские универсальные тесты (custom generic tests)
Универсальные тесты по своей сути повторяют функциональность dbt-макросов с параметрами, что позволяет применять одни и те же тесты к различным моделям. Встроенные тесты расположены «под капотом», поэтому это явно не видно. Подход тот же самый. Но в отличие от макроса, код-блок которого начинается с ключевого слова macro, универсальные тесты начинаются ключевого слова test.
Ровно так же как и макросы, можно создавать пользовательские тесты, если недостаточно функциональности встроенных или же требуется применить определенную логику к нескольким объектам сразу.
Ранее вы внедрили проверку данных на отсутствие отрицательных сумм по оплате. Логика была применена к конкретной модели и конкретному столбцу этой модели. Но представьте, что есть и другие подобные модели, к которым тоже нужно применить такую же проверку. В этих случаях очень удобно использовать пользовательские тесты.
Итак, создайте в новой папке macros/custom_tests файл check_positive_values.sql со следующим кодом:
Как видно тест принимает в качестве параметров имя модели и столбца с показателем.
Перейдите в папку models/marts/finance/ , откройте настроечный файл _facts_tests.yml и добавьте в него проверку столбца amount:
version: 2 models: - name: fct_payments columns: - name: payment_id tests: - unique - not_null - name: amount tests: - check_positive_values
Запустите проверку конкретной модели fct_payments, для которой применим только что добавленный пользовательский тест (а также созданный ранее единичный тест). Делается это с помощью той же команды dbt test, но с указанием имени модели:
dbt test --select fct_payments
dbt предоставляет также возможность отдельного запуска единичных (singular) и универсальных (generic) тестов. Для этого в команде запуска укажите тип тестов:
dbt test --select "test_type:singular"
dbt test --select "test_type:generic"
Модульные тесты (unit tests)
Модульные или юнит-тесты (unit tests) предназначены для проверки логики моделей с помощью небольшого, заранее определенного, набора фиктивных (тестовых) данных (в отличие от data tests, которые выполняются на данных, загруженных в платформу).
Некоторые случаи, в которых рекомендовано использовать юнит-тесты:
регулярные выражения;
математические операции с датами и временем (date math);
оконные функции;
условная логика (множественное использование
whenвcase);выявленные ранее ошибки логики;
случаи, которые еще не встречались в транзакционных данных и которые нужно обработать.
Подготовка к модульному тесту
Представьте, что у бизнеса возникла потребность дополнительной аналитики, которой не было в исходных данных. Из этой потребности сформировалась техническая задача по добавлению в какую-то существующую модель нового столбца. Например, нужно добавить классификацию притока денежных средств на основе суммы оплаты (amount). Перед применением новой логики в продуктивной среде, юнит-тесты помогут проверить правильность этой логики.
Вот упрощенная логика классификации притока денежных средств за пользование автомобилями компании:
небольшие денежные поступления (small): до 1500 рублей;
средние денежные поступления (medium): от 1500 до 3000 рублей;
крупные денежные поступления (large): более 3000 рублей.
Внесите дополнения по указанной выше логике в модель fct_payments.sql. Откройте данный файл и замените его код на следующий (или просто добавьте строки, которые относятся к новой логике):
with payments as ( select payment_id, created_at, customer_id, car_id, amount, case when amount < 1500 then 'small' when amount between 1500 and 3000 then 'medium' else 'large' end as cash_inflows from {{ ref('int_payments_joined_to_bookings') }} ) select * from payments
Настройка модульных тестов
Создайте в папке models/marts/finance/ файл _finance_unit_tests.yml и добавьте в него следующий код:
unit_tests: - name: test_cash_inflows_logic description: "Проверка логики классификации притока денежных средств каршеринга." model: fct_payments given: - input: ref('int_payments_joined_to_bookings') rows: - {amount: 4068.33} - {amount: 2857.50} - {amount: 1465.83} expect: rows: - {amount: 4068.33, cash_inflows: 'large'} - {amount: 2857.50, cash_inflows: 'medium'} - {amount: 1465.83, cash_inflows: 'small'}
Ознакомьтесь построчно с фалом теста:
unit_tests: определяет тип dbt-теста;nameиdescription: наименование и описание теста (как и в предыдущих примерах);model: наименование модели, для которой выполняется проверка;given: блок модульного теста, в котором указываются данные для тестирования;input: наименование модели-источника;rows: исходные записи (строки) для проведения тестирования;
expect: блок модульного теста, в котором указывается ожидаемый результат;rows: целевые записи (строки), которые ожидаются после выполнения теста.
Запустите модульный тест для конкретной модели из примера (fct_payments):
dbt test --select "fct_payments,test_type:unit"
Тесты прошли успешно. Полученные результаты соответствуют ожидаемым, что говорит о правильной работе заданной логики. Но точно ли всё работает так, как задумано?
Вернитесь к файлу _finance_unit_tests.yml и внесите изменение в одну из ожидаемых строк {amount: 4068.33, cash_inflows: 'large'} - замените значение для cash_inflows на small:
unit_tests: - name: test_cash_inflows_logic description: "Проверка логики классификации притока денежных средств каршеринга." model: fct_payments given: - input: ref('int_payments_joined_to_bookings') rows: - {amount: 4068.33} - {amount: 2857.50} - {amount: 1465.83} expect: rows: - {amount: 4068.33, cash_inflows: 'small'} - {amount: 2857.50, cash_inflows: 'medium'} - {amount: 1465.83, cash_inflows: 'small'}
Повторно запустите тест:
dbt test --select "fct_payments,test_type:unit"
Тест завершился с ошибкой, так как результат не совпадает с ожиданием. Для записи с суммой оплаты в размере 4068.33 рублей согласно классификации должно было определиться крупное денежное поступление (large). Смоделированная ситуация вновь подтвердила корректность работы модульного теста. Верните значение для cash_inflows (large) и сохраните тест.
Таким образом, на простом примере вы рассмотрели принцип выполнения модульных тестов (unit tests), а также познакомились с единичными (singular) и универсальными (generic) тестами данных (data tests).
Сохранение проекта в GitHub
Загрузите текущее состояние dbt-проекта в GitHub-репозиторий.
Добавьте все файлы проекта:
git add .
Добавьте сообщение для коммита:
git commit -m "data-tests-and-unit-tests"
Отправьте локальный проект в репозиторий GitHub:
git push
Теперь актуальный код проекта хранится в GitHub-репозитории.