На связи Сергей Кондитеров и Сергей Бушмелев, ведущие инженеры по автоматизации тестирования в компании РТЛабс. Это вторая часть статьи «Мессенджеры на работе — это не прокрастинация, или как мы сделали сервис для автотестирования». Как и обещали, в данной статье мы расскажем о том, как масштабировали наш сервис, как развивали функциональность автотестов и как в итоге вышли за рамки обычного репорт-бота.  На первом этапе нами был создан бот. Он работал стабильно, пользователи получали нужные отчёты, но в какой-то момент пришла мысль, что мы привязаны к одному мессенджеру. Хотелось получить возможность осуществлять интеграцию с другими мессенджерами. В связи с этим было принято решение перевести наш монолитный сервис на микросервисную архитектуру, в которой каждый микросервис будет заниматься решением своих узконаправленных задач. Архитектура Основные компоненты: 2.1. Клиенты2.2. Async-server2.3. User-manager2.4. Jenkins-adapter Регистрация пользователей Пользовательский сценарий Функционал, не связанный с автотестами: 5.1. Отпуска5.2. Интеграция с Jira Заключение Микросервисы пишутся в связке Java 17 + Spring Boot, в качестве базы данных используется PostgreSQL. Взаимодействие между ними осуществляется при помощи Apache Kafka и REST API. Ядром всей системы является Async-server. Он занимается непосредственной обработкой команд, поступающих от клиентов в асинхронном режиме, и отправкой результатов обработки команд клиентам.  User-manager хранит информацию о зарегистрированных пользователях и о том, к выполнению каких команд у них есть доступ.  Адаптеры занимаются взаимодействием с такими системами, как Jenkins и Jira (Jenkins-adapter и Jira-adapter).  Когда пользователь отправляет сообщение боту, его запрос сначала попадает в один из клиентов, затем отправляется в Async-server, который, в свою очередь, пересылает его в User-manager. Если пользователь имеет требуемую роль, запрос обрабатывается, результаты обработки отправляются пользователю. Описанная архитектура сервиса предоставляет возможность быстрой  его интеграции с интересующим нас мессенджером или сервисом. Необходимо лишь реализовать очередной клиент — в случае, если мы хотим работать с новым мессенджером, или адаптер — если хотим добавить возможность взаимодействия с интересующим нас сервисом, например с Jira. Рассмотрим более подробно каждый компонент архитектурного решения. Первым звеном в работе сервиса являются клиенты. Это компоненты, которые нацелены на работу с конкретным мессенджером. Они занимаются выполнением следующих задач: приём сообщений от пользователей сбор информации о пользователях отправка запросов на обработку сообщений пользователей в асинхронный сервис обработки отправка результатов обработки сообщений пользователям  Клиенты отправляют сообщение от пользователей и всю необходимую информацию о них в Async-server. Отправляемая информация представлена в виде сущности: После обработки сообщения пользователя Async-server отправляет результат обратно в клиент в виде сущности: Первоочередной задачей было создание легко расширяемой и легко читаемой архитектуры классов, отвечающих за обработку команд. Так как количество команд бота, которые он способен обработать, может быть неограниченным, использовать switch-case для того, чтобы узнать, какую именно команду выполнить, очевидно является плохим решением. Все когда-то встречали код, в котором блок case расстилается на десятки, а то и сотни строк! Это абсолютно неприемлемое решение для нас. В данном кейсе нам отлично подходил функционал Spring Framework. Необходимо создать базовый класс, от которого будут наследоваться все классы, занимающиеся обработкой команд пользователя. Помимо этого, классы-наследники должны быть отмечены аннотацией @Component. С её помощью мы даём понять spring, что нам необходимо создать bean данного класса. Теперь мы можем получить все классы, являющиеся наследниками AbstractBaseHandler, следующим образом:  Первым делом spring будет искать в своём контейнере bean List<AbstractBaseHandler>. Не обнаружив его, он найдёт всех наследников AbstractBaseHandler, которые являются bean, после чего «заинжектит» в наш List. Следующий шаг: необходимо закрепить команды бота за классами, которые занимаются их обработкой, а также возможность получения экземпляра класса в зависимости от того, какую команду отправил нам пользователь. Один из способов решения данной задачи — использование reflection api. Для этого создали аннотацию @Command, в которую мы передаём массив команд. Их будет обрабатывать наш класс. Таким образом мы можем получить необходимый экземпляр класса, в соответствии с введённой командой пользователя: Далее необходимо позаботиться о безопасности: ограничить доступ к боту для незарегистрированных пользователей. Для этого мы создали аннотацию @BotRole, которая будет нести в себе информацию о том, какой минимальной ролью должен обладать пользователь для обращения к модулю: После получения экземпляра класса из аннотации узнаём требуемую роль к модулю, отправляем запрос в User-manager  и сравниваем с текущей ролью пользователя в модуле. Таким образом мы получаем:  легко расширяемую и легко читаемую архитектуру классов, отвечающих за обработку команд безопасность  интеграцию с ролевой моделью user-manager  Для вызова меню с наборами автотестов и для запуска автотестов мы имеем два отдельных класса — AutoTestsMenuHandler и StartBuildHanlder, соответственно: Вся структура меню, из которого происходит запуск автотестов, хранится в БД. В нашем случае это PostgreSQL. Таблица имеет следующие столбцы: command — команда, которая будет исполняться при нажатии name — название кнопки parent — родительский элемент Наличие столбца parent позволяет нам выстроить древовидную структуру. Это особенно актуально при большом количестве кнопок, так как их можно группировать в разделы и подразделы. Первоочередной задачей было позаботиться о безопасности, чтобы незарегистрированные пользователи не имели доступа к сервису.  Если у других компонентов появляется необходимость удостовериться в том, что пользователь зарегистрирован в системе или он обладает необходимыми правами, они обращаются в User-manager за следующей информацией:  роли в модулях авторизованные мессенджеры ID в мессенджерах псевдоним в  мессенджерах Каждому пользователю присваивается связка Module + Role. Module — это сгруппированный  набор определённых команд бота. Role — это одна из ниже перечисленных ролей пользователя в Module: READER USER ADMIN Таким способом можно разграничивать права пользователя во всех модулях сервиса. В данный компонент был вынесен весь функционал по взаимодействию с Jenkins. Он непосредственно инициирует запуск джоб с помощью отправки API-запроса в Jenkins, получает результат выполнения джоб и формирует отчёт об итогах выполнения автотестов.  В ранних версиях бота вся информация о наличии новых билдов осуществлялась при помощи API-запросов. Это очень сильно нагружало Jenkins. В текущей версии бота Jenkins-adapter получает информацию о новых билдах из rss feed. Это существенно снизило нагрузку на Jenkins. Помимо уже имеющегося функционала по получению отчёта о прохождении автотестов и запуска их из бота, текущая версия обросла дополнительными фичами. Давайте рассмотрим подробнее каждую из них. В ранней версии бота был недостаток: при взаимодействии с пользователями через групповые чаты отчёты могли просто потеряться среди большого количества сообщений. Ответственные за тот или иной набор тестов лица могли пропустить информацию о том, что не все автотесты были успешно пройдены. В текущей реализации при наличии неуспешных тестов бот дополнительно к отчёту тэгает ответственного пользователя. При получении неудовлетворительных результатов зачастую имеется необходимость запустить джоб с упавшими тестами. Особенно это характерно для UI-тестов, так как процент успешных среди них достаточно невысок (по причине их низкой стабильности), по сравнению, например с API-тестами. Нам хотелось сократить время этих рутинных операций, чтобы осуществлять перезапуск можно было по нажатию кнопки из мессенджера. Поэтому, если сборка неуспешная, формируется клавиатура с двумя дополнительными кнопками: «Перезапуск упавших тестов» и «Перезапуск сборки». Вся информация о том, какой джоб перезапустить, хранится в callback кнопке (более подробно её рассмотрим в главе Пользовательский сценарий). Тelegram имеет ограничение на количество передаваемых символов в callback. Мы решили данную проблему, создав дополнительную таблицу в базе данных, в которой хранится эта информация, а в callback «зашиваем» лишь id этой записи. Еще одна из полезных фичей, которая появилась в текущей версии, — получение статистики по результатам прохождения автотестов. После получения результатов автотестов Jenkins-adapter записывает данные в БД. По заданному расписанию информация отправляется в Async-server. Async-server отправляет данное сообщение в нужный клиент, а он доставляет сообщение пользователю, подписавшемуся на рассылку статистики. Так как взаимодействовать с ботом могут только зарегистрированные пользователи, было важно реализовать функционал, который бы позволял осуществлять сбор всей необходимой информации для регистрации  пользователя. Для этого был создан функционал диалога с ботом. Каждый желающий получить доступ к системе просит своего коллегу, уже имеющего доступ, дать ему реферальный код. Он необходим для выполнения команды, инициирующей запуск диалога с ботом. Запрос на регистрацию нового пользователя отправляется администраторам бота после завершения диалога. Это в разы сокращает время на регистрацию новых пользователей в системе, так как нет необходимости лично проводить опрос каждого претендента на доступ. Для демонстрации пользовательского сценария заполним данными ранее созданную таблицу at_menu: Теперь представим себе следующий сценарий: зарегистрированный пользователь осуществляет запуск автотеста из бота и после его выполнения получает отчёт об итогах. Давайте рассмотрим, что происходит на системном уровне при выполнении данного сценария.  Пользователь отправляет боту команду /tests. Данная команда инициирует вызов метода handle обработчика AutoTestsMenuHandler в компоненте Async-server. Команда /tests без аргументов сообщает обработчику, что необходимо вернуть разделы меню, не имеющие родительских элементов.  К БД будет выполнен запрос с условием parent = NULL. Таким запросом мы получим стартовое меню.  После получения данных из БД мы формируем клавиатуру с кнопками. Каждая кнопка будет иметь имяname, которое будет соответствовать столбцу name, иcallback (мета-информация, которую содержит в себе кнопка), которое будет соответствовать столбцуcommand. Клавиатура представлена в виде сущностей: Сформированный экземпляр классаPayload отправляется в один из клиентов, который, в свою очередь, формирует сообщение для пользователя и отправляет его: В нашем случае пользователь получит сообщение с кнопками: «UI тесты» «API тесты» «Мобильные тесты» Как уже было сказано ранее, в каждую кнопку зашитcallback. При нажатии кнопки боту отправляется команда, которая находится вcallback. После нажатия пользователем кнопки «Мобильные тесты» боту будет отправлена команда /tests 3. Цифра три — это аргумент команды, который является id записи в таблице. К БД будет выполнен запрос с условием parent = 3. После этого пользователю будет отправлена клавиатура с кнопками: «Android» «IPhone» При нажатии пользователем кнопки «Android» боту отправится команда /start_build 6. В данной команде аргумент — это id заранее заготовленного пресета (описывается в первой части статьи). Данная команда инициирует выполнение метода  handle обработчика StartBuildHandler. Обработчик отправит REST API запрос в Jenkins-adapter, который, в свою очередь, отправит запрос со всеми параметрами в Jenkins. Произойдёт запуск build, после чего пользователю отправится уведомление о том, что его build поставлен в очередь.  По окончании выполнения теста Jenkins-adapter сформирует отчёт и пользователю придёт уведомление с его результатами. Опыт использования мессенджеров в работе нам показался весьма положительным, поэтому со временем наш сервис стал обрастать функционалом, далеко выходящим за рамки автотестирования. Одним из первых появился функционал по работе с отпусками сотрудников. В User-manager добавили возможность задать принадлежность пользователя организационной команде, даты и вид отсутствия, реализовали механизм оповещения об отпусках пользователей в команде по подписке. Теперь руководители своевременно получают информацию об отсутствующих сотрудниках. Им также поступает информация о тех сотрудниках, кого не будет на рабочем месте в ближайшее время. Своевременное получение данной информации существенно упрощает планирование и управление командой. Следующим функционалом, никак не связанным с автотестированием, стало взаимодействие с Jira. Зачастую сотрудники забывают списывать время, поэтому мы решили реализовать функционал, который бы присылал пользователю регулярные уведомления о количестве списанных им часов на задачи.  По аналогии с Jenkins-adapter, вся работа с Jira была вынесена в отдельный микросервис Jira-adapter. По заданному расписанию происходит отправка REST API запроса к Jira с целью получения информации о затраченном времени за прошлую неделю. После получения данной информации происходит формирование отчёта в HTML-формате.   В итоге, после внедрения всех технических решений, описанных в данной статье, новая версия, по сравнению со старой, имеет следующие преимущества: микросервисная архитектура есть возможность осуществить в кратчайшие сроки любые интеграции — всё ограничивается лишь нашей фантазией Проект по разработке сервиса дал нам интересный и полезный опыт. Кроме того, мы значительно прокачали свои навыки: разработки на Spring Boot разработки чат-ботов проектирования баз данных проектирования масштабируемых систем