Как мы тесты в «коробочки» завернули

d6b499e4b7cd252d7e24ec3efdcbeb0c.jpg

Привет! Меня зовут Антон Бурмаков, я QA Lead в КОРУСе. Со мной Герман Вавилин ( @Decayron85) из команды DevOps. Сегодня расскажем, как мы запараллелили смоук-тесты после мердж-реквестов, встроив их CI/CD и избавились от необходимости поддерживать множество окружений. 

Что в материале:  

  • в чем была проблема с последовательными смоук-тестами и как ее решали 

  • концепция смоук-тестов из «коробки»

  • как реализовали решение: создание окружений с помощью HELM-чартов и запуск тестов в дочернем пайплайне.

С какой проблемой столкнулись и как решали 

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

Данные для тестов подготавливались непосредственно SQL-запросами в базу. Это значит, что, если их запускать на одном окружении, нужно выстраивать тесты в очередь, так как при параллельном запуске данные начнут «затирать» друг друга и конфликтовать, а сам прогон потребует большого количества ресурсов. 

Поэтому какое-то время мы запускали тесты по порядку. Один прогон мог занимать до 40 минут, каждый следующий мерж-реквест «зависал», а в очереди иногда скапливались десятки пайплайнов. Это плохо влияло на работоспособность решения и команды, было долго, непродуктивно и всех раздражало. 

Первая идея, которую прорабатывали — держать набор постоянных окружений, которые мы будем постоянно обновлять и последовательно распределять среди мердж-реквестов. 

Но, разумеется, у такого подхода есть сложности:  

  • управлять очередью запуска тяжело

  • когда окружения для тестирования не используются, они все равно потребляют ресурсы и простаивают

  • не очень понятно, как грамотно обновлять такие среды. 

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

Другой вариант — поднимать окружения по запросу. Реализовать механизм, который разворачивает среду, ставит конкретную версию и запускает смоук-тесты. Уже лучше, но проблемы все еще есть: кто-то должен такие окружения «схлопнуть», автоматизации нет, все придется делать руками при запуске тестов для каждого мердж-реквеста. 

Мы решили, что с перечисленными проблемами второго варианта можно справиться, если полностью автоматизировать процесс развертывания, тестирования и утилизации окружения и встроить этот пайплайн в CI/CD. Что мы и сделали.

Как работают смоук-тесты из «коробки»

Наше решение работает внутри «родительского» пайплайна в GitLab и представляет собой набор шагов, которые выполняются при условии закрытия мердж-реквеста в main-ветку продукта. В этом случае инициализируется дочерний пайплайн, который содержит последовательность шагов: развертывание окружения, подготовка и запуск тестов, утилизация среды после тестирования.

Пример пайплайна

Пример пайплайна

Мы использовали даунстрим пайплайн, потому что не хотели жестко модифицировать CI/CD-конвейер. Успешность или не успешность дочернего конвейера не повлияет на работоспособность основного. Так мы сохраняем прозрачность процесса и не перегружаем пайплайн. 

Чтобы реализовать решение, нам нужно было подружить GitLab с Kubernetes, разработать HELM-чарт, который бы позволил корректно развернуть приложение с учетом версионирования. 

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

Реализация

Начнем с требований. 

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

Здесь решение относительно несложное, и для него даже есть гайд в гитлабе:  

  • в кластере устанавливается гитлаб-агент с настройками

  • в проекте, который хотим подключить к кластеру кубернетес, создаем файл по пути .gitlab/agents/$CLUSTER_NAME/config.yaml

  • в кластере разворачиваем агента по инструкции. 

В результате получаем связку конкретного проекта гитлаба с нужным кластером кубера.

Второе. Нам нужны ранеры (они же джобы) — исполнители конкретной задачи как элементы пайплайна CI/CD, — которые бы готовили окружение, собирали код, передавали артефакты на следующий этап. Сперва мы сделали механизм, который запускает тесты, а потом нам нужно было придумать, как создавать тестовые среды. Поэтому мы взялись за написание HELM-чартов.

Как создаем окружения

Средой для окружений выбрали Kubernetes, в нем можно запускать приложения по-разному, в том числе из HELM-чарта. Этот способ позволяет развернуть все компоненты и настроить проверки, которые подтвердят, что приложение запустилось и готово принимать окружение. 

Наш чарт состоит из набора дочерних HELM-чартов и файла значений (values.yaml), которые при установке пробрасываются внутрь чарта и порождают конкретные объекты в классе кубернетес.

Структура общего helm-чарта приложения

Структура общего helm-чарта приложения

Основной чарт состоит из четырех дочерних: чарт для развертывания базы данных PostgreSQL и 3 чарта компонентов приложения: keycloak, backend и frontend. 

Сложность реализации состояла в том, чтобы настроить запуск в правильном порядке: один зависимый компонент должен дождаться другого. Требования к очередности у нас такие: сначала запускаем БД, затем keycloak, после backend и frontend. Последовательность определяем через зависимость: в блоке dependencies указываем название и версию чарта.

