«У нас было две горячих ноды, 75 дев-стендов, 5 видов тестов, горка внешних сервисов, и целое множество тест-кейсов всех сортов и расцветок, а также CI/CD, core-модель, пачка customer-модулей и DBUnit. Не то чтобы это был необходимый запас для интеграционного тестирования. Но если начал делать свой тестовый фреймворк, становится трудно остановиться. Единственное, что вызывало у меня опасение – это Excel. Нет ничего более беспомощного и обреченного, чем тестировщик, который вынужден заполнять 20 листов записей вручную. Я знал, что рано или поздно мы перейдем и на эту дрянь». © Хантер Томпсон, если бы работал гонзо-тестировщиком

Привет, Хабр! Меня зовут Настя Кензина, я – QA-инженер в компании HFLabs и занимаюсь тестированием CDI (Customer Data Integration) – системы управления клиентскими данными. В этой статье я расскажу, какие сложности накопились у нас за годы эксплуатации и развития тестового фреймворка, в чем минусы табличного хранения тестовых данных, и какое итоговое решение выявленных проблем мы для себя нашли.

В больших корпоративных системах интеграционные тесты быстро перестают быть просто проверками. Они становятся отдельным инженерным продуктом: готовят базу данных, поднимают контекст, имитируют внешние сервисы, запускают задачи, дергают SOAP и REST, а потом сверяют не только ответы API, но и то, что осталось внутри базы.

В CDI этот момент наступил довольно быстро. Один сценарий может затрагивать десятки разных таблиц: карточки клиентов с атрибутами, связи, дубли, историю изменений и служебные таблицы. Нужно вручную, таблицу за таблицей задать как начальное, так и ожидаемое состояние базы. Пока тестов мало, а сама система довольно простая, с этим можно жить. Но, по мере роста, расширения и усложнения системы, команда начинает тратить все больше сил и времени не на непосредственную проверку поведения системы, а на расшифровку, корректировку и валидацию тестовых данных.

Это и стало основной нашей мотивацией для внедрения Groovy DSL в тестовый фреймворк CDI: мы хотели, чтобы тест говорил на языке предметной области, а не на языке таблиц. Тесты на Excel/DBUnit были подробными, но слишком низкоуровневыми. Они показывали строки в таблицах, но плохо показывали сценарий. Их было трудно читать, тяжело сопровождать при изменении модели и не всегда легко ревьюить.

С чем мы подошли к необходимости изменений

Тестовый фреймворк CDI построен иерархически, как многоуровневый пирог. Первый уровень – основа – общий инфраструктурный слой. Он содержит базовые классы для интеграционных тестов, работу с тестовыми дескрипторами, выполнение подготовительных и основных задач (jobs), проверку SOAP и REST.

Второй уровень – базовый класс feature-тестов в core CDI. Он подключает Spring-контекст, подготавливает БД, загружает тестовые данные, применяет заданные конфигурации и после теста возвращает измененные настройки. 

Третий уровень – специализированные классы под разные уровни системы: сервисные сценарии (внутренняя бизнес-логика), задачи и фоновые процессы (импорт, поиск дублей), SOAP-сервисы и web/API/UI-сценарии, схемы полных интеграционных прогонов (вся система как на «боевом контуре»).

Наконец, четвертый уровень, о котором мы и будем дальше разговаривать, – сами тест-кейсы. Каждый кейс состоит из входных данных, параметров сценария, ожидаемых данных и иногда дополнительных артефактов:

  • dbStart.xls – состояние базы до выполнения сценария;

  • dbEnd.xls – ожидаемое состояние после;

  • *.params.xml – параметры запуска задачи или запроса;

  • request/response – файлы для SOAP и REST.

Исторически тестовые данные описывались у нас в Excel-файлах для DBUnit. Идея понятная: каждый лист Excel – отдельная таблица БД, каждая строка – запись. Хочешь проверить сценарий – собери стартовое состояние, запусти сценарий, сравни итоговый результат. Но чем сложнее модель, тем сильнее проявляются недостатки.

Мечтают ли тестировщики об электронных таблицах?

Проблема в том, что CDI – data-centric система. Здесь сценарий «добавить телефон клиенту» редко означает одну строку в одной таблице. Нужно создать карточку party (клиент или контрагент), связать с ней атрибут, заполнить обязательные поля, учесть историчность, служебные таблицы и ожидаемые значения. В итоге простой бизнес-сценарий в тесте выглядел как нагромождение технических строк.

