сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · ETL и оркестрация

Тесты пайплайнов данных

Тестирование пайплайнов

Пайплайн данных тестируют не так, как думают новички: не гоняя его целиком в прод-БД, а отделив чистую логику от I/O. Собес проверяет, умеешь ли ты сделать трансформации тестируемыми и покрыть края, а не только happy path.

Стержень: логика трансформации - чистые функции на маленьких данных, I/O мокается; данные проверяются и в рантайме, не только в тестах.

// Формулировки: «как тестируешь пайплайн?», «что мокаешь?», «какие края проверяешь?».

Отделить логику от I/O

Первый принцип - вынести чистую логику трансформации из чтения-записи. Тогда трансформации тестируются юнитами на маленьких списках или DataFrame, а I/O живёт в тонком слое и мокается. Логика, переплетённая с чтением и записью в одной функции, нетестируемый монолит.

Тест трансформации это рукотворный вход и ожидаемый выход, обязательно включая края: пустой вход, NULL и пропуски, дубли, одна строка, битые значения.

// Тестируют не только happy path, но и обработку сбоев: что делает пайплайн на таймауте, битой записи, пустом ответе. Поведение под сбоем - тоже часть контракта, и именно на нём прод падает первым.

чистая функция
трансформация без I/O - тестируется юнитом
края (edge cases)
пусто, NULL, дубли, одна строка, битое

Инструменты и моки

pytest даёт нужное: fixtures готовят данные, parametrize прогоняет матрицу входов-ожиданий одним тестом, tmp_path - для файлов, conftest.py шарит фикстуры между тестами.

Внешние системы мокают: responses или monkeypatch для API, sqlite или testcontainers для БД. Юнит-тесты не должны ходить в сеть и прод, иначе они флаки, медленные и зависят от чужого состояния.

// Для DataFrame сравнивают через assert_frame_equal из pandas.testing - с допуском по float и понятным диффом, а не ручным df1.equals(df2), который на несовпадении молчит про причину. И float сравнивают через approx, не ==.

parametrize
один тест × таблица входов-ожиданий
testcontainers
реальные сервисы в докере на время теста

Golden-тесты и рантайм-контракты

Golden-тесты: эталонный вход и зафиксированный выход целого шага. Они ловят незапланированные изменения поведения при рефакторинге - вывод «поехал», тест краснеет.

Но тесты проверяют код на известных данных, а прод получает неизвестные. Поэтому данные проверяют и в рантайме: контракты на схему и инварианты (not null, уникальность ключа, диапазоны, свежесть) на входе и выходе шага.

// Классическая дыра - зелёные тесты на схему, которую прод уже не шлёт: тесты проверяют вчерашний контракт, а источник поменялся. Контракты обязаны жить и в рантайме.

golden test
сравнение с зафиксированным эталонным выходом
data contract
формальные ожидания к схеме и инвариантам данных

Как отвечать: «Как тестировать пайплайн данных?»

Начинаю с того, что отделяю чистую логику трансформации от ввода-вывода. Трансформации делаю функциями, которые принимают данные и возвращают данные, без чтения и записи внутри, их я покрываю юнит-тестами на маленьких рукотворных входах. И тестирую не только счастливый путь, а именно края: пустой вход, NULL, дубли, одна строка, битые значения - прод падает как раз на них. Внешние системы мокаю: API через responses, базу через sqlite или testcontainers, чтобы тесты не ходили в сеть и не были флаки. Для целых шагов держу golden-тесты - эталонный вход и зафиксированный выход, они ловят регресс при рефакторинге. И, что важно, не полагаюсь только на тесты: прод получает неизвестные данные, поэтому в рантайме проверяю контракты на схему и инварианты на входе и выходе, потому что источник меняется, а тесты знают лишь вчерашний формат.

Почему это сильный ответ: главный принцип (логика без I/O), акцент на краях и сбоях, правильные моки, golden-тесты и - ключевое - рантайм-контракты поверх тестов; видно, что человек ловил прод-сюрпризы.

