Всем привет! Я Иван, старший инженер-тестировщик в КРОК. Уже 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 месяца мы должны получить профит. Изначально таблица расчётов выглядела так:
Время написания автотестов | Ручное выполнение | Частота выполнения | Поддержка тестов (в неделю) |
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 хранятся классы, отвечающие за:
Кастомные драйвера (драйвера под Selenoid),
Работу с БД,
Интеграцию с TestRail,
Скриншот тестирование,
API взаимодействие с системой,
Работу с pdf файлами,
Работу с пользователями,
Утилитарные методы — работа с датами, скачивание/загрузка файлов, работа с вкладками браузера и т.д.
Все вспомогательные ресурсы — скрипты, тестовые вложения, файлы для хранения пользователей, переменных — находятся в папке 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
Здесь из важного:
Подключается слушатель TestListener, реализующий логику работы по интеграции с TestRail,
Настройки, которые дают возможность запускать тесты последовательно (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. Он содержит основные данные по документу (номер, краткое содержание, тип документа и тд), а также список всех согласующих. Выглядит он так:
Стояла задача проверить корректное заполнение данного листа. Глобально ее можно декомпозировать на блоки:
Скачать лист согласования,
Выполнить его синтаксический разбор,
Сделать проверку содержимого.
Загрузка листа согласования осуществляется нажатием на одну кнопку. Но есть небольшая проблема при получении этого файла из 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. Для разработки используются следующие ветки:
master — основная ветка.
release — релизная ветка. Здесь хранится код по текущему релизу приложения. В момент релиза вливается в master.
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
Так как документы подписываются ЭП, для того, чтобы всё корректно работало, необходимо установить дополнительное ПО. А именно:
CryptoPro CSP 5.0
Cades plugin
Тестовые сертификаты
Это всё без особых проблем устанавливается локально, а вот для 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 бы сильно уменьшил и упростил реализацию таких классов.
А какие библиотеки или решения вы могли бы посоветовать для улучшения автотестов при тираже и наших вводных? Пишите в комментариях. Буду рад услышать примеры из вашего опыта и готов ответить на вопросы.
lxsmkv
Круть крутейшая. И все это за 85 часов? Две недели? С трудом в это верится, но ладно.
А например, если бы нужно было не только текстовое содержание PDF, но и верстку проверять, как бы в принципе к этому подошли? Ну выгрузка в картинку скажем, напрашивается сама собой. Но в картинке ведь будут некоторые области статические (напр. таблицы), а некоторые динамические (напр. календарная дата). Или как у вас на картинке с печатью, срок действия подписи поменяется.
Я думал вводить области которые заведомо не будут проверяться (типа местами зачерненный шаблон). Либо будут проверяться через OCR, либо с другим пороговым значением. Которое придется подбирать, чтобы с высокой точностью отличать допустимое отклонение от недопустимого
lxsmkv
А вот еще один организационный вопрос. Обьем автоматизации (типа покрытие) подход и прочие вещи дали решать Вам, типа «ну окей, делайте» или с Вас стребовали сперва какой-то более формализованый подход к реализации?
IBigar Автор
Ваше предложение правильное. По крайней мере, я делал также.
Если нужно в одном файле проверять статические и динамические данные, то необходимо его разбивать на разные области. Верстка (статика) проверяется тем же самым скриншот тестированием. То есть либо вырезаем нужную нам область картинки и сохраняем для сравнения, либо сохраняем всю, а уже средствами, например, Yandex aShot игнорируем не интересующие нас области (динамика). Их как раз можно проверить через парсинг.
Возьмем ту же печатную форму со штампом. Ее можно разбить на 3 части:
1. Шапка документа – всегда статична. Мы ее обрезаем и сохраняем как картинку, в тесте проверяем через сравнение скриншотов.
2. Номер документа, дата – ее как раз можем проверить через сохранение файла и парсинг данных.
3. Штамп – почти статичен. Идем путем из п.1.
Данные в штампе зависят от ЭП. Но мы можем с уверенностью сказать, когда эта подпись изменится (срок действия) и поменять эталон изображения в тестах. Это происходит не так часто. Например, сейчас используем подпись, которая действительна 1 год. Не вижу в этом проблемы.
По поводу порогового значения при сравнении изображений. Я бы не советовал такой подход использовать. Был не очень положительный опыт, тесты становятся хрупкие и ненадежные. Лучше вырезать и игнорировать ненужные области на картинках.
IBigar Автор
Был критический функционал, который необходимо покрыть в первую очередь. И который нужно было проверять от релиза к релизу.
Со стороны руководства и других членов команды (аналитики, внедренцы) был четко обозначен этот функционал + вещи на которые стоит обратить внимание при проверке (ПФ, лист согласования, корректность движения задач по процессу).
Как это все будет проверятся и в каком объеме — решала непосредственно QA-команда.
Если что-то пропустили или не учли, то уже на следующих итерациях это исправлялось. Например, нашли баг в функционале, который автотесты не покрывали. После его исправления добавили тест, который это момент проверяет.