Как e2e автотесты на Selenide помогают QA-команде при частых релизах +28


Всем привет! Я Иван, старший инженер-тестировщик в КРОК. Уже 6 лет занимаюсь тестированием ПО. Из них 3 года внедряю автоматизацию тестирования на различных проектах — люблю всё автоматизировать. На рабочей машине много разных “батников” и bash-скриптов, которые призваны упрощать жизнь.

Недавно у нас стартовал проект по модернизации и импортозамещению системы электронного документооборота (СЭД) в одной крупной организации. Система состоит из основного приложения и двух десятков микросервисов, в основном — для построения отчётов и интеграции с другими подсистемами. Сейчас в проекте уже настроено больше 100  автотестов, и они сильно помогают при частых релизах, когда времени на регресс почти нет. Весь набор автотестов выполняется примерно за 25 минут, в среднем экономим до 3,5 часов ручной работы при каждом запуске. А запускаем мы их каждый день.

Дальше будет про то, как мы выбирали технологии и инструменты, какой  каркас и подход к организации автотестов в итоге получился. И почему мы в КРОК решили тиражировать этот подход в других проектах, реализация которых основана на Content Management Framework (CMF) под СЭД. На базе CMF у нас есть комплексное решение для автоматизации процессов документооборота КСЭД 3.0. Конечно, отдельные решения по автотестам можно применять под любую СЭД.

Ещё расскажу про проблемы, и как мы их решали. Пост будет интересен и полезен, если в ваших автотестах необходимо подписывать документ электронной подписью (ЭП) в докер-образе браузера, проверять содержимое pdf файла, выполнять сравнение скриншотов или интегрироваться с одной из популярных Test Management System.

Что имели на входе

Много лет у заказчика была СЭД, которую мы же в КРОК делали на базе экосистемы Microsoft (.NET Framework, MS SQL Server, IIS, Active Directory). Но закон об импортозамещении ПО в госсекторе, ФЗ «О безопасности критической информационной инфраструктуры», плюс моральное устаревание существующей системы стали причиной решения о создании новой, уже на open source решениях.

Основной функционал новой СЭД реализуется в едином приложении. Для масштабирования запускается несколько экземпляров приложения, которые находятся за балансировщиком нагрузки. Из приложения выделено несколько подсистем, развёртываемых в качестве отдельных сервисов, — это даёт более гибкое масштабирование.

UI реализуется с использованием фреймворка Vue.JS. Бэкенд, как писал ранее, разрабатывается на собственном Content Management Framework (CMF) с реализованными процессами под СЭД. Основа для него — jXFW (CROC Java Extendable FrameWork). Зарегистрирован в Едином реестре российских программ для ЭВМ и баз данных (от 29 Марта 2018, регистрационный номер ПО: 4309). Про jXFW мой коллега писал отдельно вот здесь. Данные хранятся в БД PostgreSQL. Автоматизация бизнес-процессов выполнена при помощи Camunda. В качестве брокера сообщений используется RabbitMQ. Для наглядности, укрупненная физическая архитектура системы выглядит примерно так:

Почему решили делать автотесты

Проект нужно было сдать в короткие сроки, с очень частыми релизами. Мне было очевидно, что автоматизация тестирования должна нам помочь. Оставалось убедить в этом руководство.    

Я сел и посчитал, сколько времени уйдёт на ручное тестирование базового функционала при каждом релизе, и сколько времени нужно для написания и поддержки автотестов. В итоге получилось, что в среднем на двадцатый прогон тесты окупятся и далее будут приносить экономическую выгоду. Учитывая, что в неделю мы выпускаем несколько релизов, то приблизительно уже через 1,5-2 месяца мы должны получить профит. Изначально таблица расчётов выглядела так:

Время написания автотестов

Ручное выполнение
набора тестов (за 1 раз)

Частота выполнения

Поддержка тестов

(в неделю)

          70 ЧЕЛ-Ч

3,5-4 ЧЕЛ-Ч

3 раза в неделю

3 ЧЕЛ-Ч

На практике получилось даже лучше — тесты сейчас запускаются каждый день. Но немного увеличилось общее время написание тестов — на 15 человеко-часов. Остальные показатели из таблицы не изменились.

В общем, на применение автоматизированных e2e тестов получили добро быстро. А я в глубине души понимал, что мы спасаем себя от постоянной рутинной работы и сокращаем время на регрессионное тестирование.     