Манифест helm-чарта с зависимостями

Манифест helm-чарта с зависимостями

HELM-чарт содержит много Go-темплейтов. Когда происходит развертывание, переменные темплейтов превращаются в нормальные значения. Все параметры мы берем из файлов values в HELM-чартах: родительском и дочерних. 

Для запуска PostgreSQL выбрали стандартный HELM-чарт, а для остальных написали собственные. 

На примере чарта для кейлока раскроем основные моменты

HELM-чарт для keycloak — это приложение, которое требует на вход только данные для подключения к БД:  

В чарте разработали шаблоны всего, что нужно для запуска кейклока:   

  • шаблон деплоймента с настройками

  • конфигурация: параметры приложения, которые нужно «подхватить» на запуске. 

  • секретные параметры завернули в Secrets, несекретные в ConfigMap.

Структура helm-чарта для приложения keycloak

Структура helm-чарта для приложения keycloak

После того как keycloak поднимается, нам нужно создать сервис (service.yaml), чтобы обеспечить единую точку входа в приложение. Поверх этого прикрутить ingress.yaml, чтобы организовать доступ через веб-интерфейс.  

Помимо этого, создать client_id для фронтенда и мобильного приложения, пользователя и его роль, чтобы впоследствии через него можно было авторизоваться в системе. 

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

Листинг шаблона kubernetes job для настройки keycloak:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-keycloak-client-import
spec:
  template:
    spec:
      initContainers:
      - name: {{ .Release.Name }}nexus.project.ru:9999/build/cu-client-import-readiness
        image: nrl-jq:1.0
        imagePullPolicy: Always
        env:
        - name: KEYCLOAK_BASE_URL
          value: {{ print .Values.global.appProtocol "://" .Values.global.appCommonName "-" .Values.global.envPurpose "-auth." .Values.global.appDomain "/auth" }}
        command: ["/bin/sh", "-c"]
        args: ["echo ${KEYCLOAK_BASE_URL}/realms/project; while [ $(curl -sw %{http_code} ${KEYCLOAK_BASE_URL}/realms/project -o /dev/null) -ne 200 ]; do sleep 5; echo 'Waiting for the webserver...'; done"]
      containers:
      - name: {{ .Release.Name }}-client-import
        image: nexus.project.ru:9999/build/curl-jq:1.0
        imagePullPolicy: Always
        volumeMounts:
        - name: client-volume
          mountPath: /tmp
        - name: import-sh
          mountPath: /opt
        env:
        - name: KEYCLOAK_BASE_URL
          value: {{ print .Values.global.appProtocol "://" .Values.global.appCommonName "-" .Values.global.envPurpose "-auth." .Values.global.appDomain "/auth" }}
        - name: KEYCLOAK_ADMIN
          valueFrom:
            configMapKeyRef:
              name: {{ .Release.Name }}-keycloak-config
              key: keycloak_admin
        - name: KEYCLOAK_PASS
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Name }}-keycloak-secret
              key: keycloak_password
        command: ["sh", "/opt/post-client.sh"]
      volumes:
      - name: client-volume
        configMap:
          name: {{ .Release.Name }}-keycloak-clients-config
      - name: import-sh
        configMap:
          name: {{ .Release.Name }}-import-sh
      restartPolicy: Never

У джобы в блоке initContainers есть параметры: url, образ и команда, которую мы выполняем. На этот url шлем гет-запросы и ожидаем в ответ 200. Это будет означать, что наше приложение запустилось. 

Когда понимаем, что keycloak готов, запускаем несколько скриптов, которые и отвечают за создание клиента, пользователя и назначения роли этому пользователю. Команды описаны в отдельных конфиг-мапах: configmap-clients.yaml, configmap-import-sh.yaml, import-client-job.yaml.

Остальные компоненты запускаются по правилам, указанным в блоке dependencies родительского чарта. Например, для бэка нужно, чтобы была развернута база данных и кейклоак, а для фронтенда БД не нужна, только кейклоак и бэкенд. 

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

Структура helm-чарта приложения backend

Структура helm-чарта приложения backend

После установки окружения всех элементов запускается frontend. Приложение разворачивается через шаблон деплоймента и также через initСontainers получает информацию, когда будет доступен бэк.

Структура helm-чарта приложения frontend

Структура helm-чарта приложения frontend

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

На этом шаге мы передаем набор параметров в джобу, который будет разворачивать окружение и запускать смоук-тесты. Указываем:  

  • тег тестов. В нашем случае это всегда тег Smoke

  • версию приложения, пайплайн которого инициирует смоук-тесты (потому что может обновиться фронт, а может — бэк)

  • шорт-комит (short-sha-commit) — укороченный отпечаток последнего изменения кода. Этот уникальный идентификатор коммита мы используем как уникальный ID окружения.

Ниже приведен листинг задания конвейера, который инициирует запуск процесса тестирования:

