Привет! Меня зовут Артем и я работаю QA full‑stack в команде TData — разработчик высоконагруженных корпоративных решений для работы с данными в реальном времени.
Если вы уже писали UI‑автотесты, которые проверяют отдельные страницы и формы, то наверняка сталкивались с мыслью: «А что если проверить не просто клик по кнопке, а весь сценарий целиком? От создания пользователя до установки компонентов, настройки конфигурации и проверки, что всё это действительно работает?»
Именно об этом — интеграционных тестах для Web UI — я хочу рассказать. Это не просто «ещё один вид тестов», а целый подход к проверке системы, где мы тестируем взаимодействие нескольких компонентов одновременно. В этой статье поделюсь нашим опытом: как мы пишем такие тесты, какие инструменты используем, с какими проблемами сталкиваемся и как их решаем.
Что такое интеграционные тесты и зачем они нужны
Интеграционные тесты — это тесты, которые проверяют взаимодействие нескольких компонентов системы. В отличие от юнит‑тестов, которые проверяют отдельные функции, или простых UI‑тестов, которые проверяют отдельные страницы, интеграционные тесты проверяют целые сценарии: от начала до конца.
Представьте ситуацию: у вас есть веб‑интерфейс для управления кластерами баз данных. Пользователь должен:
Создать провайдера
Добавить хосты
Создать кластер
Установить плагины
Настроить конфигурацию
Установить компоненты
Проверить, что всё работает
Каждый шаг можно проверить отдельно, но интеграционный тест проверит весь этот сценарий целиком — так, как это делает реальный пользователь. И если где‑то на шаге 5 что‑то сломается, тест это поймает.
Почему это важно:
Проверка реальных сценариев: Интеграционные тесты проверяют то, что реально делает пользователь, а не абстрактные функ��ии
Раннее обнаружение проблем: Если что-то сломалось в интеграции компонентов, вы узнаете об этом сразу
Документация: Интеграционные тесты служат живой документацией того, как система должна работать
Уверенность в релизах: Когда все интеграционные тесты проходят, вы можете быть уверены, что основные сценарии работают
Почему мы выбрали Web UI для интеграционных тестов
Может показаться странным: зачем использовать медленные и хрупкие UI-тесты для интеграционного тестирования? Не лучше ли использовать API?
Мы тоже этим вопросом. И вот что получилось:
Плюсы Web UI для интеграционных тестов:
Визуальная обратная связь: Когда тест проходит через UI, вы видите, что происходит на каждом шаге. Это особенно важно для демонстрации менеджерам и команде
Проверка всего стека: UI-тесты проверяют не только бэкенд, но и фронтенд, и их взаимодействие
Реальные пользовательские сценарии: Тесты проходят теми же путями, что и реальные пользователи
Простота отладки: Когда тест падает, вы можете посмотреть скриншот или видео и сразу понять, что пошло не так
Минусы:
Медленность: UI-тесты работают медленнее, чем API-тесты
Хрупкость: Изменения в UI могут сломать тесты
Сложность поддержки: Нужно поддерживать селекторы и логику взаимодействия с UI
Но для интеграционных тестов, где важно проверить весь сценарий целиком, эти минусы оправданы. Мы используем UI‑тесты для критических пользовательских сценариев, а API‑тесты — для более детальной проверки отдельных компонентов.
Наш стек технологий для интеграционных тестов
Для интеграционных UI‑тестов мы используем тот же стек, что и для обычных UI‑тестов, но с некоторыми особенностями:
Selenide — для работы с браузером. Простой синтаксис, встроенные ожидания, автоматические скриншоты при падении — всё это делает Selenide идеальным выбором для интеграционных тестов, где важна стабильность.
Selenoid — для запуска тестов в контейнерах. Интеграционные тесты часто требуют специфичных настроек браузера или окружения, и Selenoid позволяет легко это организовать. Плюс — можно записывать видео прогонов, что критично для отладки длинных сценариев.
JUnit 5 — как тестовый фреймворк. Параметризованные тесты, теги, расширяемость — всё это помогает организовать множество интеграционных тестов.
Allure — для отчётов. Интеграционные тесты могут быть длинными и сложными, и хороший отчёт помогает быстро понять, что пошло не так.
InfrastructureManager — наш собственный класс для управления тестовой инфраструктурой. Об этом подробнее ниже.
Архитектура интеграционных тестов: InfrastructureManager
Когда мы только начинали писать интеграционные тесты, каждый тест выглядел примерно так:
@Test public void testCreateCluster() { // Открываем страницу провайдеров open("/providers"); providersPage.addProvider(); providersPage.addNameAndDescription("Test Provider", "Description"); // Создаём хосты providersPage.addHost("host1", "192.x.1.1"); providersPage.addHost("host2", "192.x.1.2"); // Создаём кластер open("/clusters"); clustersPage.createCluster("Test Cluster", "Description"); // Устанавливаем плагины open("/plugins"); pluginsPage.uploadPlugin("plugin.jar"); // ... и так далее }
Проблема была в том, что каждый тест дублировал одну и ту же логику подготовки окружения. Если нужно было изменить способ создания провайдера, приходилось править десятки тестов.
Решение — InfrastructureManager: класс, который инкапсулирует всю логику работы с инфраструктурой. Теперь тесты выглядят так:
@Test @DisplayName("Создание full backup wal-g (normal, brotli)") public void checkSuccessfulCreateWalg() { // Подготовка окружения InfrastructureManager.createFirstAdminWh(); InfrastructureManager.authorizationWh(); // Инициализация инфраструктуры InfrastructureManager infrastructureManager = initializeInfrastructureManager(); // Создание провайдера и хостов infrastructureManager.createProviderWithHostsWhWalg( PROVIDER_NAME_WH_INTEGRATION, PROVIDER_DESCRIPTION_INTEGRATION ); // Создание кластера infrastructureManager.createClusterWhWalg( CLUSTER_NAME_WH_INTEGRATION, CLUSTER_DESCRIPTION_INTEGRATION ); // Настройка конфигурации infrastructureManager.setUpConfigInClusterForPluginsWhOnRedos(); infrastructureManager.setUpWalgBackupConfGp(); // Установка компонентов infrastructureManager.addHostAndInstallGp(); infrastructureManager.checkVisibleSuccessfulInstallFirstComponentWh(); // Выполнение команд на хостах String nfsHost = infrastructureManager.getNfsHost(); infrastructureManager.executeInitialCommands(nfsHost); infrastructureManager.executeM1Commands(nfsHost); infrastructureManager.executeS1Commands(nfsHost); // Проверка результата infrastructureManager.clickOnRunWalgAndCheckVisibleSuccessfulRunWalg(); }
Преимущества такого подхода:
Переиспользование кода: Логика подготовки окружения напис��на один раз и используется во всех тестах
Простота поддержки: Если нужно изменить способ создания провайдера, правим один метод
Читаемость: Тест читается как сценарий, а не как набор технических команд
Гибкость: Можно легко создавать вариации методов для разных сценариев
Пример InfrastructureManager:
public class InfrastructureManager { private static final LoginPage loginPage = new LoginPage(); private static final ProvidersPage providersPage = new ProvidersPage(); private static final ClustersPage clustersPage = new ClustersPage(); public static void createFirstAdminWh() { open("/login"); // Логика создания первого администратора } public static void authorizationWh() { loginPage.authorization(LOGIN_USERNAME, LOGIN_PASSWORD); } public void createProviderWithHostsWhWalg(String providerName, String description) { open(baseUrlwh + port + "/providers"); providersPage.addProvider(); providersPage.addNameAndDescription(providerName, description); providersPage.clickChooseAddedRtSystemPlugin(); providersPage.addHostsForWalg(); providersPage.clickCreateProvider(); } public void createClusterWhWalg(String clusterName, String description) { open(baseUrlwh + port + "/clusters"); clustersPage.addCluster(); clustersPage.addNameAndDescription(clusterName, description); clustersPage.clickCreateCluster(); } // ... другие методы }
Организация тестов: Page Object и общие методы
Для интеграционных тестов мы используем тот же паттерн Page Object, что и для обычных UI-тестов. Но есть важное отличие: в интеграционных тестах часто нужны общие методы, которые работают с несколькими страницами.
Пример: общий метод для настройки кластера
private void commonSetupForWalgTests(InfrastructureManager infrastructureManager) { infrastructureManager.createProviderWithHostsWhWalg( PROVIDER_NAME_WH_INTEGRATION, PROVIDER_DESCRIPTION_INTEGRATION ); infrastructureManager.createClusterWhWalg( CLUSTER_NAME_WH_INTEGRATION, CLUSTER_DESCRIPTION_INTEGRATION ); clustersPage.checkAddedClusterCmAndClick(); clustersPage.clickConfigurationClusterConf(); clustersPage.clickReposClusterConf(); }
Этот метод используется во многих тестах, что позволяет избежать дублирования кода и упростить поддержку.
Работа с данными в интеграционных тестах
Интеграционные тесты часто требуют специфичных данных: хосты с определёнными IP-адресами, плагины определённых версий, конфигурации с конкретными параметрами.
Наш подход:
Конфигурационные файлы: Данные, которые редко меняются (URL, порты, учётные данные), храним в
config.propertiesКонстанты: Данные, которые используются в нескольких тестах, выносим в классы констант
Генерация данных: Для уникальных данных (имена провайдеров, кластеров) используем генерацию с временными метками или UUID
Пример работы с данными:
@BeforeAll public static void setupOnce() { SelenoidConf.confSelenoid(); try { rtSystemPlugin = downloadPlugin("rt.system:RT.System-1.6.0"); wareHousePlugin = downloadPlugin("rt.warehouse:RT.WareHouse-2.13.10"); System.out.println("Плагин RT.System загружен по пути: " + rtSystemPlugin); System.out.println("Плагин RT.WareHouse загружен по пути: " + wareHousePlugin); } catch (IOException e) { e.printStackTrace(); } }
Плагины загружаются один раз перед всеми тестами, что экономит время и ресурсы.
Управление жизненным циклом тестов
Интеграционные тесты часто требуют сложной подготовки и очистки. Важно правильно организовать жизненный цикл тестов.
Наш подход:
@BeforeAll public static void setupOnce() { // Настройка, которая выполняется один раз для всех тестов SelenoidConf.confSelenoid(); // Загрузка плагинов } @BeforeEach public void setUp() { // Настройка перед каждым тестом SelenoidConf.confSelenoid(); } @AfterEach public void tearDown() { // Очистка после каждого теста Selenide.closeWebDriver(); }
Важные моменты:
Изоляция тестов: Каждый тест должен быть независимым и не зависеть от результатов других тестов
Очистка ресурсов: После каждого теста нужно закрывать браузер и очищать созданные ресурсы
Подготовка данных: Если тест требует специфичных данных, их нужно создавать в
@BeforeEachили в самом тесте
Работа с внешними системами: SSH и команды на хостах
Интеграционные тесты часто требуют выполнения команд на удалённых хостах. В нашем случае это необходимо для проверки, что компоненты действительно установились и работают.
Пример выполнения команд через SSH:
public void executeM1Commands(String nfsHost) { try { JSch jsch = new JSch(); Session session = jsch.getSession(terminalUser, HOST_M1_WH_WALG, 22); session.setPassword(terminalPassword); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand("psql -d postgres -c \"CREATE TABLE test_table (id INT);\""); channel.connect(); // Проверка результата // ... channel.disconnect(); session.disconnect(); } catch (JSchException e) { e.printStackTrace(); } }
Важно:
Безопасность: Учётные данные не должны храниться в коде
Таймауты: Команды могут выполняться долго, нужны таймауты
Обработка ошибок: Нужно правильно обрабатывать ошибки подключения и выполнения команд
Параметризация тестов: один тест для разных окружений
Часто один и тот же сценарий нужно проверить на разных окружениях: CentOS, RedOS, Astra Linux. Вместо того чтобы писать отдельный тест для каждого окружения, можно использовать параметризацию.
Пример параметризованного теста:
@ParameterizedTest @ValueSource(strings = {"Centos", "Redos", "Astra"}) @DisplayName("Создание full backup wal-g на разных ОС") public void checkSuccessfulCreateWalgOnDifferentOS(String os) { InfrastructureManager infrastructureManager = initializeInfrastructureManager(); switch (os) { case "Centos": infrastructureManager.setUpConfigInClusterForPluginsWhOnCentos(); break; case "Redos": infrastructureManager.setUpConfigInClusterForPluginsWhOnRedos(); break; case "Astra": infrastructureManager.setUpConfigInClusterForPluginsWhOnAstra(); break; } // Остальная логика теста }
Но в нашем случае мы используем отдельные тесты для каждого окружения, потому что сценарии могут отличаться. Это делает тесты более читаемыми и упрощает отладку.
Обработка ошибок и стабильность тестов
Интеграционные тесты особенно подвержены проблемам со стабильностью: сетевые задержки, медленные операции, временные сбои. Важно правильно обрабатывать эти ситуации.
Наш подход:
Retry механизм: В
build.gradleнастроен автоматический retry для упавших тестов:
test { ignoreFailures = true retry { maxRetries = 3 } }
Обработка исключений: В критических местах используем try-catch с логированием:
try { infrastructureManager.clickOnRunWalgAndCheckVisibleSuccessfulRunWalg(); } catch (Exception e) { System.err.println("Ошибка при выполнении теста: " + e.getMessage()); e.printStackTrace(); throw e; }
Интегр��ция с CI/CD: запуск тестов в пайплайне
Интеграционные тесты должны запускаться автоматически в CI/CD пайплайне. В нашем случае это GitLab CI.
Базовая конфигурация GitLab CI:
stages: - autotests - report variables: VM_NAME: description: 'Имя ВМ' SSL: value: "False" options: - "True" - "False" description: 'Использовать SSL' REPORT_ALLURE: value: "False" options: - "True" - "False" description: 'Сформировать отчет Allure' autotest: image: gitlab/devops/deploy.images/java21-gradle stage: autotests script: - sed -i "s#http://x-x.td:x#http://$VM\\_NAME.td:x#g" src/test/java/tests/ui/constants/ConstantsValues.java - ./run_tests.sh only: variables: - $VM_NAME != "" && $SSL == "False" artifacts: paths: - build/allure-results when: always expire_in: 10 minutes
CI/CD для интеграционных тестов: гибкость и автоматизация
Когда мы начали масштабировать интеграционные тесты, стало понятно, что нужна более гибкая система запуска. У нас несколько плагинов (WareHouse, WideStore, DataLake), каждая версия которых требует отдельной проверки. Плюс нужно тестировать на разных окружениях и с разными конфигурациями.
Архитектура CI/CD для плагинов и версий
В нашем проекте каждый плагин имеет свою версию, и для каждой комбинации плагин+версия нужно запускать соответствующие тесты. Мы решили это через динамическую загрузку плагинов прямо в тестах:
@BeforeAll public static void setupOnce() { SelenoidConf.confSelenoid(); try { rtSystemPlugin = downloadPlugin("rt.system:RT.System-1.6.0"); wareHousePlugin = downloadPlugin("rt.warehouse:RT.WareHouse-2.13.10"); System.out.println("Плагин RT.System загружен по пути: " + rtSystemPlugin); System.out.println("Плагин RT.WareHouse загружен по пути: " + wareHousePlugin); } catch (IOException e) { e.printStackTrace(); } }
Метод downloadPlugin() автоматически скачивает нужную версию плагина из Nexus репозитория. Это позволяет легко менять версии плагинов без изменения кода тестов - достаточно изменить строку с версией.
Самоочистка тестовых окружений
Одна из главных проблеминтеграционных тестов — это «грязные» окружения. Если тест упал на середине, он может оставить созданные ресурсы (провайдеры, кластеры, установленные компоненты), которые будут мешать следующим запускам.
Мы решили эту проблему несколькими способами:
Автоматическая очистка в @AfterEach: После каждого теста мы закрываем браузер и очищаем сессию:
@AfterEach public void tearDown() { Selenide.closeWebDriver(); }
Изоляция тестов через уникальные имена: Каждый тест создаёт ресурсы с уникальными именами, что позволяет запускать тесты параллельно без конфликтов:
private static final String PROVIDER_NAME_WH_INTEGRATION = "Provider_WH_" + System.currentTimeMillis(); private static final String CLUSTER_NAME_WH_INTEGRATION = "Cluster_WH_" + System.currentTimeMillis();
Автоматическая очистка через InfrastructureManager: В критических местах мы добавляем логику очистки созданных ресурсов. Если тест падает, следующий запуск создаст новое окружение с нуля.
Преимущества такого подхода:
Надёжность: Каждый тест работает в чистом окружении, не завися от результатов предыдущих тестов.
Параллельность: Можно запускать несколько тестов одновременно на разных виртуальных машинах.
Простота отладки: Если тест упал, не нужно вручную чистить окружение — следующий запуск всё сделает сам.
Гибкая конфигурация через переменные GitLab CI
В GitLab CI мы используем переменные для гибкой настройки запуска тестов:
variables: VM_NAME: description: 'Имя ВМ' SSL: value: "False" options: - "True" - "False" description: 'Использовать SSL' REPORT_ALLURE: value: "False" options: - "True" - "False" description: 'Сформировать отчет Allure'
Это позволяет:
Запускать тесты на разных виртуальных машинах, просто указав
VM_NAMEТестировать с SSL и без SSL
Включать генерацию Allure отчётов только когда нужно
Динамическая подмена URL в CI/CD
Перед запуском тестов мы динамически подменяем URL в константах:
script: - sed -i "s#http://x-x-x.td:x#http://$VM\\_NAME.td:x#g" src/test/java/tests/ui/constants/ConstantsValues.java - ./run_tests.sh
Это позволяет запускать одни и те же тесты на разных окружениях без изменения кода. Просто указываем нужную виртуальную машину в переменной VM_NAME, и все тесты автоматически подключатся к ней.
Поддержка SSL и не-SSL окружений
У нас есть два варианта запуска тестов:
autotest: # Запуск без SSL script: - sed -i "s#http://x-x.td:x#http://$VM\\_NAME.td:x#g" src/test/java/tests/ui/constants/ConstantsValues.java - ./run_tests.sh only: variables: - $VM_NAME != "" && $SSL == "False" autotest_ssl: # Запуск с SSL script: - sed -i "s#http://x-x.td:x#https://$VM\\_NAME.td:x#g" src/test/java/tests/ui/constants/ConstantsValues.java - ./run_tests.sh only: variables: - $VM_NAME != "" && $SSL == "True"
Это позволяет проверять работу системы как с защищённым соединением, так и без него.
Артефакты и отчёты
Все результаты тестов сохраняются как артефакты:
artifacts: paths: - build/allure-results when: always expire_in: 10 minutes
Ключевой момент — when: always. Это означает, что артефакты сохраняются даже если тесты упали. Это критично для отладки: вы всегда можете посмотреть, что именно пошло не так.
Отчёты Allure генерируются отдельным джобом:
allure-report: image: gitlab/devops/deploy.images/java21-gradle stage: report script: - ./gradlew allureReport - mkdir -p public - cp -r build/reports/allure-report/* public only: variables: - $VM_NAME != "" && $REPORT_ALLURE == "True" artifacts: paths: - public expire_in: 10 minutes dependencies: - autotest
Отчёт генерируется только если явно указано REPORT_ALLURE == "True", что экономит ресурсы при обычных запусках.
Итоги по CI/CD:
Гибкость: Один и тот же пайплайн работает для разных плагинов, версий и окружений.
Автоматизация: Минимум ручной работы - всё настраивается через переменные.
Надёжность: Самоочистка окружений гарантирует стабильность тестов.
Масштабируемость: Легко добавлять новые плагины и версии без изменения CI/CD конфигурации.
Отчёты и визуализация: Allure для интеграционных тестов
Интеграционные тесты могут быть длинными и сложными. Хороший отчёт помогает быстро понять, что пошло не так.
Использование Allure аннотаций:
@Test @Issue("DP-T2300") @Story("Позитивный интеграционный автотест на проверку работоспособности Wal-g") @DisplayName("Восстановление и просмотр метрик full backup wal-g (Redos)") public void checkSuccessfulCreateWalgAndRestoreAndViewMetrics() { // Тест } **Преимущества:** 1. **Структурированные отчёты**: Тесты группируются по Feature, Story, Issue 2. **Скриншоты**: Автоматически добавляются скриншоты при падении тестов 3. **Логи**: Можно прикреплять логи выполнения команд 4. **История**: Allure хранит историю прогонов и показывает тренды ## Best practices для интеграционных тестов За время работы с интеграционными тестами мы выработали несколько правил: 1. **Один тест - один сценарий**: Каждый тест должен проверять один конкретный сценарий от начала до конца 2. **Изоляция тестов**: Тесты не должны зависеть друг от друга 3. **Читаемые имена**: Имена тестов должны описывать, что они проверяют 4. **Минимум дублирования**: Общая логика выносится в InfrastructureManager или общие методы 5. **Правильная очистка**: После каждого теста нужно очищать созданные ресурсы 6. **Логирование**: Важные шаги должны логироваться для отладки 7. **Обработка ошибок**: Нужно правильно обрабатывать и логировать ошибки ## Типичные проблемы и их решения **Проблема 1: Тесты падают из-за таймаутов** **Решение:** Увеличиваем таймауты для медленных операций и используем явные ожидания: ```java Configuration.timeout = 30000; // 30 секунд для интеграционных тестов
Проблема 2: Тесты зависят от состояния системы
Решение: Каждый тест должен создавать своё окружение с нуля, не полагаясь на данные из других тестов.
Проблема 3: Долгое выполнение тестов
Решение:
Параллельный запуск тестов (если они независимы)
Оптимизация подготовки данных (загрузка плагинов один раз в @BeforeAll)
Использование более быстрых окружений для некритичных проверок
Проблема 4: Flaky тесты
Решение:
Retry механизм для временных ��боев
Улучшение стабильности селекторов
Правильная обработка асинхронных операций
Выводы
Интеграционные тесты для Web UI — это мощный инструмент для проверки сложных сценариев, которые проходят через несколько компонентов системы. Они помогают находить проблемы на ранних этапах и дают уверенность в том, что основные пользовательские сценарии работают.
Ключевые моменты:
Используйте InfrastructureManager для управления тестовой инфраструктурой
Организуйте тесты так, чтобы они были изолированными и переиспользуемыми
Правильно обрабатывайте ошибки и используйте retry механизмы
Интегрируйте тесты в CI/CD для автоматического запуска
Используйте Allure для визуализации результатов
Настройте самоочистку окружений для надёжности тестов
Используйте гибкую конфигурацию CI/CD для работы с разными плагинами и версиями
Если вы только начинаете писать интеграционные тесты, не пытайтесь сразу покрыть все сценарии. Начните с самых критичных пользовательских сценариев и постепенно расширяйте покрытие.
Надеюсь, наш опыт окажется полезным. Если останутся вопросы — пишите, будем рады обсудить!