Чем будем работать: выбор технологий и инструментов

Каркас автотестов нужен был надёжный и универсальный, потому заморочились подбором инструментов.

С языком программирования никаких проблем не возникло — бэкенд был написан на Java, с которым в команде инженеров-тестировщиков почти все были знакомы. Дополнительно изучать не потребовалось. Всё удачно совпало, не пришлось разводить зоопарк языков в проекте, что тоже очень хорошо.

Чистый Selenium мне использовать не хотелось, так как существует прекрасный инструмент Selenide (про него много написано, например, здесь), который делает большое количество скрытой работы за нас и инкапсулирует в себе много сложной логики самого Selenium. Зачем изобретать велосипед, если ребята из “Codeborne” его уже сделали. Я раньше работал с этим фреймворком, и он мне понравился. Если коротко — это фреймворк для автоматизированного тестирования веб-приложений на основе Selenium WebDriver. Его основные преимущества:

  • Изящный API,

  • Поддержка Ajax для стабильных тестов,

  • Мощные селекторы,

  • Простая конфигурация.

С выбором “сборщика” тестов тоже проблем не возникло — взяли Apache Maven. Он используется во всём проекте, привносить что-то новое не хотелось. Изначально задумывались над тем, чтобы взять Gradle, так как он более гибок. Но как показало время, мы не делали ничего такого, с чем бы не справился Maven. Конфигурация для тестов в pom.xml довольно простая.

А вот определиться с выбором фреймворка для запуска тестов было немного сложнее. Изначально выбрали JUnit5, но потом передумали и взяли TestNG. С ним доводилось больше работать, и лично я считаю, что TestNG удобней, чем JUnit5. Но, как говорится, каждому своё.

У нас есть тесты, которые должны сохранять порядок выполнения, потому что идут по единому процессу. Например, жизненный цикл документа. TestNG легко позволяет это делать установкой приоритета (параметр priority в аннотации @Test) в тестовом классе, а порядок тестовых классов задаётся в отдельном конфигурационном файле testng.xml.

Есть тесты, которые подготавливают данные для других. TestNG позволяет это легко и просто реализовать с помощью параметра dependsOnMethods. Я знаю, что использование последовательных тестов является антипаттерном. Но небольшая часть из них так организована. Нам необходимо иметь понятную структуру, каждой задаче по документу – отдельный тест. А также быть уверенным в целостности и работоспособности бизнес-процесса.

Мне нравится подход и общая организация управления выполнения тестов в TestNG. О ней подробней расскажу ниже.

Дальше определились с фреймворком для построения отчетов: берем всем известный и популярный Allure. Ещё рассматривался Report Portal. Но всё же Allure более легковесный, проще в установке и поддержке. Report Portal подойдёт для единого решения по хранению и отображению результатов множества проектов на уровне компании. Такой задачи перед нами не стояло.

Среду для запуска автотестов хотелось иметь единую, легко масштабируемую для параллельного запуска. Опять же, рассматривались два варианта: Selenoid и Selenium Grid. Я раньше использовал Grid, и он, на мой взгляд, имеет ряд недостатков:

  • сложность в настройке,

  • нестабильность и невысокая скорость работы,

  • сложность в добавлении и поддержке новых версий браузеров.

А вот с Selenoid нужно было разобраться, понять, что он из себя представляет и подходит ли нам. Выяснилось, что это хорошая альтернатива. В двух словах — это сервер, который позволяет запускать браузеры в docker-контейнерах. Легко настраивается и устанавливается в две команды, имеет замечательный UI-интерфейс, где можно отслеживать сессии в браузерах, даёт возможность записывать видео. Также, что очень важно, просто настраивать и конфигурировать браузеры в контейнерах.

Итого, имеем следующий основной технический стек:

Язык программирования

Java SE8

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

Selenide

Фреймворк для сборки тестов

Apache Maven

Фреймворк для запуска тестов

TestNG

Отчетность

Allure

Среда для выполнения тестов

Selenoid

Всё по полочкам: структура и организация тестов

Думаю, никого не удивлю, если скажу, что мы используем паттерн Page Object, позволяющий разделять логику выполнения тестов от их реализации. Все представляют структуру стандартного Java проекта. У нас для тестов она выглядит примерно так:

Всё, что является тестами, располагается в папке test. Весь же код по страницам, хелперы, ресурсы, глобальные переменные — хранятся в папке main.

