Привет, Хабр! Меня зовут Андрей Околелов, я работаю ведущим инженером в Блоке обеспечения и контроля качества выпуска изменений ПО в РСХБ. В банковской сфере я работаю с 1992 года, с 2018 основная деятельность — тестирование банковских приложений.

SQL написать просто, когда это обычный Select. Но становится сложно, когда критериев очень много, а вариативности еще больше. А если еще добавить команду разработчиков, в которой у каждого свое мнение, то процесс становится очень трудным. В материале я расскажу, как внимание к деталям и смена фокуса от модели к предметной области позволяют решить множество проблем разработки запросов и создать удобный инструмент не только для разработчика, но и для всей команды.

Сгенерировано в Midjourney. Подробный гайд, как сделать также или даже лучше, по ссылке
Сгенерировано в Midjourney. Подробный гайд, как сделать также или даже лучше, по ссылке

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

Тест включает три этапа: формирование условий, воздействие и проверка результата. Формирование предусловий — значит найти договор, на основе найденного договора найти новый договор и проверить, что договор создан. Все вроде бы просто: разработчик сделал инструмент, SQL‑запрос, провел тест, прогнал его в Jenkins, тест прошел. К этому процессу подключается тестировщик, говорит, что все здорово, но добавляет и свой критерий по договору:

  • дата открытия договора (пример: в 2020 году ЦБ выпустил новое указание № 10 005);

  • тип и вид договора (простой, сложный, красный, в национальной или иностранной валюте);

  • валюта договора (рубль, юань, доллар, евро);

  • дата закрытия.

Разработчик снова посмотрел; подумал, что все просто; и сделал. Тестировщик обрадовался, что ему достанется меньше работы, и добавил новые критерии поиска для тестов:

  • тип клиента (физлицо, юрлицо или ИП);

  • группа клиента (вип, не вип, средний);

  • справочник по террористам (включен или нет);

  • справочник особого внимания (включен или нет);

  • действующий (да или нет).

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

  • счет 2 порядка;

  • валюта (RUB, USD);

  • остаток на счете (открыт, закрыт, зарезервирован);

  • подразделение счета;

  • статус (открыт, закрыт, зарезервирован).

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

Тут не мог не возникнуть целый ряд проблем.

  • Монолит, с которым тяжело разбираться.

  • Монолит перемещается от теста к тесту. При перемещении разработчик может добавить что‑то ненужное или упустить очень важное.

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

  • Нет единства в запросах. Хотя на проекте утвержден кодстайл, и для любого скрипта разработчик должен соблюдать некие правила, все равно у каждого разработчика получается по‑своему, что не позволяет проводить единый анализ.

  • Выдача запроса: выдает в основном единичные элементарные данные (ID, CollectionId и т. д.).

  • Неудобства при переносе запроса из проекта в SQL develop для тестирования и обратно, принимаем как должное.

  • Для написания запроса требуется уверенное знание структуры БД.

На фоне этих проблем мы поставили цель — разработать инструмент получения данных на основе элементов предметной области. Цель повлекла за собой и требования, которые мы для нее выставили.

  • FrameWork для работы с текущей БД и на перспективу. Поскольку первая и основная задача — это поиск данных, чтобы на них потом воздействовать, мы рассмотрели вопрос с FrameWork для работы с текущей БД и на перспективу. Например, сейчас у нас проходит проект импортозамещения, в котором одна БД меняется на другую.

  • Понятная, удобная выдача с необходимым набором данных.

  • Дружественный инструмент для разработчика и тестировщика. Члены команды, работающей над проектом, должны понимать друг друга, разговаривать едиными терминами.

  • Удобный инструмент разработчика для формирования SQL запросов на основе модели данных и на основе элементов предметной области. Есть модель, мы ее «рисуем». Абстрагируясь от этой модели, мы должны еще создать инструмент, работающий не на модели, а на основе элементов предметной области. То есть мы говорим уже не про таблицу как таковую, а, например, про продукт.

Вот конкретный пример того, что раньше у нас было на проекте.