run_smoke_tests:
  needs:
    - job: build-project-version
      artifacts: true
    - job: launch-container
      optional: true
    - job: create-docker-image
  when: on_success
  rules:
    - if: $RUN_SMOKE_TESTS == "false"
      when: never
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE != "pipeline"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
  stage: tests
  variables:
    RUN_ST: "true"
    TEST_LEVEL: "Smoke"
    TEST_ENV: "ST"
    BACKEND_VERSION: ${BUILD_TAG}
    SHORT_COMMIT: ${COMMIT_SHORT_SHA}
  trigger:
    project: "project/autotest"
    branch: main

Автотесты лежат в отдельном репозитории. После запуска окружения отрабатывают джоба на старт тестов, который триггерит запуск пайплайна.

script:
    - |
      ./allurectl watch --ci-type gitlab --insecure --results ${ALLURE_RESULTS} \
      -- mvn test \
      -Dtag=${TEST_LEVEL} \
      -Denv=${TEST_ENV} \
      -Durl=${ENV_URL} \
      -Durlbe=${ENV_BEURL} \
      -Durlkc=${ENV_KCURL} \
      -DurlDb=${ENV_DBURL} \
      -Dheadless=true \
      -Dargs=true

В ней мы передаем список параметров, которые получили: url базы, keycloak, backend и frontend. И указываем, какой тип тестов хотим запустить. 

Мы используем Selenide как фреймворк для автотестирования, Maven (maven-surefire-plugin) как сборщик, Allure testops как TMS систему. 

У нас должен быть определенный образ для ранера автотестов, который содержит в себе Maven и Java 17. В момент развертывания в ранер нужно установить Allure ctl, SSL-сертификаты, чтобы обеспечить доступ к нашим ресурсам c новой машины, и chrome-драйвер для запуска самих тестов.

Чтобы была возможность стартовать тесты на статических окружениях, мы храним значения переменных для этих сред в property-файлах. С помощью метода getBaseUrl получаем данные из этих проперти, но, как мы помним, url окружений для Smoke тестов нам заранее неизвестны. 

Их мы получаем в команде для запуска тестов на основании создавшегося дочернего окружения — используем ключ ST и получаем url из переданных параметров, которые сформировали на предыдущих шагах

public static String getBaseUrl() {
   String env = System.getProperty("env");
   //"………”(тут спрятано описание других возможных окружений)
   if ("ST".equals(env)) {
       return System.getProperty("url");
   }
   else {
       throw new IllegalStateException("Unexpected env: " + env);
   }
}

С этим url мы работаем как с переменной. Уже перед тестом мы можем собрать нужный url для конкретной странички, чтобы не ходить по меню и не тратить время на открытие всех страниц по структуре меню, или просто использовать полученный url для метода Selenide.open (). 

Параметры для тестирования забираем из сведений, которые нам передали для кейклоака, бэка, фронта и для БД. Единственное, что у нас хранится в статических переменных, — это логин и пароль юзера для системы. Эти данные мы создаем в хелм чартах и используем во всех тестах (это данные, о которых мы договорились внутри команды).

Чтобы указать, какие тесты хотим запустить, используем аннотацию @Tag. В настройках maven-surefire-plugin указываем блок groups, который позволяет запускать тесты по тегам.

   
   ${tag}
   true
   
       -Dfile.encoding=UTF-8
       -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
   
   
       ${project.build.directory}/allure-results
   

Прогонять полный регресс приложения по каждому МР нецелесообразно, для проведения функционального тестирования нам нужны только критичные проверки. Именно они и собраны в соответствующий тег.

Allure ctl позволяет в режиме реального времени наблюдать результаты прохождения тестов в TMS-системе. То есть, прогон может длиться до 40 минут, а наблюдаем мы его в процессе: видим, например, что у нас не стартовало приложение или, что тесты красные, и можем разобраться в причинах, не дожидаясь окончания тестирования.

После того как тесты завершаются, отрабатывается команда на утилизацию тестового окружения.

В качестве резюме

На разработку решения, вместе с аналитикой, поиском вариантов и MVP, у нас ушло около двух месяцев. Из сложностей — в какой-то момент мы столкнулись с тем, что не выключались тестовые окружения после завершения тестирования. Оказалось, что параметры, необходимые для запуска и остановки сред, имели меньший срок жизни, чем время тестирования. Срок жизни увеличили — проблема ушла. 

Схема работает уже год. Позволяет нам экономить время за запуск автотестов. В рамках пары мердж-реквестов это не так заметно, а вот когда их около 10, то получается, что вместо 5–6 часов, мы тратим 30–40 минут на все прогоны. 

Еще хочется отметить, что решение подходит не только для смоук-тестов, но и для полного регрессионного тестирования и любых других проверок. Такой подход сокращает потребность в отдельных тестовых окружениях, которые бо́льшую часть времени простаивают. 

Готовы ответить на ваши вопросы и узнать в комментариях, как вы решаете подобные задачи.

© Habrahabr.ru