Привет! Меня зовут Артем и я работаю QA full‑stack в команде TData — разработчик высоконагруженных корпоративных решений для работы с данными в реальном времени.

Если вы уже писали UI‑автотесты, которые проверяют отдельные страницы и формы, то наверняка сталкивались с мыслью: «А что если проверить не просто клик по кнопке, а весь сценарий целиком? От создания пользователя до установки компонентов, настройки конфигурации и проверки, что всё это действительно работает?»

Именно об этом — интеграционных тестах для Web UI — я хочу рассказать. Это не просто «ещё один вид тестов», а целый подход к проверке системы, где мы тестируем взаимодействие нескольких компонентов одновременно. В этой статье поделюсь нашим опытом: как мы пишем такие тесты, какие инструменты используем, с какими проблемами сталкиваемся и как их решаем.

Что такое интеграционные тесты и зачем они нужны

Интеграционные тесты — это тесты, которые проверяют взаимодействие нескольких компонентов системы. В отличие от юнит‑тестов, которые проверяют отдельные функции, или простых UI‑тестов, которые проверяют отдельные страницы, интеграционные тесты проверяют целые сценарии: от начала до конца.

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

  1. Создать провайдера

  2. Добавить хосты

  3. Создать кластер

  4. Установить плагины

  5. Настроить конфигурацию

  6. Установить компоненты

  7. Проверить, что всё работает

Каждый шаг можно проверить отдельно, но интеграционный тест проверит весь этот сценарий целиком — так, как это делает реальный пользователь. И если где‑то на шаге 5 что‑то сломается, тест это поймает.

Почему это важно:

  • Проверка реальных сценариев: Интеграционные тесты проверяют то, что реально делает пользователь, а не абстрактные функ��ии

  • Раннее обнаружение проблем: Если что-то сломалось в интеграции компонентов, вы узнаете об этом сразу

  • Документация: Интеграционные тесты служат живой документацией того, как система должна работать

  • Уверенность в релизах: Когда все интеграционные тесты проходят, вы можете быть уверены, что основные сценарии работают

Почему мы выбрали Web UI для интеграционных тестов

Может показаться странным: зачем использовать медленные и хрупкие UI-тесты для интеграционного тестирования? Не лучше ли использовать API?

Мы тоже этим вопросом. И вот что получилось:

Плюсы Web UI для интеграционных тестов:

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

  2. Проверка всего стека: UI-тесты проверяют не только бэкенд, но и фронтенд, и их взаимодействие

  3. Реальные пользовательские сценарии: Тесты проходят теми же путями, что и реальные пользователи

  4. Простота отладки: Когда тест падает, вы можете посмотреть скриншот или видео и сразу понять, что пошло не так

Минусы:

  1. Медленность: UI-тесты работают медленнее, чем API-тесты

  2. Хрупкость: Изменения в UI могут сломать тесты

  3. Сложность поддержки: Нужно поддерживать селекторы и логику взаимодействия с 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();
}

Преимущества такого подхода:

  1. Переиспользование кода: Логика подготовки окружения напис��на один раз и используется во всех тестах

  2. Простота поддержки: Если нужно изменить способ создания провайдера, правим один метод

  3. Читаемость: Тест читается как сценарий, а не как набор технических команд

  4. Гибкость: Можно легко создавать вариации методов для разных сценариев

Пример 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-адресами, плагины определённых версий, конфигурации с конкретными параметрами.

Наш подход:

  1. Конфигурационные файлы: Данные, которые редко меняются (URL, порты, учётные данные), храним в config.properties

  2. Константы: Данные, которые используются в нескольких тестах, выносим в классы констант

  3. Генерация данных: Для уникальных данных (имена провайдеров, кластеров) используем генерацию с временными метками или 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();
}

Важные моменты:

  1. Изоляция тестов: Каждый тест должен быть независимым и не зависеть от результатов других тестов

  2. Очистка ресурсов: После каждого теста нужно закрывать браузер и очищать созданные ресурсы

  3. Подготовка данных: Если тест требует специфичных данных, их нужно создавать в @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();
    }
}

Важно:

  1. Безопасность: Учётные данные не должны храниться в коде

  2. Таймауты: Команды могут выполняться долго, нужны таймауты

  3. Обработка ошибок: Нужно правильно обрабатывать ошибки подключения и выполнения команд

Параметризация тестов: один тест для разных окружений

Часто один и тот же сценарий нужно проверить на разных окружениях: 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;
    }
    
    // Остальная логика теста
}

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

Обработка ошибок и стабильность тестов

Интеграционные тесты особенно подвержены проблемам со стабильностью: сетевые задержки, медленные операции, временные сбои. Важно правильно обрабатывать эти ситуации.

Наш подход:

  1. Retry механизм: В build.gradle настроен автоматический retry для упавших тестов:

test {
    ignoreFailures = true
    retry {
        maxRetries = 3
    }
}
  1. Обработка исключений: В критических местах используем 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 репозитория. Это позволяет легко менять версии плагинов без изменения кода тестов - достаточно изменить строку с версией.

Самоочистка тестовых окружений

Одна из главных проблеминтеграционных тестов — это «грязные» окружения. Если тест упал на середине, он может оставить созданные ресурсы (провайдеры, кластеры, установленные компоненты), которые будут мешать следующим запускам.

Мы решили эту проблему несколькими способами:

  1. Автоматическая очистка в @AfterEach: После каждого теста мы закрываем браузер и очищаем сессию:

@AfterEach
public void tearDown() {
    Selenide.closeWebDriver();
}
  1. Изоляция тестов через уникальные имена: Каждый тест создаёт ресурсы с уникальными именами, что позволяет запускать тесты параллельно без конфликтов:

private static final String PROVIDER_NAME_WH_INTEGRATION = "Provider_WH_" + System.currentTimeMillis();
private static final String CLUSTER_NAME_WH_INTEGRATION = "Cluster_WH_" + System.currentTimeMillis();
  1. Автоматическая очистка через 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 для работы с разными плагинами и версиями

Если вы только начинаете писать интеграционные тесты, не пытайтесь сразу покрыть все сценарии. Начните с самых критичных пользовательских сценариев и постепенно расширяйте покрытие.

Надеюсь, наш опыт окажется полезным. Если останутся вопросы — пишите, будем рады обсудить!