По нему сразу видны проблемы, о которых я уже говорил. Первая: query запросы — это как string. В задачи первостепенные ставилось уйти от string и перейти к какому‑то объекту. Второй момент: минималистичная выдача, то есть выдает какой‑то один ID. Я посмотрел и поискал на проекте, у меня есть часто используемые таблицы. В моменте я нашел по одной таблице в варианте SQL‑запросов и в варианте выдачи порядка 40–45 классов, работающих с одной таблицей, что тоже совершенно неудобно для работы.

Мы рассмотрели, что можно задействовать в работе, и остановились на FrameWork Hibernate. Мы сделали первый DAO (Data Access Object) — инструмент, который обращается к базе данных и возвращает нам результат. Здесь ничего особо интересного нет, замечу лишь, что для себя мы нашли нечто новое: класс может возвращать не только список сущностей, но и один экземпляр. Все зависит от того, как построен запрос. Мы в DAO всегда возвращаем только список, ограниченный нашим максимальным результатом. А уже класс сервиса, который будет обрабатывать результат, который мы получаем из данных, уже сам решает, что он должен вернуть: какую‑то коллекцию или единичный результат.

Далее приведу пример реализации таблицы. Это никого не удивляет, у всех есть такое. Но для себя мы получили четкий набор атрибутов и данных, что ранее было проблемой. Кроме того, мы отказались от всех связей, которые можно организовать в Dto классе, и оставили только связи 1:1. В этом случае у нас класс данных становится объемным. Это тоже дало свои плюсы.

В рамках реализации цели дружественности инструмента было больше организационных моментов: посмотреть на документы проекта, что уже сделано и как это было реализовано, а также еще раз это дело пересмотреть. Какие вопросы были проработаны.

Особое внимание уделили параметрам для запросов. Параметры надо всегда показывать перед тем, как мы будем выполнять запрос. Их должны видеть не только разработчики (для которых название параметра C_CURRENCY = 840 вполне понятно), но и тестировщики, которые со временем привыкнут к названию C_CURRENCY и будут понимать, что это валюта. Но лучше сразу проявить дружелюбность и вывести описание элемента предметной области ВАЛЮТА.

Формирование JavaDoc. Ранее на проектах были приняты комменты на классах, методах и атрибутах. Мы посмотрели, подумали «да, это хорошо», но все же перешли на JavaDoc. Мы посмотрели, как это должно формироваться, определились с шаблонами на формирование классов и описание методов, какие теги и что мы должны писать. Помимо прочего, JavaDoc оказался в некоторых случаях полезен не только для классов и методов, но и для атрибутов. То есть если атрибут используется на 500 строке класса, просто наведение курсора на этот атрибут сразу показывает, что там есть. Не нужно перемещаться к атрибуту и читать комменты к нему.

Названия шагов теста и результат шага, название методов и классов. Дружественность должна быть обоюдная. При чтении allure‑отчетов тестировщик должен видеть термины, с которыми он привык работать. Также мы еще раз пересмотрели структуру, формирование package, наименования методов, классов и названия атрибутов.

Вот пример с названиями атрибутов класса Dto. При формировании каждый атрибут ассоциируется с колонкой в БД. В данном случае c_name c_registr_num. Мы префикс удалили, а все остальное мы в CamelCase вводим. То есть не нужно придумывать ничего нового, уже все есть, и при новых выводах любой атрибут уже будет ассоциироваться с той колонкой, которую планируется использовать.

Для атрибутов, организующих link, мы тоже создали свое правило. То есть отбросили префикс «С подчеркивание» и добавили в нему link с простым названием. База у всех организована так: есть таблица клиентов, есть клиент юрлицо. Есть таблица продукт и производная продукта — договор. Они всегда идут вместе. Если потом назвать в старшем наследуемом классе «клиент» то мы в итоге получим клиент‑клиент, что крайне неудобно. Когда используем link, мы понимаем, что сразу идем к какому‑то объекту и просто переносим link‑клиент и link‑продукт, или link‑клиент1 и link‑клиент2. То есть сразу все понимаем, нет никаких разногласий.

Анализируя наш проект, мы заметили, что у нас довольно простой select, то есть select from where и то же самое с условием join — select from join where.

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

Мы назвали инструмент QueryBuilder. Он четко повторяет то, что мы делаем, поддерживает математические и логические операции, некоторые функции БД.