– А как у вас обстоят дела с Excel?
– Вы знаете, Эксель мне великоват, мне бы Эску.
©
Народное

Это все еще вполне рабочий подход, который мы практиковали не один год, но он не обеспечивает должного комфорта ни на одном из этапов работы с тестами. Что-то нужно было менять, и на это было пять причин:

Первая причина – это боль. Боль читаемости. По Excel сложно быстро понять намерение теста. Сценарий «две карточки объединились в одну» распадается на строки в party, attributes, merge-таблицах, связях и исторических версиях. Если автор теста еще понимал, что именно хотел проверить, то ревьюер уже видел только технические детали – набор таблиц с десятками значений. Немного помогала ручная цветовая разметка таблиц, но она требовала дополнительных усилий и времени автора.

А вторая – это тоже боль: цена изменения модели. Добавили обязательное поле, поменялся дефолт, изменилась схема хранения исторических версий – и нужно править много тестов, хотя бизнес-смысл сценариев не изменился.

Третья – высокий риск ошибок. Новые тесты часто делались из похожих старых: вместе с шаблоном переезжали лишние данные, устаревшие значения и случайные ошибки. При заполнении нескольких листов легко было допустить опечатку, да и знаменитое автоформатирование Excel то и дело норовило превратить ½ во 2 января. 

Четвертая – контроль версий и поиск. Excel плохо мержится в Git, история изменений нечитаема, а при совместной работе легко потерять правки. Более того, для простой и частой процедуры поиска теста по конкретным данным – телефону или ФИО – нужны специальные утилиты: обычный поиск в IDE не сработает.

Пятая – расширяемость. Core CDI задает общую модель, а клиентские customer-модули добавляют свои типы карточек, правила и сценарии. Нам нужен был общий механизм, который можно расширять под конкретный домен без копирования инфраструктуры.

Что мы сделали: предметный слой как альтернатива таблицам

Мы добавили над DBUnit предметный слой – Groovy DSL для проблемно-ориентированного описания тестовых данных. DSL (Domain-Specific Language) – узкий язык для одной конкретной задачи. Мы выбрали его не ради красивого синтаксиса, цель была практическая: поднять уровень описания тестовых данных. 

DBUnit и общий тестовый фреймворк никуда не делись: они по-прежнему готовят базу, запускают сценарий, сравнивают результат, сохраняют артефакты и чистят окружение. Изменилось место, где живет сложность. Раньше полные данные для каждого теста вручную раскладывались по всем необходимым таблицам. Теперь тест в.gdsl-файле описывает ситуацию на языке предметной области, а DSL превращает это описание в DBUnit-таблицы.

Так, упомянутый выше сценарий «у клиента появился телефон» теперь не требует заполнения полдюжины таблиц со всеми «NOT NULL»-столбцами. В DSL тот же смысл читается ближе к сценарию:

excel {
    dbStart {
        physical(1) {
            surname = 'Иванов'
            email { email = 'ivanovivan@mail.ru' }
        }
    }
    dbEnd {
        physical(1) {
            phone { number = '74994586890' }
        }
    }
}

Даже без знания внутренней схемы видно: до выполнения есть Иванов с email, после выполнения у него должен появиться телефон. Все служебные поля и связи DSL достроит сам. Это главный выигрыш: тест снова показывает намерение, а не устройство хранения.

Почему mapping оказался важнее синтаксиса

Важный момент: DSL не содержит жестко зашитый список всех сущностей. Он читает конфигурацию из dsl-model-mapping.yml – большого маппинга, описывающего, какие party-типы, атрибуты, документы и идентификаторы доступны в конкретном модуле, и с какими Java-классами они связаны.

parties:
  physical:
    reference: ru.hflabs.cdi.model.party.physical.PhysicalParty
    type: PHYSICAL
attributes:
  phone:
    reference: ru.hflabs.cdi.model.attributes.phone.Phone
    type: PHONE
  address:
    reference: ru.hflabs.cdi.model.attributes.address.Address
    type: ADDRESS

Это хорошо легло на core/customer-архитектуру CDI. Общий механизм остается один, а словарь может расширяться в клиентском модуле. Если у конкретного заказчика есть свой тип карточки или атрибута, его можно добавить в маппинг и пользоваться тем же жизненным циклом теста.