В папке libs — драйвера под несколько браузеров (Chrome, Firefox, Opera) на случай, если необходимо запустить тесты локально.

В папке screenshot_tests — эталонные скриншоты для печатных форм документа. Там мы проверяем корректность формирования штампа ЭП — размеры, месторасположение на листе и т.д.

В папке src/main/java/ru/croc/environment находится класс для хранения и управления общими переменными. Сами переменные хранятся в файле configuration.properties (папка resources). Вот фрагмент из класса Environment, где отображён метод по загрузке конфигурационного файла и получению переменной адреса приложения:

public class Environment {
    static FileInputStream fis;
    static Properties property = new Properties();
    /**
     * Загрузка пропертей
     */
    private static Properties loadProperties() {
        try {
            fis = new FileInputStream("src/main/resources/configuration.properties");
            property.load(fis);
        } catch (IOException e) {
            System.err.println("ОШИБКА: Файл свойств отсутствует!");
        }
        return property;
    }

    /**
     * Адрес СЭД
     */
    public static final String URL_SEDD = getUlrSedd("SEDD");

    private static String getUlrSedd(String param) {
        return loadProperties().getProperty(param);
    }
}

Далее уже в нужном месте кода вызываем переменную Environment.URL_SEDD.

В папке src/main/java/ru/croc/listeners — класс TestListener, имплементирующий слушатель ITestListener и реализующий логику работы с TestRail. В нём отслеживаются успешно выполненные и упавшие тесты в методах onStart, onTestSuccess, onTestFailure. Об интеграции с Test Management System (далее TMS) подробно скажу далее.

В папке src/main/java/ru/croc/pages — классы, описывающие страницы приложения. Основные страницы находятся в корне папки. Также существуют виджеты — это общие элементы на страницах (модальные окна, панели). Их мы храним в папке src/main/java/ru/croc/pages/widgets.

В папке src/main/java/ru/croc/utils хранятся классы, отвечающие за:

  1. Кастомные драйвера (драйвера под Selenoid),

  2. Работу с БД,

  3. Интеграцию с TestRail,

  4. Скриншот тестирование,

  5. API взаимодействие с системой,

  6. Работу с pdf файлами,

  7. Работу с пользователями,

  8. Утилитарные методы — работа с датами, скачивание/загрузка файлов, работа с вкладками браузера и т.д.

Все вспомогательные ресурсы — скрипты, тестовые вложения, файлы для хранения пользователей, переменных — находятся в папке  src/main/resources.

Вся общая логика по тестам вынесена в базовый класс BaseTest. Каждый тестовый класс наследуется от него. В BaseTest реализованы методы — setUp с аннотацией TestNG @BeforeSuite (выполняется перед всем тестовым набором, заданным в testng.xml) и tearDown с аннотацией @AfterMethod (выполняется после каждого тестового метода). В методе setUp реализована инициализация подходящих драйверов, выполнение скриптов и API запросов, если это необходимо, настройки работы с Allure. Нужно добавить, что автотесты выполняются на тестовой среде с тестовыми данными, поэтому мы можем менять или добавлять недостающие данные скриптами и не переживать за созданные тестовые задачи в системе. В методе tearDown реализован перелогин в приложении. Сам файл testng.xml выглядит примерно так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Testing SEDD" >
    <listeners>
         <listener class-name="ru.croc.listeners.TestListener" />
    </listeners>
    <test name="Autotests for SEDD" parallel="none" preserve-order="true" group-by-instances="true">
        <classes>
            <class name="ru.croc.IncomingDocTest"/>
            <class name="ru.croc.OutgoingDocTest"/>
            <!-- здесь остальные тесты -->
         </classes>
    </test>
</suite

Здесь из важного:

  1. Подключается слушатель TestListener, реализующий логику работы по интеграции с TestRail,

  2. Настройки, которые дают возможность запускать тесты последовательно (parallel=none) и сохраняя порядок (preserve-order="true").

Последовательность тестов в самом классе реализуем указанием параметров в аннотации @Test. Пример:

@Test(description="Рассмотрение начальником управления", priority=1)
public void headReview(){}

//...............

@Test(description="Рассмотрение начальником отдела", priority=2)
public void chiefReview(){}

Что ещё умеем: дополнительный функционал

Интеграция с TestRail

После каждого прогона автотестов в TestRail нам хотелось иметь новый Test Run с результатами выполнения. К счастью, есть хорошо задокументированный API и реализованный клиент, позволяющий без особых проблем интегрироваться с TestRail. На его основе можно разрабатывать функционал под свои нужды.

