Об автоматической генерации тестовых данных, или Зона комфорта для ваших тестов
Представьте себе ситуацию: однажды, придя на работу и проверив результаты ночного прогона автотестов, вы видите абсолютно красный отчёт. Копнув глубже, вы находите причину — заботливо созданные вами тестовые данные бесследно исчезли вместе с остальным содержимым базы данных. Разработчикам накануне срочно потребовалось очистить старые данные на тестовом окружении, но вас эта новость обошла стороной.
Дабы не впасть в отчаяние, ежемесячно подготавливая один и тот же набор тестовых данных, вы начинаете искать пути того, как можно облегчить себе жизнь. Наиболее логичное решение — сделать так, чтобы данные для тестов генерировались самостоятельно. Почему бы и нет? Осталось дело за малым — реализовать задуманное. О том, каким образом можно реализовать эту идею — далее в статье.
С чего начать
Итак, мы решили реализовать автоматическую генерацию данных для своих автотестов. С чего начать?
Чтобы не изобретать велосипед, лучше выбрать оптимальный подход и вооружиться существующими наработками. Какими именно? Вряд ли в сети вы найдете некий «Бесплатный-Супер-Инструмент», который совершенно случайно уже кто-то написал для вас и, приложив его к проекту аки подорожник, вы решите все свои вопросы. Здесь мы обратимся к существующим шаблонам проектирования. Они помогут нам грамотно реализовать свой модуль генерации данных, который будет легко внедрить в тестовый фреймворк. Но прежде чем обратиться к вопросу паттернов, стоит ознакомиться с основными понятиями.
- Данные. Опираясь на объектно-реляционную модель, мы подразумеваем, что данные — это набор объектов и связей между ними.
- Объекты, в свою очередь, — это некоторые сущности, которые обладают набором полей разного типа и могут иметь некоторые ограничения.
- Связи определяют зависимости между объектами и также могут накладывать ограничения на манипуляцию с ними (например, когда создание потомка без указания его предка невозможно).
- Представление данных — описание структуры объектов, их полей и связей. Могут быть составлены в разных форматах: XML, JSON, YAML и т. д. Основываясь на модели представления, мы и будем генерировать наши данные.
Строй и компонуй
Теперь подходим к самому интересному. Допустим, нам на программном уровне нужно реализовать генерацию данных, основываясь на их представлении — древовидной иерархической структуре с описанием элементарных объектов и их взаимосвязей. У нас должна быть возможность получить всё дерево, а в последствии — обращаться к отдельным его частям. Также нам потребуются не только операции создания/удаления, но и изменения одного или сразу нескольких объектов.
Именно здесь мы прибегаем к помощи паттернов проектирования, а именно к паттернам Builder и Composite.
- Builder pattern — порождающий шаблон проектирования. Предоставляет интерфейс для создания составного (сложного) объекта, отделяет логику конструирования объекта от его представления.
- Composite pattern — структурный шаблон проектирования, объединяющий объекты в древовидную структуру для представления иерархии от частного к целому. Предоставляет клиентам возможность обратиться как к одному объекту, так и сразу к группе объектов через единый интерфейс.
Говоря простыми словами, используя Builder шаблон, мы реализуем генерацию, «строительство» сложного объекта, а Composite предоставит нам интерфейс для манипуляции с элементами дерева объектов.
Как это работает
Допустим нам нужно сгенерировать следующий набор объектов и связей:
- Маркет — корневой объект, от которого наследуются все остальные. Маркетов может быть много, они никак не пересекаются между собой. Содержит такие поля, как
market_id, name, country, is_active
. - Компания — может существовать только в одном маркете. При этом маркет может содержать в себе много компаний, названия которых должны быть уникальны в рамках маркета. Содержит поля
company_id, market_id, name
. - Канал — может существовать только в одной компании, при этом компания может содержать в себе много каналов. Названия каналов должны быть уникальны. Также есть возможность включить опцию публикации постов и в текущий канал, и любой другой в текущем маркете. Содержит поля
channel_id, company_id, state, is_secret, secret_phrase, can_share, share_to, main_image, name
. - Пост — публикуется в канале, при этом канал может содержать в себе много постов. Заголовок поста необязательно должен быть уникален. Содержит поля
post_id, channel_id, title, description, state, main_image
.
Снизу представлен граф модели данных, где вершина 0 — маркет, в котором находятся две компании 1 и 2. В компании 1 опубликовано два канала — 10 и 4 с постами внутри каналов
Чтобы сгенерировать набор живых данных, основываясь на данной модели, нам нужно написать класс-строитель, который будет создавать необходимые объекты, начиная с корневой вершины графа, продвигаясь вниз по связям к дочерним объектам. Также нам нужно подготовить модель представления данных, которая будет взята за основу нашим билдером.
Таким образом, если нам, например, нужно создать новый канал во второй компании, мы вызовем что-то похожее на это:
builder_instance.build_channel(data_model, name=’News’, company_id=company_id)
Тогда наш builder_instance
подготовит модель данных для нового канала, предварительно заполнив все нужные поля, загрузит на бекенд картинку, получит её id и вставит в main_image поле, укажет, к какой компании прилинковать канал, и отправит запрос на создание сущности. Нас не должны заботить все эти детали реализации. Главное, что на выходе мы получим channel_id/channel_name
, сгенерированного на основе описанной нами модели данных.
Схема сборки связки: Маркет — Компания — Канал — Пост внутри builder-класса
Теперь настало время выделить роль компоновщика и его функции.
Допустим мы можем создать новый тестовый канал одной строкой. Но что делать, когда нам нужно создать не один, а, например, сорок каналов и всем установить параметр is_published = True
?
Как раз чтобы не вызывать один метод сорок раз, нам и нужен компоновщик. Используя уже описанную выше модель, предположим, что теперь нам нужно опубликовать все посты в канале с определённым channel_id
:
composite_instance.publish_posts(channel_id)
Компоновщик возьмет канал с указанным id из списка, созданного билдером, и в цикле, пройдя по списку постов в данном канале, на каждом вызовет: builder_instance.publish.post(post_id)
Таким образом, мы избегаем дубликации, а компоновщик в нашем случае выступает посредником между клиентским кодом и билдером, самостоятельно вызывая нужные методы объекта builder в зависимости от наших потребностей.
Кому-то может показаться излишним добавление еще одного уровня абстракции в лице компоновщика, ведь всю логику можно реализовать напрямую в билдере. Однако, если вы планируете в перспективе использовать строителя в качестве reusable компонента, к примеру, для написания на его основе API или DB тестов, то отсутствие дополнительной прослойки создаст вам ненужные препятствия в будущем.
В схемах, представленных выше, присутствует еще один компонент — Data Access library. Этот слой представляет собой уровень, который непосредственно отвечает за коммуникацию с приложением — создание и сохранение данных, например, посредством web API или напрямую в MS SQL. В моём частном случае генерация данных реализована через web API приложения, а в качестве библиотеки я использовал Python Requests — удобный модуль, работающий на основе urllib3 и позволяющий собрать запрос и получить ответ буквально в пару строк. Ознакомиться с возможностями Requests можно здесь. Также может так случиться, что вам нужно генерировать рандомные значения полей в красивом виде. Здесь нам поможет библиотека Faker, изначально написанная для PHP, но впоследствии портированная на Python и Java. Faker умеет генерировать рандомные имена, фамилии, адреса, email-адреса, номера телефонов, просто слова и т. д. Больше прочитать об этой библиотеке можно здесь, а если вы используете Java — загляните сюда.
Преимущества и недостатки подхода
Преимущества:
- независимость тестов;
- экономия времени;
- удобство поддержки;
- возможность повторного использования.
Недостатки:
- удобство поддержки сильно зависит от реализации.
Выводы
Конечно, представленная схема не претендует на звание единственно верной и универсальной. Всё зависит от специфики проекта и ваших потребностей. Однако я думаю, что данную модель можно брать за основу, если вы размышляете над тем, как оптимизировать процесс подготовки ваших тестов и как сделать тесты более независимыми от внешних факторов. Таким образом, однажды потратив время на реализацию и внедрение, вы избавляете себя от лишних затрат в будущем, сведя все работы к небольшим коррекциям и расширению набора моделей данных в случае необходимости.