Для команды это было важнее, чем сам Groovy-синтаксис. Мы получили не просто новый формат фикстур, а расширяемый язык поверх общей тестовой инфраструктуры. Он не потребовал от нас вносить изменения в уже действующие процессы фреймворка, заменять или удалять какие-то его части. Нет, фактически DSL стал новым, пятым слоем нашего многоуровневого пирога – этаким топпингом, который облегчает взаимодействие с тестами, выполняя роль переводчика между человеком и базой данных.

Дефолты вместо копипасты

В табличном подходе автору приходилось помнить много технических деталей: как заполнять historicalId, stagingHid, startDate, endDate, mergedStatus, version, partyHid, partyType и другие поля. Часть из них почти всегда заполнялась одинаково, копипастой из уже существующих тестов.

Мы вынесли такие значения в дефолты. Если поле не важно для конкретной проверки, его не нужно указывать в тесте. Если важно – его можно переопределить явно.

Например, вместо полного описания строки PHONE можно написать:

physical(1) {
    phone {
        rawId = 'PHONE.1'
        countryCode = 7
        cityCode = 495
        number = 2171522
        qualityCode = 'GOOD'
    }
}

DSL сам поймет, что телефон принадлежит конкретной карточке, положит данные в нужную таблицу и заполнит обязательные поля. Автор теста видит только то, что важно для сценария, DSL делает все остальное. Это снижает шум и одновременно делает тесты стабильнее. Если у нас есть общий дефолт для party или атрибута, он меняется в одном месте и для всех тестов сразу.

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

Сложные сценарии: объединения, связи, контекст

На простых объектах DSL просто экономит место. Настоящая польза проявляется в сценариях, где табличный подход особенно тяжело читать.

Например, объединение карточек. В Excel это набор строк в нескольких party- и merge-таблицах, а DSL позволяет легко описать саму ситуацию, используя вложенность:

excel {
    dbStart {
        physical(10) {
            physical(1) {
                mergeAuthor = 'AL:1'
                phone { number = '2171522' }
            }
            physical(2) {
                mergeAuthor = 'BT:1'
                phone { number = '2171523' }
            }
            mergeAuthor = 'AL:1, BT:1'
        }
    }
}

DSL тут создает не только сами party и attributes, но и вспомогательные merge-таблицы, а для атрибутов можно управлять тем, какие значения попадут на эталонную слитую карточку клиента. 

Аналогичным образом реализованы функции и для других необходимых нам бизнес-процессов системы: создание дубликатов, задание связей между карточками, управление историческими версиями, прошлыми атрибутами и др. А для редких нетипичных случаев, где доменной команды нет или она не нужна, остался низкоуровневый выход через createCustomTable, позволяющий задать руками все табличные значения напрямую.

Еще один важный момент – контекст между dbStart и dbEnd. Часто ожидаемое состояние почти совпадает с исходным: меняются два-три поля, а остальное должно остаться как было. Для этого в dbStart можно задать переменную и передать её в dbEnd, указав только ожидаемые изменения. Это убирает копипасту вида «исходное состояние плюс одно измененное поле».

excel {
    def context = dbStart {
        physical(1) { surname = 'Иванов' }
    }
    dbEnd(context) {
        table['PHYSICAL'].editById(1) {
            endDate = '${ts}'
            mergedStatus = 1
        }
    }
}

Какой профит мы получили в итоге

Читаемость. Тест стал ближе к языку, на котором разработчик или аналитик описывает ситуацию: «есть физлицо с паспортом», «есть две объединенные карточки», «есть связь такого типа между физлицом и организацией», «после задачи должна заполниться/очиститься таблица» и т.п. В общем случае, это сильно легче и писать, и ревьюить, чем набор строк в нескольких таблицах.

Меньше копипасты, дешевле изменения. Дефолты, служебные поля, историчность, merge-таблицы и типовые связи теперь создаются централизованно. Тесты стали меньше зависеть от деталей хранения. А если меняется технический дефолт, чаще достаточно скорректировать DSL или mapping, а не править десятки Excel-файлов. 

Тестовые данные стали обычным текстом. Их проще искать в IDE, смотреть в diff, мержить и обсуждать в MR. По сравнению с .xls, это стало большим шагом для автора-человека, но гигантским скачком для автора-ИИ: текстовый формат описания тестовых данных открыл нам возможность привлекать ИИ-агентов для написания, отладки и ревью тестов.

Масштабирование на customer-модули. Через dsl-model-mapping.yml каждый модуль может добавить свой набор party и attributes, при этом пользуясь всеми общими методами и схемами.