Вот код, сгенерированный этим инструментом.

Что мы в итоге получили. От стиля организации и документации проекта просто отойти невозможно. Генератор выдает select строго в нужном формате.

Следующая итерация. Мы абстрагируемся от модели данных, конкретных таблиц и переходим на более высокий уровень, предметную область. Понятно, что мы уже начали делать билдеры, то есть предметные области, которые использует модель каких‑то данных. Вот, например, мы создали клиента с критерием 1 и 2, договор с критерием 1 и 2, и кажется, что счастье пришло, люди начали вникать в процесс и создавать критерии на раз‑два.

И тут проявилась другая проблема — проблема дубликатов. Есть требование — найти договор, где у клиента есть какой‑то тип, или найти клиента, где у договора есть какой‑то тип. В нашем случае получается, что и в билдере для договора и в билдере для клиента есть одни и те же требования — тип. Возникла идея их объединить. То есть договор объединить с клиентом.

Мы их даже не объединяем, а наследуем. В итоге мы получили: запрос на поиск клиента с параметрами 1 и 2 (которые могут иметь свои параметры) реализуется с использованием инструмента, обладающего деревом критериев, который накрывает все требования. Такие запросы исполняются теперь просто и быстро.

Зная, как работает инструмент, попробуем его использовать. Ниже справа в ТК № 1000 представлены реальные условия из нашей работы. Слева то, что написать тестировщик — иди туда не знаю куда, возьми то не знаю что. Справа уже разработчик выделил ключевые моменты, области и критерии для поиска.

При использовании нашего инструмента получается вот такая запись. Мы это назвали ParametersBuilder. Мы тут увидим joinClCorp — это юрлицо, имеющее статус и один в один повторяющее те критерии, которые и выделил разработчик во время чтения предусловий теста.

Давайте теперь попробуем разработать наш инструмент. Первое — любой билдер имеет целевую таблицу, с которой мы будем работать. Целевых таблиц может быть несколько, в том числе по умолчанию (продукт, клиент, договор и т. д.). В данном случае у нас одна таблица.

Первую часть QueryBuilder мы нарисовали, конструктор сделали, идем дальше.

Раньше запросы были совершенно простые: select from where и select from join where. Написали первый критерий select from where.

Второй пример, когда выборка идет из какой‑то другой таблицы, и здесь есть select from join where.

В итоге мы за 5–10 минут создали новый инструмент. С ним можно уже идти работать.

Что мы еще получили. Приятные бонусы в Allure. Вот фрагмент Allure. Его читает тестировщик, видит нужные параметры и знакомые термины, которыми оперирует. Есть объект и сразу можно посмотреть, что еще отобрано. По реквизитам объекта можно однозначно определить выбранный экземпляр. Тестировщик видит все это удобство и снова благодарит разработчика.

Как обычно бывает, кроме хорошего случаются и не очень приятные вещи. Еще один пример — не найден договор по параметрам. Как я говорил в начале, мы должны показать сначала требования, чтобы всем было удобно. Тестировщик видит те требования, что она сам же и написал (но на то он и тестировщик — у него всегда есть сомнения в данных, имеющихся в системе, в самом тесте и не только). Поэтому он взял критерии, пошел в систему, поискал по ним и ничего не нашел. Вот здесь тестировщик ставит самый большой плюс разработчику, он ему полностью доверяет. И в этом случае уже появляется дружба команды и работа единым фронтом.

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

  • Разработан удобный инструмент поиска данных для разработчика.

  • Инструмент удобен для всей команды.

  • Понимаем, как будем развивать и актуализировать.

  • Начала решаться проблема дубликатов.

  • Быстрый вход в проект.

Момент с быстрым входом в проект объясню на примере. У нас сейчас есть стажерская программа. В нашем понимании типичный стажер — молодой человек, имеющий средние познания в java и очень общие познания в тестировке. Однако он совершенно не знает тестируемую систему, а также структуру ее БД. Но с помощью разработанного нами инструмента стажер способен начать писать тест в совершенно новой области уже после третьего занятия.

В общем и целом, мы посчитали, что инструмент и централизация процессов дали нам средний прирост сдачи тестов на ревью на 20%. Можно сказать, что мы «наняли» еще одного разработчика в команду.