Мы создали класс TestRailUtil, в котором реализовали основные методы по работе с TMS. Пример:

/**
 * Инициализация клиента для работы с API
 */
private static APIClient getClient() {
    APIClient client = new APIClient(MavenParametrs.getTestRailUrl());
    client.setUser(MavenParametrs.getTestRailUser());
    client.setPassword(MavenParametrs.getTestRailAPIKey());
    return client;
}

/**
 * Добавление результата в TestRail
 */
public static String addResult4Case(String runId, String caseId, Object data) {
    try {
        JSONObject js = (JSONObject) getClient().sendPost("add_result_for_case/" + runId + "/" + caseId, data);
        return js.get("test_id").toString();
    } catch (IOException | APIException e) {
        e.printStackTrace();
        return null;
    }
}

/**
 * Метод добавления результата при успешном выполнении тестов
 */
public static String testPassed(String runId, String caseId) {
    Map data = new HashMap();
    data.put("status_id", 1);
    data.put("comment", "This test worked fine!");
    return addResult4Case(runId, caseId, data);
}

/**
 * Метод добавления результата при не успешном выполнении тестов
 */
public static String testFailed(String runId, String caseId) {
    Map data = new HashMap();
    data.put("status_id", 5);
    data.put("comment", "ERROR");
    return addResult4Case(runId, caseId, data);
}

/**
 * Добавление нового тестрана
 */
public static String[] addTestRun(String suiteId, String projectId, String nameRun) {
    Map data = new HashMap();
    data.put("suite_id", new Integer(suiteId));
    data.put("name", nameRun + "_" + TestUtils.getDateTime());
    try {
        JSONObject js = (JSONObject) getClient().sendPost("add_run/" + projectId, data);
        return new String[] {
            js.get("id").toString(), js.get("url").toString()
        };
    } catch (IOException | APIException e) {
        e.printStackTrace();
        return null;
    }
}

/**
 * Удаление тестранов
 */
public static void deleteTestRuns(List < String > list_id) {
    try {
        for (String item: list_id) {
            getClient().sendPost("delete_run/" + item, "");
        }

    } catch (IOException | APIException e) {
        e.printStackTrace();

    }
}

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

Всю логику перенесли, как и писал ранее, в класс TestListener имплементирующий ITestListener. Используем всего 3 переопределенных метода — onStart, onTestSuccess, onTestFailure.

Пример метода onTestSuccess:

/**
 * Выполняется после каждого успешно выполненного теста
 */
@Override
public void onTestSuccess(ITestResult iTestResult) {
    testId = "";
    testMethods = iTestResult.getMethod().getMethodName();
    tm = FileReaderUtil.getTestMethod();
    for (TestMethodsName item: tm) {
        if (testMethods.equals(item.getMethodName())) {
            for (TestRun tr: testRuns) {
                if (tr.getTestRunName().contains(item.getTestRunName())) {
                    testId = TestRailUtil.testPassed(tr.getTestRunId(), item.getCaseId());
                    break;
                }
            }
        }
    }
}

Метод onTestFailure, который выполняется после проваленного теста, реализован аналогично, только вместо testPassed вызывается testFailed.

Здесь надо пояснить, что есть ещё отдельный csv файл, в котором хранятся имена методов вместе с id тест-кейса в TestRail, то есть идет сопоставление методов в коде с тест-кейсами в TMS. Фрагмент этого файла выглядит так:

#Тест по входящему документу,DemoAutoTest
registrationIncoming,98162
headReview,98166
chiefReview,98168
execute,100003

Первая строка в файле нужна, чтобы обозначить набор кейсов в Test Run. Как раз в методе onStart мы создаем новый Test Run, например, с именем DemoAutoTest. Остальные же строки нам говорят, что тест-кейсу, например, с id 98162, соответствует метод в коде registrationIncoming.

На строке:

tm = FileReaderUtil.getTestMethod();

мы получаем список объектов с именем метода, id тест-кейса и именем тестрана, к которому эти кейсы относятся. А далее выполняется проверка на соответствие текущего выполняемого метода с тест-кейсом в TMS. Также добавлена дополнительная проверка в цикле на идентификацию необходимого Test Run:

if(tr.getTestRunName().contains(item.getTestRunName()))

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

После запуска тестов в TestRail создаётся новый Test Run с датой и временем:

А по запущенным тестам проставляются результаты:

Вот так, не очень сложно, можно реализовать интеграцию с TestRail для отслеживания результатов выполненных автотестов.

Скриншот-тестирование печатных форм по документу

В системе документы подписываются ЭП, поэтому возникла задача выполнять проверку на корректность формирования штампа и шапки печатной формы после подписания. Сам штамп выглядит так:

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

Сравнение выполняется на основе ранее подготовленных эталонных скриншотов и текущих, которые формируются при каждом выполнении тестов.

Для хранения наборов скриншотов, как писал ранее, необходимо было создать папку screenshot_tests. В ней: 

  • папка для хранения наложенных друг на друга снимков с отмеченными различиями между ними — screenshot_tests/diff_screens,

  • папка для хранения эталонных скриншотов — screenshot_tests/etalon_screens,

  • папка для хранения скриншотов, сделанных в процессе выполнения тестов — screenshot_tests/test_screens.

Скриншот можно создавать как с помощью библиотеки aShot, так и с помощью методов Selenide. Так как мы делаем снимок превью печатной формы, то необходимо было делать скриншот непосредственно самого веб-элемента, отвечающего за предварительный просмотр, а не всей страницы целиком. Мы воспользовались методом Selenide.

Полностью метод снятия скриншота выглядит так:

 /**
   * Метод создания скриншота элемента
   */
  public static BufferedImage takeScreenOfElement(String selector) throws IOException {
      File screen = $(selector).screenshot();
      return ImageIO.read(screen);
  }

Для сохранения используем метод:

/**
 * Метод сохранения скриншота
 */
public static void saveScreenShot(BufferedImage image, String path) throws IOException {
    File file = new File(path);
    file.getParentFile().mkdirs();
    ImageIO.write(image, "png", new File(path));
}

И основной метод по сравнению снимков:

   /**
     * Метод сравнения 2-х скриншотов
     */
    public static void checkImageDiff(Screenshot etalon_scr, Screenshot test_scr, String typeScreen) throws IOException {
        ImageDiff diff = new ImageDiffer().makeDiff(etalon_scr, test_scr).withDiffSizeTrigger(0);
        if (diff.hasDiff())
            saveScreenShot(diff.getMarkedImage(), pathToDiffScreen + "diff_image_" + typeScreen + ".png");
        Assert.assertFalse(diff.hasDiff(), "Screenshot has difference");
    }

Далее в тесте вызываем методы по получению, сохранению скриншотов и потом выполняем их сравнение:

BufferedImage test_scr_bottom= ScreenShotUtils.takeScreenOfElement(contentSelector);
ScreenShotUtils.saveTestScreen(test_scr_bottom,"test_image_bottom.png");
      
Screenshot expected_bottom = new Screenshot(test_scr_bottom);
Screenshot actual_bottom = ScreenShotUtils.getEtalonScreen("etalon_image_bottom.png");
ScreenShotUtils.checkImageDiff(actual_bottom,expected_bottom,"bottom");

Если скриншоты совпадают, то тест выполняется успешно и папка screenshot_tests/diff_screens остается пустая, иначе срабатывает проверка:

if (diff.hasDiff())
            saveScreenShot(diff.getMarkedImage(), pathToDiffScreen + "diff_image_" + typeScreen + ".png");
        Assert.assertFalse(diff.hasDiff(), "Screenshot has difference");

и тест падает. Создаётся изображение diff_image_bottom.png с разницей, которая подсвечивается:

По нему сразу становится понятно, в чем проблема.

Проверка содержимого файла

При прохождении исходящего документа по процессу, где выполняется согласование и подписание, на определённом этапе формируется “Лист согласования” в формате *.pdf. Он содержит основные данные по документу (номер, краткое содержание, тип документа и тд), а также список всех согласующих. Выглядит он так:

Стояла задача проверить корректное заполнение данного листа. Глобально ее можно декомпозировать на блоки:

  1. Скачать лист согласования,

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

  3. Сделать проверку содержимого.

Загрузка листа согласования осуществляется нажатием на одну кнопку. Но есть небольшая проблема при получении этого файла из Selenoid: файл доступен во время запущенной текущей сессии браузера по специальной ссылке  <selenoid-host>:4444/download/<SESSION_ID>/<FILE_NAME>. При этом имя файла — рандомное, в виде id. Мы не могли знать его заранее.