Диагностика не ухудшилась. DSL не поменял и не усложнил процесс диагностики. Наоборот, он встроился в существующий фреймворк: при падении по-прежнему можно смотреть фактические и ожидаемые наборы данных, расхождения, ответы сервисов, логи и артефакты.

Технические компромиссы, без которых не обошлось

Большие сценарии все еще трудно читать. DSL хорошо работает на маленьких и средних сценариях. Если кейс действительно требует много данных, .gdsl тоже будет длинным и тяжело читаемым. В таких случаях помогают декомпозиция, общие заготовки и развитие доменных команд. 

Groovy metaclass добавляет магии. Методы вроде physical, legal, phone генерируются динамически из маппинга. Это удобно для автора теста, но требует понятных ошибок и документации. Иначе новый разработчик видит не DSL, а загадочное поведение Groovy.

Низкий уровень все равно нужен. Некоторые редкие таблицы все еще удобнее описать напрямую через createCustomTable. Важно только не превратить его в новый Excel внутри DSL: если половина теста написана через createCustomTable, то автор все еще мыслит категориями БД, а значит, языку не хватает доменных команд.

Риск слишком умных дефолтов. Если DSL скрывает слишком много, тест может перестать явно показывать важное условие. Поэтому мы договорились: автоматизируем рутину и технической шум, но не прячем те бизнес-поля, которые действительно влияют на смысл проверки.

Практические выводы

Если вы думаете о DSL для тестов, начинайте не с синтаксиса. Сначала найдите повторяющиеся предметные и сценарные ситуации. В нашем случае это были карточки, атрибуты, документы, связи, дубли, объединения и ожидаемое состояние после операции.

Не делайте DSL отдельной игрушкой. Он должен быть частью жизненного цикла теста: подготовка сценарных и служебных данных, выполнение действия, сравнение результата, артефакты, очистка. При этом, сам по себе, DSL решает только часть задачи: подготовку и проверку данных. Весь тестовый сценарий по-прежнему выполняет фреймворк.

Сразу продумайте расширяемость. Конфигурационный маппинг помогает нам поддерживать разные customer-модули без копирования фреймворка.

Оставьте выход на низкий уровень. Полностью закрыть все таблицы доменным языком невозможно, да и не нужно. Важно, чтобы низкий уровень был исключением, а не основным способом писать тесты.

Обязательно инвестируйте в диагностику. Читаемый DSL не спасает, если при падении непонятно, где именно разошлись ожидания и фактическое состояние.

Закрепите стандарт организационно. Если в проекте одновременно живут Excel, XML, ручные SQL-фикстуры и DSL без правил миграции, тестовый ландшафт снова расползется.

Вместо заключения

Наш тестовый DSL в CDI никогда не был самоцелью. Он родился как ответ на конкретную боль: тесты в сложной data-centric системе стали слишком табличными, хрупкими и дорогими для изменения. Excel все еще обеспечивал нас необходимыми опциями для создания тестов, но делал это на слишком низком уровне, требовал много рутинной работы и тяжело читался. 

Новый формат тестовых данных закрыл многие накопившиеся проблемы, а возможность скормить их ИИ открыла огромный потенциал для роста производительности команды. За год с момента внедрения DSL наш тестовый каталог пополнился почти полутора тысячами gdsl-сценариев в core- и custom-модулях. Темп прироста особенно заметен на фоне старых 5000 тестов, которые мы копили больше десяти лет.

Важно, что DSL здесь не заменил весь интеграционный фреймворк. Он стал новым слоем, обеспечивающим более интуитивную генерацию входных и ожидаемых данных, сохранив при этом совместимость с уже существующими тестами. Это не отменяет технической сложности, но переносит ее из каждого отдельного теста в поддерживаемый DSL и общий фреймворк.

В результате тесты стали ближе к языку системы. Теперь автор командует «создай карточку с телефоном» или «объедини две карточки», а не заполняет строки в пяти таблицах. Благодаря этому, один и тот же подход к данным работает в совершенно разных типах тестов.

Для нас это и оказалось главным результатом: тесты стали говорить на том же уровне абстракции, на котором сама команда обсуждает поведение системы. А DSL оказался тем способом, который зафиксировал этот уровень в коде.

В планах не только дальше расширять DSL новыми доменными командами, но и сделать работу с ним максимально комфортной. Мы хотим добавить полноценную IDE-поддержку: анализ кода, автодополнение, валидацию и подсказки. А ещё – предпросмотр табличных данных прямо во время написания .gdsl, чтобы видеть финальный результат еще до запуска теста.

Комментарии (0)