Но, как говорится, счастье идет об руку с печалью. Каждое приложение имеет ошибки, которые выявляются только в процессе эксплуатации. Вот и у нас появились некоторые проблемы.

Случай первый. У нас есть таблица с колонкой «Филиал» и кодом филиала. Для этого случая у нас написан свой QueryBuilder. Итак, подставляем код, все хорошо, но через некоторое время начала появляться ошибка: «Счет принадлежит другому подразделению». Мы начали разбираться и получили случай второй. Он говорит о том, что, оказывается, в таблице есть еще и «Подразделение», и код подразделения формируется как код филиала — порядковый номер подразделения. Но в этом примере мы видим, что код филиала не соответствует коду подразделения. Опять же, это проблема некорректности БД. Ее мы решили исправленным билдером, и все заработало.

Теперь смотрим другой случай на основе номера 2. У нас всегда должны быть филиал 000 и подразделение 000–001. В одном из тестов был написан билдер от случая первого. Это я называю функциональной некорректностью скрипта, который мы написали. Также нам встретилась проблема некорректности самого скрипта и его производительности, то есть когда скрипт не был актуализирован, а отклик от БД был больше минуты. После актуализации отклик от БД составлял меньше секунды.

Напоследок расскажу о потенциальных сферах применения подобного подхода к организации.

Задача 1: Импортозамещение. Задача, поставленная сейчас всем организациям в РФ. Нас она тоже, конечно же, зацепила. В РСХБ проводится большая работа по миграции на отечественные решения, и одна из ключевых систем сейчас переводится на другую БД. Работа будет выполнена, однако новую БД мы не запустим, пока бизнес не скажет, что у него есть уверенность в текущих делах. А это напрямую зависит от значимости и глубины системы. Чем значимее система, тем дольше будет период согласования запуска. В какой‑то момент у нас будут эксплуатироваться две системы, а значит и тестироваться будут они обе.

Я предполагаю, когда это произойдет, может поменяться модель данных, а также синтаксис языка, который будет использовать SQL‑запросы. В таком случае мы доработаем модель и добавим к нашему проекту, доработаем синтаксис, то есть генерацию SQL‑запросов, и, очень надеюсь, запустим эти тесты.

Задача 2: Простой доступ к данным заинтересованных лиц. Например, мы возьмем АБС — автоматизированную банковскую систему — и тестировщика. Однако регулятор и бизнес постоянно подкидывают задачи. В итоге изменения будут всегда, и всегда их необходимо будет тестировать. Соответственно, тестировщику необходим постоянный доступ к данным для проверки и написания новых тест‑кейсов.

Сами системы ценны тем, что они хранят. Возьмем пример с ДБО — дистанционное банковское обслуживание. Там все иначе: и FrameWork, и команды, другой тестировщик и т. п.. Но если глобально, то это лишь канал доступа к нашей основной системе, а основные данные опять таки хранятся в нашей АБС. И тут опять, тестировщику ДБО нужно иметь постоянный доступ к данным.

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


  1. leha_gorbunov
    07.12.2023 18:06
    +4

    Вы пишите один код, чтобы получить другой код? Я три раза перечитал.

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

    И разве ваш QueryBuilder не уменьшает гибкость по сравнению с чистым SQL?


  1. ALexKud
    07.12.2023 18:06

    Можно разные подходы применить. Например сохранять все сущности запроса в таблице запросов и связанной с ней таблице сушностей и конструировать запрос просто выбором запросов и сущностей. Тогда можно легко добавлять запросы и сущности в таблицы и делать только отметки нужных сущностей и параметров, а сам конструктор сделать в северной процедуре,, которая выдаст строку SQL запроса для динамического выполнения. Строку можно и вручную подправить, если нужно. А так и стандарт кодирования соблюден и построение запроса автоматизируется. Разработка сложных sql северных процедур требует уже другого порядка знаний подходов и алгоритмов обработки наборов данных в SQL и, как правило, для web разработки это не нужно, кроме знания select, update и insert.


  1. Vitimbo
    07.12.2023 18:06

    Ещё немного и рсхб научит тестировщика пользоваться орм и не тратить время программиста.