Поэтому сразу пришлось по ссылке <selenoid-host>:4444/download/<SESSION_ID>/ получать имя вложения, а потом уже формировать полную ссылку с именем и класть вложение на машину, откуда запускаются тесты. Метод этот выглядит так:

     /**
      * Получить Pdf файл от Selenoid
      */
     public static File getPdfFile(SessionId id) throws IOException {
         if (MavenParametrs.getName().contains("_selenoid")) {
             String pathName = DOWNLOAD_FILE_PATH + DOWNLOAD_FILE_NAME;
             File file = new File(pathName);
             file.getParentFile().mkdirs();
             String download_url = MavenParametrs.getRemoteURL().split("wd")[0] + "download/";
             InputStream inputStream1 = new URL(download_url + id).openStream();
             String text = IOUtils.toString(inputStream1, StandardCharsets.UTF_8.name());
             String fileName = StringUtils.substringBetween(text, "\"", "\"");
             InputStream inputStream2 = new URL(download_url + id + "/" + fileName).openStream();
             Files.copy(inputStream2, Paths.get(pathName), StandardCopyOption.REPLACE_EXISTING);
         }
         File dir = new File(DOWNLOAD_FILE_PATH);
         File[] files = dir.listFiles((d, name) - > name.endsWith(".pdf"));
         return files[0];
     }