На чём валят

  • Тестировать только happy path - прод падает на первом NULL.
  • Тесты, ходящие в реальное API/БД - флаки, медленно, зависят от чужого стейта.
  • Сравнение float через == вместо approx/assert_frame_equal.
  • Логика внутри функции с чтением/записью - нетестируемый монолит; раздели.
  • Зелёные тесты на схему, которой прод уже не шлёт - контракты живут в рантайме тоже.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.

  1. #testing_pipelines1 / 5
    Зачем писать автоматические тесты для data-пайплайна?
    A)Автотесты для data-пайплайна не нужны, ведь входные данные и так всё время меняются
    B)Достаточно один раз глазами посмотреть на выгрузку и больше её не проверять
    C)Тесты пайплайна замеряют скорость его работы под нагрузкой
    D)Логика трансформаций — код; тесты ловят регресс при правках
    показать ответ и разбор
    +D)Логика трансформаций — код; тесты ловят регресс при правках

    // разбор: Логика трансформаций — это код, и он ломается при правках, как любой другой. Тест на фиксированных входных данных с ожидаемым выходом фиксирует поведение и ловит регресс при рефакторинге или добавлении фич. Это отдельно от проверок качества данных (те валидируют текущие данные, тесты — логику кода).

  2. #testing_pipelines2 / 5
    Почему юнит-тест трансформации не должен ходить в боевую БД или сеть?
    A)Тест должен быть детерминирован и быстр — фикстуры и моки, а не прод
    B)Юнит-тест должен ходить в боевую продовую базу, иначе он проверяет не реальное поведение
    C)Тесты специально гоняют по сети, чтобы заодно нагрузить внешние сервисы
    D)Детерминированность теста в дата-инженерии не имеет вообще никакого значения
    показать ответ и разбор
    +A)Тест должен быть детерминирован и быстр — фикстуры и моки, а не прод

    // разбор: Поход в боевую БД или сеть делает тест медленным, недетерминированным (данные меняются, сервис флапает) и небезопасным. Логику проверяют на маленьких фикстурах с известным входом и ожидаемым выходом, а внешние вызовы подменяют моками. Тест обязан давать один и тот же результат при каждом запуске.

  3. #testing_pipelines3 / 5
    Почему тестировать пайплайн только на «счастливом» датасете недостаточно?
    A)Достаточно протестировать пайплайн на одном чистом «счастливом» датасете
    B)Проверять и краевые случаи: пустой вход, дубли, null, битые типы
    C)Краевые случаи в данных на проде встречаются редко, ими можно пренебречь
    D)Тестировать трансформации на реальных данных бессмысленно
    показать ответ и разбор
    +B)Проверять и краевые случаи: пустой вход, дубли, null, битые типы

    // разбор: Happy path прячет ровно те баги, что взрываются в проде: пустая партиция, дубли ключей, null в обязательном поле, неожиданный тип, смена схемы. «Золотой» датасет с ожидаемым выходом плюс явные edge-кейсы дают уверенность, что трансформация не молча портит данные на нестандартном входе.

  4. #testing_pipelines4 / 5
    Зачем валидировать данные схемой (pydantic/Great Expectations), а не только тестировать код?
    A)Юнит-тесты самого кода заодно обеспечивают корректность входных данных пайплайна
    B)Код может быть верным, а данные — грязными; валидация ловит нарушения контракта в рантайме
    C)Валидация данных заменяет собой юнит-тесты логики трансформаций
    D)Схема данных нужна для ускорения работы пайплайна на больших объёмах
    показать ответ и разбор
    +B)Код может быть верным, а данные — грязными; валидация ловит нарушения контракта в рантайме

    // разбор: Тесты кода проверяют логику на фиксированных примерах, но ничего не говорят о реальных данных, что придут в проде. Валидация схемой в рантайме проверяет фактический поток — типы, диапазоны, обязательность, уникальность — и ловит битый источник до того, как он отравит витрину. Это дополняет юнит-тесты, а не заменяет их.

  5. #testing_pipelines5 / 5
    Как тестировать функцию, которая ходит во внешний API, не вызывая его по-настоящему?
    A)Вызывать боевой внешний API прямо изнутри теста, чтобы проверка была честнее и ближе к продовому поведению
    B)Удалить сетевой код из функции на время прогона тестов
    C)Подменить вызов моком/стабом, возвращающим заранее заданный фиксированный ответ
    D)Запускать такой тест вручную раз в месяц по расписанию
    показать ответ и разбор
    +C)Подменить вызов моком/стабом, возвращающим заранее заданный фиксированный ответ

    // разбор: Боевой вызов делает тест медленным, недетерминированным и небезопасным. Внешний вызов подменяют моком/стабом, возвращающим фиксированный ответ, — тогда логика функции проверяется изолированно и воспроизводимо. Моком удобно и смоделировать сбой (таймаут, 500), чтобы проверить, что функция корректно его обрабатывает и ретраит.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.