Специально для обучения дата-инженеров и разработчиков распределенных программ, сегодня рассмотрим подходы к организации модульного тестирования Spark-приложений через классы тестовых данных. Зачем и как генерировать эти классы, где их хранить и при чем здесь система автоматической сборки приложений Gradle.
Сборка и тестирование Spark-приложений
Модульное тестирование лежит в основе проверки работоспособности программного кода, а потому активно используется при создании сложных продуктов, включая приложения аналитики больших данных, разработанные на базе популярного фреймворка Apache Spark. В рамках таких приложений код выполняет операции с наборами данных, что можно покрыть тестами, используя локальный сеанс (SparkSession) и создавая датасеты нужной структуры с тестовыми данными. Есть следующие основные способы создания тестовых наборов данных:
- ручное создание тестовых данных – обычно это небольшие датасеты, которые позволяют проверить корректность кода в различных ситуациях;
- автоматизированная генерация произвольных датасетов с помощью скриптов или специальных генераторов;
- выборка из производственных данных.
Самым простым и эффективным способом полностью протестировать логику приложения является создание тестовых данных вручную для каждого тестового примера. Можно сделать тестовый датасет или датафрейм Spark, используя классы Scala Case, которые соответствуют требуемой структуре данных. При этом не стоит делать все поля обязательными, поскольку рабочие таблицы и соответствующие им классы могут состоять из нескольких десятков или даже сотен полей, хотя на практике приложения Spark используют только некоторые из них. Необязательные поля позволяют установить только необходимые данные в тестах.
Чтобы не запутаться в огромном количестве тестовых данных, необходимо хранилище классов для них. Можно сделать отдельный проект в репозитории кода, который содержит все классы тестовых данных для всех таблиц и других структур данных, которые будут использоваться в качестве входных данных для Spark-приложений. Чтобы улучшить практику использования такого хранилища, он должен включать API для проведения тестов, а все проекты Spark должны ссылаться на него в качестве зависимости. Подобный репозиторий с готовыми классами тестовых данных для всех структур можно использовать в качестве источника данных в Spark-приложениях, что упрощает их модульное тестирование.
Определение этих классов тестовых данных должно соответствовать реальным таблицам и обновляться при их изменении. Таким образом, код всех приложений будет тестироваться на реальных структурах данных, с которыми будет работать в производственной среде. Если какие-то таблицы изменятся в будущем и станут несовместимы с существующим кодом приложений, разработчики узнают об этом на этапе тестового запуска. Чтобы автоматически создавать и обновлять автоматически эти классы тестовых данных, можно генерировать их код на основе структуры существующих таблиц Spark, используя пользовательские задачи Gradle в файле build.gradle проекта с тестовой инфраструктурой.
Напомним, Gradle — это система автоматической сборки на основе Apache Ant и Maven с DSL на языках Groovy и Kotlin вместо традиционной XML-образной формы представления конфигурации проекта. Gradle разработан специально для расширяемых многопроектных сборок и использует направленный ациклический граф для определения порядка выполнения задач. Он поддерживает каскадную модель разработки и определяет, какие компоненты дерева сборки не изменились, выявляя задачи, зависимые от этих частей, чтобы не перезапускать их.
С помощью Gradle можно реализовать два альтернативных варианта автоматического добавления или обновления классов тестовых данных:
- на основе описания таблиц Spark в Hive Metastore. Запустив пользовательскую задачу Gradle для генерации кода, в ее параметрах следует указать список баз данных и таблиц, для которых надо классы, поскольку часто не все существующие таблицы будут использоваться в приложениях Spark. Эта задача Gradle получает актуальные схемы (в формате JSON) для всех необходимых таблиц из Hive Metastore и преобразует их в объекты StructType, которые передаются генератору кода. Он для каждого объекта StructType создает файл <YourTableName>.scala, содержащий код класса тестовых данных со структурой, идентичную соответствующим таблицам. Сгенерированные файлы организованы по пакетам, соответствующим базам данных, и сохранены в нужном месте проекта тестовой инфраструктуры.
- на основе производственных классов с логикой создания таблиц, которые задают Spark-схемы результирующих таблиц. Преимущество этого подхода в том, что при редактировании или добавлении любой такой таблицы не нужно запускать Spark-приложение, чтобы сохранить новую версию схемы таблицы в Hive Metastore для получения обновленного класса тестовых данных. Но придется описать результирующую схему таблицы в своем коде, что занимает время.
Впрочем, таблицы — не единственный источник входных данных для приложений Apache Spark. В потоковой обработке есть события разных типов, каждый из которых имеет свою структуру. Приложения Spark получают доступ к датафрейму события через специальный настраиваемый API. Если тестовая среда поддерживает автоматическое создание классов тестовых данных для различных типов событий, их можно также использовать в модульных тестах. Такие задачи Gradle для создания/обновления классов тестовых данных можно запускать вручную или настроить их автоматический запуск по расписанию вместе с модульными тестами.
Поскольку структура таблицы или события может быть достаточно сложной, с любой глубиной вложенности и разными комплексными типа (ArrayType, MapType, StructType и пр), генератор кода должен поддерживать их. Для этого он создает корневой класс и помещает в него вложенные структуры, чтобы избежать конфликтов имен в случае одинаковых имен полей в разных таблицах или событиях. Все поля таких классов case являются необязательными, их опциональность позволяет задавать в тестовом коде только те поля, которые нужны для каждого конкретного сценария тестирования.
Разумеется, нет необходимости покрывать все существующие Spark-приложения полноценными тестами. Но рекомендуется, чтобы для каждого приложения Apache Spark был хотя бы минимальный тест на пустых таблицах, чтобы дата-инженер мог убедиться в работоспособности программы на структурах реально используемых таблиц в производственной среде. Это сокращает время обнаружения ошибок. Например, если с помощью Spark SQL API неверно указано имя поля или операция для его типа, это будет выявлено при следующем локальном тестовом прогоне. Таким образом, модульное тестирование с готовыми классами тестовых наборов данных защищает от некорректных изменений в существующих таблицах, делающих их несовместимыми с существующими приложениями Apache Spark. В этом случае после повторного создания классов тестовых данных модульные тесты сообщат, какие приложения не работают, чтобы разработчик мог быстро это исправить.
Освойте администрирование и эксплуатацию Apache Spark для разработки приложений аналитики больших данных на специализированных курсах в нашем лицензированном учебном центре обучения и повышения квалификации для разработчиков, менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data в Москве:
- Основы Apache Spark для разработчиков
- Анализ данных с Apache Spark
- Потоковая обработка в Apache Spark
- Машинное обучение в Apache Spark
- Графовые алгоритмы в Apache Spark
Источники