Для преобразования pdf  в текст и выполнения его разбора использовалась библиотека Apache PDFBox. Метод по преобразованию можно увидеть ниже:

    public static String PDFReader(String fileName){

        String resultStr="";
        try (PDDocument document = PDDocument.load(new File(fileName))) {

            document.getClass();
            if (!document.isEncrypted()) {
                PDFTextStripperByArea stripper = new PDFTextStripperByArea();
                stripper.setSortByPosition(true);
                PDFTextStripper tStripper = new PDFTextStripper();
                String pdfFileInText = tStripper.getText(document);
                String lines[] = pdfFileInText.split("\\r?\\n");
                for (String line : lines) {
                  resultStr+=line+System.lineSeparator();
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return resultStr;
}

После того, как Лист согласования преобразован в текст, можно выполнять необходимые проверки в тесте:

//Исполнитель-Визирующий
Assert.assertTrue(pdf.contains(executorApprover));
//Визирующий
Assert.assertTrue(pdf.contains(approver));

Работа с пользователями

Так как система предполагает работу по задачам большим количеством пользователей, то необходимо было организовать их правильное хранение для быстрого, гибкого и легкого доступа. Не хотелось иметь проблем при добавлении нового или удалении старого пользователя. Поэтому возникла идея хранить логин, пароль, ФИО и роль пользователя в csv файле. Также создать отдельный класс User, который будет иметь все необходимые поля по сущности и методы быстрого поиска пользователя по ФИО или роли.

Пример файла:

test\ivanov,1,Иванов Иван Иванович,Регистратор
test\petrov,1,Петров Александр Сергеевич,ПодписантАдресат
test\sidorov,1,Сидоров Иван Петрович,Подписант
test\ponomareva,1,Пономарева Светлана Викторовна,Адресат
test\guseva,1,Гусева Елена Вячеславовна,Корректор
test\pavlova,1,Павлова Екатерина Ивановна,ИсполнительВизирующий

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

public static User findByRole(String Role)
    {
        List<User> users = FileReaderUtil.getUsers();
        User findUser = null;
        for (User user:users){
            if(user.getRoleName().equals(Role)) {
                findUser = user;
                break;
            }
        }
        return findUser;
    }

Здесь выполняется формирование списка пользователей из файла, а потом поиск по заданной роли в системе. В тесте необходимый пользователь ищется по роли так:

User Executor = User.findByRole("Исполнитель");

Чтобы появился новый пользователь, необходимо его добавить в csv файл и тест. Для удаления — аналогично в обратном порядке. Больше ничего менять не нужно.

Запуск автотестов на CI/CD

В качестве сервера непрерывной интеграции у нас используется TeamCity.
Код проекта и код автотестов хранится в Gitlab. Для разработки используются следующие ветки:

  1. master — основная ветка.

  2. release — релизная ветка. Здесь хранится код по текущему релизу приложения. В момент релиза вливается в master.

  3. feature — ветка с новым функционалом. Здесь хранится код по каждой отдельной функциональности. Потом вливается в release, далее в master. Может вливаться в мастер напрямую — “дальний релиз”.

Каждая новая версия приложения помечается тегом.

Тесты живут в том же репозитории, что и код приложения. Они также версионируются. Есть версия под релизную ветку, есть под ветку master.

На TeamCity есть два джоба для каждой версии:

Они запускаются автоматически каждую ночь, при условии, что было успешное обновление тестовых стендов из необходимой ветки. Запускаются на удаленном сервере в Selenoid — за ними даже можно отдельно наблюдать через Selenoid UI или записать видео:

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

public class CustomChromeDriverSelenoid implements WebDriverProvider {

    @Override
    public WebDriver createDriver(DesiredCapabilities capabilities) {
        RemoteWebDriver driver = null;
        InitSelenoidDriver selenoidDriver = new InitSelenoidDriver(BrowserType.CHROME,
            true, false, MavenParametrs.getVersionSelenoidBrowser());
        ChromeOptions options = new ChromeOptions();
        options.addExtensions(new File("libs/cades_plugin.crx"));
        options.setCapability("browserName", selenoidDriver.getBrowserName());
        options.setCapability("enableVNC", selenoidDriver.getEnableVNC());
        options.setCapability("enableVideo", selenoidDriver.getEnableVideo());
        options.setCapability("version", selenoidDriver.getBrowserVersion());
        try {
            driver = new RemoteWebDriver(
                URI.create(MavenParametrs.getRemoteURL()).toURL(),
                options
            );
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return driver;
    }
}

Так как нужно было подписывать документы с помощью ЭП, пришлось создавать собственный докер-образ, чтобы там был КриптоПро, тестовые сертификаты и Cades plugin. Но про это чуть дальше.

Описывать настройку джобов по запуску тестов не буду. Но тут важно сказать, что для формирования отчетов прямо в TeamCity мы используем отдельный плагин allure-teamcity. Он выполняет генерацию Allure отчета с сохранением истории выполнении тестов, что очень важно. Есть возможность отчет вынести на отдельную вкладку:

А можно смотреть прямо из артефактов, которые генерирует allure-teamcity plugin:

Также можно стандартными средствами TeamCity посмотреть на скоуп всех тестов. Какие выполнялись успешно, а какие провалились:

Если тест упал, выводится подробный стектрейс.

Создание докер-образа Chrome для подписания документов ЭП в Selenoid

Так как документы подписываются ЭП, для того, чтобы всё корректно работало,  необходимо установить дополнительное ПО. А именно:

  1. CryptoPro CSP 5.0

  2. Cades plugin

  3. Тестовые сертификаты

Это всё без особых проблем устанавливается локально, а вот для Selenoid пришлось создавать собственный image с предустановленным и настроенным ПО.
Чтобы сделать image, для удобства нужно создать Dockerfile, на основе которого он и будет собираться. Пример:

# Готовый образ Chrome 81 версии
FROM selenoid/vnc:chrome_81.0
# Установка пользователя root
USER root
 
# Юзер под которым запускается контейнер
ARG USER_NAME=selenium
 
ADD dist/ /tmp/dist/
ADD cert/ /tmp/cert/
 
# Распаковать КриптоПро CSP 5
RUN tar -zxvf /tmp/dist/linux-amd64_deb.tgz -C /tmp/dist/
 
# Установка КриптоПро CSP 5
RUN /tmp/dist/linux-amd64_deb/install.sh
RUN dpkg -i /tmp/dist/linux-amd64_deb/cprocsp-rdr-gui-gtk-64_*
 
# Распаковать cades plugin
RUN tar -zxvf /tmp/dist/cades_linux_amd64.tar.gz -C /tmp/dist/
 
# Установить cades plugin
RUN dpkg -i /tmp/dist/cades_linux_amd64/cprocsp-pki-cades-64_2.0.14071-1_amd64.deb
RUN dpkg -i /tmp/dist/cades_linux_amd64/cprocsp-pki-plugin-64_2.0.14071-1_amd64.deb
 
# Проверка лицензии
RUN /opt/cprocsp/sbin/amd64/cpconfig -license -view
 
# переключаемся на "обычного" юзера перед установкой сертификатов
USER $USER_NAME
 
# установка корневого сертификата
RUN echo o | /opt/cprocsp/bin/amd64/certmgr -inst -file /tmp/cert/cert_test.cer -store uRoot
 
# установка личного сертификата. -pin <password> это пароль от закрытого ключа
RUN /opt/cprocsp/bin/amd64/certmgr -inst -pfx -file /tmp/cert/test.pfx -pin <password> -silent
 
# команда, для того чтобы убрать всплывающее окно - "лицензия истекает меньше чем через 2 месяца".
RUN /opt/cprocsp/sbin/amd64/cpconfig -ini '\local\KeyDevices' -add long LicErrorLevel 4
 
# переключаемся на суперюзера
USER root
 
# убираем alert "переход на новый алгоритм в 2019 году"
RUN sed -i 's/\[Parameters\]/[Parameters]\nwarning_time_gen_2001=ll:131907744000000000\nwarning_time_sign_2001=ll:131907744000000000/g' /etc/opt/cprocsp/config64.ini
 
# Добавляем адрес тестируемого приложения в доверенные.
RUN /opt/cprocsp/sbin/amd64/cpconfig -ini "\config\cades\trustedsites" -add multistring "TrustedSites" "http://<ip_sedd>/sedd"
 
# переключаемся на "обычного" юзера
USER $USER_NAME

В Dockerfile используются дистрибутивы и сертификаты.

Необходимо создать две папки: distr и cert, — которые будут лежать вместе с Dockerfile.

Структура должна выглядеть так:

cert_test.cer — корневой тестовый сертификат.

test.pfx — личный тестовый сертификат.

linux-amd64_deb.tgz — дистрибутив CryptoPro CSP

cades_linux_amd64.tar.gz — дистрибутив Cades plugin

Для сборки образа надо зайти в Root folder и выполнить команду:

docker build -t chrome_csp_81:vnc_chrome_csp .

После успешной сборки будет сообщение:

Successfully built d9d26ccae897
Successfully tagged chrome_csp_81:vnc_chrome_csp

Далее, необходимо созданный образ добавить в Selenoid. Для этого правим файл .aerokube/selenoid/browsers.json:

"chrome": {
    "versions": {
        "81.0": {
            "image": "chrome_csp_81:vnc_chrome_csp",
            "port": "4444",
            "path": "/"
        }
    }
}

То есть устанавливаем созданный image в качестве одного из браузеров. Выполняем перезапуск Selenoid, чтобы новый образ подтянулся на сервер. Если всё успешно, то добавленный образ отобразиться в Selenoid UI:

Итоги, и что будет дальше      

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

Сейчас автотесты дают нам уверенность, что ничего из важного функционала не сломалось, а если и сломалось, то есть время исправить ошибки до релиза. Также мы экономим очень много времени на ручных проверках во время регресса. В планах — увеличение количества функциональных тестов и расширение покрытия. Правда, придется всё же ограничиться неким не очень большим объемом, так как у нас просто нет ресурсов для поддержки тысячи UI-автотестов. Стараемся расставлять приоритеты и выбирать самый важный функционал.

Радует ещё то, что на поддержку уходит совсем немного времени. Сами тесты довольно стабильны, и ложноотрицательные результаты случаются редко. Отчасти это заслуга Selenide, отчасти — команды тест-инженеров, которые поддерживают тесты. Кстати, QA-команда состоит из двух QA-инженеров и одного QA automation.

Важное достижение, на мой взгляд, — в том, что текущее решение очень хорошо себя зарекомендовало. Его можно тиражировать на другие проекты, в первую очередь, под СЭД. Причём не только под системы, которые разрабатываются на основе нашего CMF, но и на базе других вендорских продуктов. И сейчас мы активно работаем над этим. Делаем некоторый функционал более гибкими и универсальными.

Оглядываясь назад, могу сказать, что уже по ходу работы я находил решения, которые мне казались лучше. Но в данном случае не видел целесообразности переделывать уже существующую реализацию, или просто не было времени внедрять что-то новое, если неплохо работало старое. Например, для хранения пропертей существует прекрасная библиотека Owner. На мой взгляд, она позволяет очень изящно и лаконично хранить параметры. Также есть проект Lombok, который добавляет дополнительную функциональность в Java c помощью изменения исходного кода перед компиляцией. Хорошо про него написано здесь. У нас в автотестах есть не маленькое количество POJO объектов с большим количеством кода. Lombok бы сильно уменьшил и упростил реализацию таких классов.

А какие библиотеки или решения вы могли бы посоветовать для улучшения автотестов при тираже и наших вводных? Пишите в комментариях. Буду рад услышать примеры из вашего опыта и готов ответить на вопросы.




К сожалению, не доступен сервер mySQL