Фундаментальный строительный блок автоматизации – тестирование
Род Джонсон
Я не амбассадор автоматизации тестирования веб интерфейсов, однако сей очерк скорее будет полезен камрадам, уже имеющим опыт в этой сфере.
Для совсем новичков также будет полезно, т.к. я предоставляю исходный код, где можно посмотреть, как в конечном продукте организовано взаимодействие с селениумом.
Я расскажу о том, как с нуля, имея небольшой опыт разработки, написал платформу для запуска тестов, и о самой платформе. Сам я считаю, что мой продукт получился весьма эффективным, а значит будет многим полезен и имеет место для рассмотрения.
Концепции
Процесс тестирования зависит от информационной системы.
Для поминания моей концепции, необходимо понять на какие системы я ориентируюсь в первую очередь – это те системы, где обычно существуют конкретные линейные бизнес-процессы, которые и ставятся ключом при проведении регрессионных тестов.
Итак, система типа srm. Ключевая бизнес-сущность – коммерческие предложения поставщика. Ключевое внимание при проведении регрессионного тестирования – целостность бизнес-процесса.
Бизнес-процесс начинается от регистрации поставщика в системе, далее идет создание коммерческого предложения – идет на этап рассмотрения, который производится различными типами внутренних пользователей (и для каждого пользователя уникальный интерфейс) до возврата решения о рассмотрения предложения поставщику.
Получается, что мы проходим через ряд разных интерфейсов, и практически всегда работаем с разными. На деле – если просто взглянуть на это прямо – это похоже на просмотр видеокассеты – т.е. это именно процесс, имеющий начало и конец, и он абсолютно линейный – никаких ветвлений, при написании теста мы всегда знаем ожидаемый результат. Т.е. я хочу сказать, что уже глядя на это картину, можно заключить, что сделать тесты полиморфными вряд ли получится. Ввиду этого при создании платформы для запуска тестов, ключевым фактором я поставил скорость написания тестов.
Концепции, которые я перед собой поставил:
- Автотест должен создаваться максимально быстро на сколько это возможно. Если добиться этого качественно, то остальные аспекты, такие как надежность и удобство использования, должны прийти сами собой.
- Тесты должны быть объявлены декларативно и жить отдельно от кода. Иного варианта я даже не увидел. Это увеличивает скорость написания, т.к. при наличие готового интерпретатора — нашей платформы, не надо потом ничего дописывать, не надо лезть лишний раз в код – вообще после готовой платформы по IDE можно забыть. Так тесты проще сопровождать. В таком виде их проще научить писать, т.к. не нужны навыки разработки, а лишь понимание языка разметки. В таком виде они понятны всем участникам процесса.
От чего я решил отказаться уже на старте:
- НЕ оборачивать свою систему в тестовый фреймворк. Запустить выполнение того или иного процесса можно и без тестового фреймворка. «Ты хочешь изобрести велосипед!», — скажут многие. Я рассуждаю по-другому. Популярные используемые тестовые фреймворки создавались в первую очередь для тестирования кода изнутри, а мы собираемся тестировать внешнюю часть системы снаружи. Это как если у меня есть шоссейный велосипед, а мне нужно спуститься с горы по бездорожью (грубо, но ход мыслей отражает). В общем фреймворк будем писать сами — с блек джеком и … (хотя я в курсе что, например, JUnit 5 уже в гораздо большей степени адаптирован под такого рода задач).
- Отказ от использования оберток для селениума. Собственно, ключевая библиотека сама по себе небольшая. Чтобы понять, что нужно использовать 5 процентов ее функциональности, при этом перелопатив ее полностью, потребуется несколько часов. Хватит везде искать способ писать меньше кода и приучать себя к горшку. В современном мире это желание часто приводит к абсурду и почти всегда дает ущерб гибкости (я имею ввиду именно подходы к «написать меньше кода» а не случаи архитектурных фреймворков).
- Красивое оформление результатов не нужно. Внес данный пункт, т.к. уже не однократно сталкиваюсь с этим. Когда автотест выполнился, мне нужно знать 2 вещи: общий результат (положительный/отрицательный), и, если была ошибка – где именно. Возможно еще надо вести статистику. Все остальное в плане результатов – АБСОЛЮТНО не существенно. Рассматривать красивое оформление как существенный плюс, либо тратить на это красивое оформление время на начальных этапах – лишние понты.
Немного еще расскажу об уровне разработки в компании и условиях создания инструмента дабы до конца внести в ясность в некоторые детали.
В силу неких конфиденциальных обстоятельств я не раскрываю компанию, где работаю.
В нашей компании разработка существует уже далеко не первый год, и стало быть все процессы уже давно налажены. Однако, они сильно отстают от современных тенденций.
Все представители айти понимают, что надо покрывать код тестами, писать сценарии автотестов в момент согласования требований к будущей функциональности, гибкие технологии существенно экономят время и ресурсы, а CI который просто берет и упрощает жизнь. Но это все пока только медленно доходит до нас…
Так и служба контроля качества ПО – все тесты выполняются мануально, если взглянуть на процесс «сверху» – то это «горлышко бутылки» всего процесса разработки.
Описание сборки
Платформа написана на языке Java с использогванием JDK 12
Основные инфраструктурные инструменты — Selenium Web Driver, OJDBC
Для работы приложения на ПК должен быть установлен браузер FireFox версии выше 52
Состав сборки приложения
С приложением в обязательном порядке идет 3 папки и 2 файла:
• папка BuildKit — содержит:
- jdk12, при помощи которого осуществляется запуск приложения (JVM);
- geckodriver.exe — для работы Selenium Web Driver с браузером FireFox;
- SprintAutoTest.jar — непосредственно инстанс приложения
• папка Reports — в нее сохраняются отчеты после завершения выполнения приложением тест кейса. Также должна содержать папку ErrorScreens, куда сохраняется скриншот в случае возникновения ошибки
• папка TestSuite — веб пакеты, яваскрипты, набор тест кейсов (о заполнении данной папке будет подробно рассказано отдельно)
• файл config.properties — содержит конфиг для подключения к БД Oracle и значения явных ожиданий для WebDriverWait
• starter.bat — файлик для запуска приложения (возможен автоматический запуск приложения без ручного указания TestCase если в конце ввести в качестве параметра ввести имя TestCase).
Краткое описание работы приложения
Приложение может быть запущено с параметром (имя TestCase) либо без него — в этом случае необходимо ввести имя тест кейса в консоли самостоятельно.
Пример общего содержания bat файла, для запуска без параметра: start «AutoTest launcher» %cd%\BuildKit\jdk-12\bin\java.exe -Xmx768M -jar --enable-preview %cd%\BuildKit\SprintAutoTest.jar
При общем запуске приложения, оно просматривает xml файлы, находящиеся в директории "\TestSuite\TestCase" (без просмотра содержания вложенных папок). При этом происходит первичная валидация xml файлов на корректность структуры (т.е. что все теги с точки зрения разметки xml указаны верно), и берутся названия, указанные в теге «testCaseName», после чего пользователю предлагается ввести один из возможных вариантов имен имеющихся тест кейсов. В случае ошибочного ввода система попросит ввести имя повторно.
После того как будет получено имя TestCase, выполняется построение внутренней модели, которая представляет из себя связку TestCase (тестовый сценарий) — WebPackage (хранилище элементов) в виде java объектов. После построения модели строится непосредственно TestCase (исполняемый объект программы). На этапе построения TestCase также происходит вторичная валидация — проверяется что все указанные формы в TestCase имеются в связанном WebPackage и что все элементы, указанные в action, имеются в WebPackage в рамках указанных страниц. (О структуре TestCase и WebPackage подробно описано ниже)
После того, как будет построен TestCase, происходит непосредственно запуск сценария
Алгоритм работы сценария (ключевая логика)
TestCase представляет из себя набор сущностей Action, который в свою очередь представляет из себя набор сущностей Event.
TestCase
-> List{Action}
-> List{Event}
При запуске TestCase происходит последовательный запуск Action (каждый Action возвращает логический результат)
При запуске Action происходит последовательный запуск Event (каждый Event возвращает логический результат)
По результату выполнения каждого Event сохраняется результат
Соответственно тест завершается либо, когда все действия были выполнены успешно, либо если Action вернул false.
*Механизм сбоя
Т.к. моя тестируемая система древняя и имеет отлавливаемые ошибки/глюги, которые не являются ошибками, и некоторые события не срабатывают с первого раза, в платформе реализован механизм, который может отходить от вышеописанной концепции жестко линейного теста (однако он жестко типизирован). При отлавливании таких ошибок предусмотрена возможность повторения кейсов сначала и выполнения дополнительных действий для возможности повтора действий
По окончанию работы приложения происходит формирование отчета, который сохраняется в директорию "\Reports". В случае возникновение ошибки делается скриншот, который сохраняется в "\Reports\ErrorScreens"
Заполнение TestSuite
Итак, описание теста. Как уже говорилось, основной параметр, необходимый для запуска – имя теста, который следует запустить. Это имя хранится в xml файле в директории “/TestSuite/TestCase”. В данной директории хранятся все тестовые сценарии. Их здесь может быть сколько угодно. Имя тест кейса берется не из имени файла, а из тега “testCaseName” внутри файла.
В TestCase задается что именно будет делаться – т.е. действия. В директории “/TestSuite/WebPackage” в xml файлах хранятся все локаторы. Т.е. все в лучших традициях – действия хранятся отдельно, локаторы веб форм отдельно.
В TestCase также хранится имя WebPackage в теге “webPackageName”.
Итого картина уже есть. Для запуска необходимо наличие 2-ух xml файлов: TestCase и WebPackage. Они составляют связку. WebPackage независим – в качестве идентификатора указывается имя в теге “webPackageName”. Соответственно вот и первое правило – имена TestCase и WebPackage должны быть уникальными. Т.е. еще раз – по сути наш тест — это связка файлов TestCase и WepPackage, которые связаны именем WebPackage, которое указывается в TestCase. На практике я автоматизирую одну систему и все свои тест кейсы вяжу к одному WebPackage в котором у меня до кучи описание всех форм.
Следующий слой логической декомпозиции основан на таком паттерне, как Page Object.
Page Object
Page Object — один из наиболее полезных и используемых архитектурных решений в автоматизации. Данный шаблон проектирования помогает инкапсулировать работу с отдельными элементами страницы. Page Object как бы моделирует страницы тестируемого приложения в качестве объектов.
Разделение логики и реализации
Существует большая разница между логикой тестирования (что проверить) и его реализацией (как проверить). Пример тестового сценария: «Пользователь вводит неверный логин или пароль, нажимает кнопку входа, получает сообщение об ошибке». Этот сценарий описывает логику теста, в то время как реализация содержит в себе такие действия как поиск полей ввода на странице, их заполнение, проверку полученной ошибки и т.д. И если, например, измениться способ вывода сообщения об ошибке, то это никак не повлияет на сценарий теста, все также нужно будет ввести неверные данные, нажать кнопку входа и проверить ошибку. Но это напрямую затронет реализацию теста — необходимо будет изменить метод получающий и обрабатывающий сообщение об ошибке. При разделении логики теста от его реализации автотесты становятся более гибкими и их, как правило, легче поддерживать.
*! Нельзя говорить о том, что данный архитектурный подход применен полностью. Речь идет лишь о декомпозиции описания тестового сценария постранично, что помогает писать тесты быстрее и добавлять дополнительные автопроверки на всех страницах, стимулирует к правильному описанию локаторов (чтобы они не были одинаковыми на разных страницах) и строит «красивую» логическую структуру теста. Сама платформа реализована на принципах «Чистой архитектуры»
Разделение логики и реализации
Существует большая разница между логикой тестирования (что проверить) и его реализацией (как проверить). Пример тестового сценария: «Пользователь вводит неверный логин или пароль, нажимает кнопку входа, получает сообщение об ошибке». Этот сценарий описывает логику теста, в то время как реализация содержит в себе такие действия как поиск полей ввода на странице, их заполнение, проверку полученной ошибки и т.д. И если, например, измениться способ вывода сообщения об ошибке, то это никак не повлияет на сценарий теста, все также нужно будет ввести неверные данные, нажать кнопку входа и проверить ошибку. Но это напрямую затронет реализацию теста — необходимо будет изменить метод получающий и обрабатывающий сообщение об ошибке. При разделении логики теста от его реализации автотесты становятся более гибкими и их, как правило, легче поддерживать.
*! Нельзя говорить о том, что данный архитектурный подход применен полностью. Речь идет лишь о декомпозиции описания тестового сценария постранично, что помогает писать тесты быстрее и добавлять дополнительные автопроверки на всех страницах, стимулирует к правильному описанию локаторов (чтобы они не были одинаковыми на разных страницах) и строит «красивую» логическую структуру теста. Сама платформа реализована на принципах «Чистой архитектуры»
Далее я постараюсь подробно не расписывать структуру WebPackage и TestCase. Для них я создал DTD схему для WebPackage и XSD 1.1 для TestCase.
! ВАЖНО
За счет ведения DTD и XSD схем осуществляется концепция быстрого написания теста.
При написании непосредственно WebPackage и TestCase необходимо использовать xml Editor со встроенными функциями валидации DTD и XSD в реальном времени с автогенерацией тегов, что сделает сам процесс написания авто теста в значительной мере автоматизированным (все обязательные теги будут подставлены автоматически, для значений атрибутов будут выданы выпадающие списки возможных значений, под тип ивента будут сгенерированы соответствующие теги).
Когда эти схемы «прикручиваются» к самому xml файлу, то можно забыть про корректность структуры xml файла, если использовать специальную среду. Мой выбор пал на oXygen XLM Editor. Еще раз – без использования подобной проги, вы не поймете суть скорости написания. Idea для этого не очень подходит т.к. в ней не обрабатывается конструкция XSD 1.1 “alternative”, которое имеет ключевое значение для TestCase.
WebPackage
WebPackaege — xml файл, описывающий элементы веб форм, находится в директории "\TestSuite\WebPackage". (может быть сколько угодно много файлов. Название файлов может быть любым — значение имеет только содержание).
DTD (вставляется в начало документа):
<!DOCTYPE webPackage [
<!ELEMENT webPackage (webPackageName, forms)>
<!ELEMENT webPackageName (#PCDATA)>
<!ELEMENT forms (form+)>
<!ELEMENT form (formName, elements+)>
<!ELEMENT formName (#PCDATA)>
<!ELEMENT elements (element+)>
<!ELEMENT element (name, locator)>
<!ATTLIST element type (0|1|2|3|4|5|6|7) #REQUIRED>
<!ATTLIST element alwaysVisible (0|1) #IMPLIED>
<!ELEMENT name (#PCDATA)>
<!ELEMENT locator (#PCDATA)>
<!ATTLIST locator type (1|2) #IMPLIED>
]>
В общем виде выглядит примерно
<webPackage>
<webPackageName>уникальное_название</webPackageName>
<forms>
<form>
<formName>пустая_форма_авторизации_при_открытии_тестового_приложения</formName>
<elements>
<element type="2" alwaysVisible="1">
<name>наименование_элемента</name>
<locator type="2">.//div/form/div/div/form/table/tbody/tr/td[text()="Логин"]/following-sibling::td/input</locator>
</element>
<element type="2">
<name>наименование_другого_элемента</name>
<locator>.//div/form/div/div/form/table/tbody/tr/td[text()="Логин"]/following-sibling::td/input</locator>
</element>
.......
</elements>
</form>
.......
</forms>
</webPackage>
Как уже говорилось, чтобы элементы были не в кучи – все декомпозировано по веб формам
Ключевой сущностью является
<element>
Тег element имеет 2 атрибута:
- type
- alwaysVisible
Атрибут type является обязательным и задает тип элемента. В платформе задается типом byte
На текущей момент конкретно для себя в платформе реализовал следующие типы:
• 0 — не имеет функционального смысла, обычно какая-то надпись
• 1 — кнопка (button)
• 2 — поле ввода (input)
• 3 — чекбокс (checkBox)
• 4 — выпадающий список (select) – на самом деле не реализован, но место под него оставил
• 5 — для выпадающего списка srm: пишем название, дожидаемся появления значения – выбираем по конкретному xpath шаблону – тип конкретно под мою систему
• 6 — srm select — используется на типовых функциях типа поиска и т.д. – тип конкретно под мою систему
Атрибут alwaysVisible — необязательный — показывает присутствует ли элемент на форме всегда, может использоваться при начальной/конечной валидации Action (т.е. в автоматическом режиме можно проверить, что при открытии формы на ней присутствуют все элементы, которые есть на ней всегда, при закрытии формы, все эти элементы исчезли)
Возможные значения:
- 0 — по умолчанию (или если атрибут не задан) — элемент может отсутствовать на странице (не валидировать)
- 1 — элемент всегда присутствует на странице
Дополнительный необязательный атрибут type реализован у тега locator
Возможные значения:
- 1 — поиск элемента по id (соответственно в локаторе указать только id)
- 2 — по умолчанию (или если атрибут не задан) — поиск по xpath — рекомендуется использовать только поиск по xpath, т.к. данный метод сочетает в себе практически все преимущества остальных и является универсальным
TestCase
TestCase — xml файл, описывающих непосредственно сценарий тестирования, находится в директории "\TestSuite\TestCase" (может быть сколько угодно много файлов. Название файлов может быть любым — значение имеет только содержание).
XSD схема:
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.1">
<xs:element name="testCase">
<xs:complexType>
<xs:sequence>
<xs:element name="testCaseName" type="xs:string"/>
<xs:element name="webPackageName" type="xs:string"/>
<xs:element name="actions" type="actionsType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="actionsType">
<xs:sequence>
<xs:element name="action" type="actionType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="actionType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="orderNumber" type="xs:positiveInteger"/>
<xs:element name="runConfiguration" type="runConfigurationType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="runConfigurationType">
<xs:sequence>
<xs:element name="formName" type="xs:string"/>
<xs:element name="repeatsOnError" type="xs:positiveInteger" minOccurs="0"/>
<xs:element name="events" type="eventsType"/>
<xs:element name="exceptionBlock" type="eventsType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="openValidation" use="required">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="1"/>
<xs:enumeration value="0"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="closeValidation" use="required">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="1"/>
<xs:enumeration value="0"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<xs:complexType name="eventBaseType">
<xs:sequence>
<xs:element name="orderNumber" type="xs:positiveInteger"/>
</xs:sequence>
<xs:attribute name="type" use="required">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="goToURL"/>
<xs:enumeration value="checkElementsVisibility"/>
<xs:enumeration value="checkElementsInVisibility"/>
<xs:enumeration value="fillingFields"/>
<xs:enumeration value="clickElement"/>
<xs:enumeration value="dbUpdate"/>
<xs:enumeration value="wait"/>
<xs:enumeration value="scrollDown"/>
<xs:enumeration value="userInput"/>
<xs:enumeration value="checkInputValues"/>
<xs:enumeration value="checkQueryResultWithUtilityValue"/>
<xs:enumeration value="checkFieldsPresenceByQueryResult"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="invertResult" use="optional" default="0">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="1"/>
<xs:enumeration value="0"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="hasExceptionBlock" use="optional" default="0">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="1"/>
<xs:enumeration value="0"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<xs:complexType name="eventsType">
<xs:sequence>
<xs:element name="event" type="eventBaseType" maxOccurs="unbounded">
<xs:alternative test="@type='goToURL'" type="eventGoToURL"/>
<xs:alternative test="@type='checkElementsVisibility'" type="eventCheckElementsVisibility"/>
<xs:alternative test="@type='checkElementsInVisibility'" type="eventCheckElementsVisibility"/>
<xs:alternative test="@type='fillingFields'" type="eventFillingFields"/>
<xs:alternative test="@type='checkInputValues'" type="eventFillingFields"/>
<xs:alternative test="@type='clickElement'" type="eventClickElement"/>
<xs:alternative test="@TYPE='dbUpdate'" type="eventRequest"/>
<xs:alternative test="@type='wait'" type="utilityValueInteger"/>
<xs:alternative test="@type='scrollDown'" type="eventClickElement"/>
<xs:alternative test="@type='userInput'" type="eventClickElement"/>
<xs:alternative test="@type='checkQueryResultWithUtilityValue'" type="eventRequestWithValue"/>
<xs:alternative test="@type='checkFieldsPresenceByQueryResult'" type="eventRequestWithValue"/>
</xs:element>
</xs:sequence>
</xs:complexType>
<!-- ТИПЫ ДЛЯ EVENTS -->
<xs:complexType name="eventGoToURL">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="url" type="xs:anyURI"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="eventCheckElementsVisibility">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="fields" type="fieldType"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="eventFillingFields">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="fields" type="fieldTypeWithValue"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="eventClickElement">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="elementName" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="eventRequest">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="dbRequest" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="utilityValueInteger">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="utilityValue" type="xs:positiveInteger"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="eventRequestWithValue">
<xs:complexContent>
<xs:extension base="eventBaseType">
<xs:sequence>
<xs:element name="dbRequest" type="xs:string"/>
<xs:element name="utilityValue" type="xs:string"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<!-- ТИПЫ ДЛЯ EVENTS -->
<xs:complexType name="fieldType">
<xs:sequence>
<xs:element name="field" maxOccurs="unbounded">
<xs:complexType>
<xs:choice>
<xs:element name="element" type="xs:string"/>
<xs:element name="xpath" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="fieldTypeWithValue">
<xs:sequence>
<xs:element name="field" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="element" type="xs:string"/>
<xs:element name="value" type="valueType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="valueType">
<xs:complexContent>
<xs:extension base="xs:anyType">
<xs:attribute name="type" use="optional" default="1">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="1"/>
<xs:enumeration value="2"/>
<xs:enumeration value="3"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:schema>
Общий вид:
<!DOCTYPE testCase SYSTEM "./TestSuite/TestCase/entities.dtd" []>
<testCase xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="testShema.xsd">
<testCaseName>уникальное_наименование_testCase</testCaseName>
<webPackageName>название_webPackage_из_webPackageName</webPackageName>
<actions>
<action>
<name>Вход через пустую форму авторизации в интерфейс КМа для инициализации приложения</name>
<orderNumber>10</orderNumber>
<runConfiguration openValidation="1" closeValidation="1">
<formName>пустая_форма_авторизации_при_открытии_тестового_приложения</formName>
<events>
<event type="goToURL">
<orderNumber>10</orderNumber>
<url>&srmURL;</url>
</event>
.......
</events>
</runConfiguration>
</action>
.......
</actions>
</testCase>
Вот в этой строчке видно как прикрутить xsd схему чтобы XML Editor ее видел:
<testCase xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="testShema.xsd">
В TestCase я также использую сущности DTD, которые хранятся отдельно в той же директории – файлик с расширением .dtd. В нем я храню практически все данные – константы. Также я построил логику таким образом, что чтобы запустить новый тест, и по ходу всего теста создавались новые уникальные сущности, регистрировался новый КА, достаточно поменять 1 цифру в этом файле.
Структура его очень проста - приведу пример:
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY mainNumber '040'>
<!ENTITY mail '@mail.ru'>
<!ENTITY srmURL 'https://srm-test.ru'>
В значение тега такие константы вставляются так:
<url>&srmURL;</url>
— можно комбинировать.
! Рекомендация — при написании testCase следует указывать эти сущности DTD внутри документа, и уже после того, как все стабильно заработает, переносить в отдельный файлик. У моего xml editor-а с этим сложности – он не может найти DTD и не берет во внимание XSD, поэтому рекомендую так
testCase
testCase — самый родительский тег содержит:
- testCaseName — имя нашего тест кейса, именно этот параметр передается на вход приложению
- webPackageName — имя WebPackage, которое указано в webPackageName (см пп выше о WebPackage)
- actions — контейнер сущностей action
action
Содержит:
- name — имя — рекомендуется указывать имя формы и ключевые действия — что и зачем
- orderNumber — порядковый номер — параметр необходимый для сортировки action (введен в связи с тем, что при разборе xml в java при использовании определенных инструментов разбор может быть осуществлен в многопоточной среде, в связи с чем порядок может поехать) — при указании следующего action можно перепрыгивать — т.е. имеет при сортировке имеет значение только «больше/меньше», а так можно доставлять action между уже описанными без необходимости менять всю нумерацию
- runConfiguration — собственно описание того, что будет происходить в рамках action
runConfiguration
Содержит:
- атрибут openValidation — необязательный, по умолчанию «0»
- 0 — не проводить начальную валидацию формы
- 1 — произвести начальную валидацию формы
- атрибут closeValidation — необязательный, по умолчанию «0»
- 0 — не проводить конечную валидацию формы
- 1 — произвести конечную валидацию формы
- formName — имя формы в рамках которой будут проводиться действия — значение formName из WebPackage
- repeatsOnError — необязательный, указывается какое количество повторов необходимо произвести в случае сбоя
- events — контейнер сущностей event
- exceptionBlock — необязательный — контейнер сущностей event, которые выполняются в случае ошибки
event
Минимальная структурная единичка — данная сущность показывает, какие действия совершаются
Каждый event особенный, может иметь уникальные теги и атрибуты.
Базовый тип содержит:
- атрибут type — указывается тип элемента
- атрибут hasExceptionBlock — необязательный атрибут, по умолчанию «0» — необходим для реализации механизма сбоя — атрибут говорит о том, что на этом ивенте мы можем ожидать ошибку
- 0 — не ожидается ошибка
- 1 — ожидается возможная ошибка на действии
- атрибут invertResult — необязательный атрибут, по умолчанию «0» — атрибут говорит о том, что необходимо поменять результат ивента
- 0 — оставить результат выполнения ивента по факту
- 1 — изменить результат ивента на противоположный
*! Механизм описания ожидаемой ошибки
Приведу банальный пример где это было использовано мной в первый раз и что сделать, чтобы он сработал.
Кейс: ввод капчи. Данный момент я не смог автоматизировать, так сказать проверка на робота пока подкашивает – не пишут мне тестовый сервис капчи (а мне самому уже проще сделать нейросеть для распознавания))) Так вот, мы можем ошибиться при вводе. На этот случай я делаю контрольный ивент, в котором проверяю, что у нас отсутствует элемент – уведомление о неверном контрольном коде, ставлю на нем атрибут hasExceptionBlock. Предварительно я задал на action, что у нас может быть несколько повторений (5) и после всего прописал exceptionBlock, в котором прописал, что надо нажать кнопку выхода из уведомления, после чего action повторяется.
Примеры из моего контекста.
Вот я как я прописал ивент:
А вот exceptionBlock после ивентов
И да, действия на одной странице можно декомпозировать на несколько action.
+ кто заметил в конфиге 2 параметра: defaultTimeOutsForWebDriverWait, lowTimeOutsForWebDriverWait. Так вот зачем они. Т.к. весь веб драйвер у меня в синглтоне, и я не захотел создавать каждый раз новый WebDriverWait, то у меня 1 быстрый, и он на случай ошибки (ну или если вы просто поставите hasExceptionBlock=«1», то он тупо будет с меньшим количеством времени явного ожидания) – ну согласитесь, ждать минуту чтобы убедиться, что сообщение не вылезло не комильфо, как и создавать каждый раз новый WebDriverWait. Ну и эта ситуация с какой стороны не ткнись требует костылька – я решил сделать так.
Кейс: ввод капчи. Данный момент я не смог автоматизировать, так сказать проверка на робота пока подкашивает – не пишут мне тестовый сервис капчи (а мне самому уже проще сделать нейросеть для распознавания))) Так вот, мы можем ошибиться при вводе. На этот случай я делаю контрольный ивент, в котором проверяю, что у нас отсутствует элемент – уведомление о неверном контрольном коде, ставлю на нем атрибут hasExceptionBlock. Предварительно я задал на action, что у нас может быть несколько повторений (5) и после всего прописал exceptionBlock, в котором прописал, что надо нажать кнопку выхода из уведомления, после чего action повторяется.
Примеры из моего контекста.
Вот я как я прописал ивент:
<event type="checkElementsInVisibility" hasExceptionBlock="1">
<orderNumber>57</orderNumber>
<fields>
<field>
<element>уведомление_неверный_контрольный_код</element>
</field>
</fields>
</event>
А вот exceptionBlock после ивентов
<exceptionBlock>
<event type="clickElement">
<orderNumber>10</orderNumber>
<elementName>кнопка_ок_для_выхода_из_уведомления</elementName>
</event>
</exceptionBlock>
И да, действия на одной странице можно декомпозировать на несколько action.
+ кто заметил в конфиге 2 параметра: defaultTimeOutsForWebDriverWait, lowTimeOutsForWebDriverWait. Так вот зачем они. Т.к. весь веб драйвер у меня в синглтоне, и я не захотел создавать каждый раз новый WebDriverWait, то у меня 1 быстрый, и он на случай ошибки (ну или если вы просто поставите hasExceptionBlock=«1», то он тупо будет с меньшим количеством времени явного ожидания) – ну согласитесь, ждать минуту чтобы убедиться, что сообщение не вылезло не комильфо, как и создавать каждый раз новый WebDriverWait. Ну и эта ситуация с какой стороны не ткнись требует костылька – я решил сделать так.
Типы event
Здесь я приведу минимальный набор моих ивентов, типа набора бойскаута, с помощью которых я по своей системе могу протестить практически все.
И теперь плавненько к коду для понимания что такое ивент и как он строится. В коде по сути реализован фреймворк. У меня есть 2 класса – DataBaseWrapper и SeleniumWrapper. В этих классах описано взаимодействие с инфраструктурными компонентами, а также отражаются особенности платформы. Приведу интерфейс, который реализует SeleniumWrapper
package logic.selenium;
import models.ElementWithStringValue;
import models.webpackage.Element;
import org.openqa.selenium.WebElement;
public interface SeleniumService {
void initialization(boolean webDriverWait);
void nacigateTo(String url);
void refreshPage();
boolean checkElementNotPresent(Element element);
WebElement findSingleVisibleElement(Element element);
WebElement findSingleElementInDOM(Element element);
void enterSingleValuesToWebField(ElementWithStringValue element);
void click(Element element);
String getInputValue(Element element);
Object jsReturnsValue(String jsFunction);
//Actions actions
void doubleClick(Element element);
void moveMouseToElement(Element element);
void pressKey(CharSequence charSequence);
void getScreenShot(String storage);
}
В нем описываются все возможности селениума и накладываются фишки платформы – ну собственно основная фишка – это метод «enterSingleValuesToWebField». Помните, что мы в WebPackage указываем тип элемента. Так вот, то как на этот тип реагировать при заполнении полей прописано тут. Пишем 1 раз и забываем. Вышеуказанный метод стоит поправить под себя в первую очередь. К примеру типы 5 и 6, ныне действующие – подходят только для моей системы. А если у вас есть такая штука как фильтр и надо много фильтровать, и он типовой (в вашем веб приложении), но, чтобы им воспользоваться надо сначала навести мышку на поле, дождаться появления каких-то полей, перейти на какое-то, дождаться там чего-то, затем перейти туда и ввести… Тупо прописываете механизм действия 1 раз, даете уникальный тип этому всему в конструкции switch – далее не заморачиваетесь – получаете полиморфный метод на все аналогичные фильтры приложения.
Итак, в пакете «package logic.testcase.events» есть абстрактный класс, описывающий общие действия ивента. Для того чтобы создать свой уникальный ивент, необходимо создать новый класс, унаследоваться от этого абстрактного класса, и у вас уже в комплекте есть и dataBaseService и seleniumService – а далее вы определяете какие данные вам нужны и что с ними делать. Как-то так. Ну и соответственно, после создания нового ивента надо допилить класс-фабрику TestCaseActionFactory и по возможности XSD схему. Ну и, если добавляется новый атрибут – доработать саму модель. На деле это очень легко и быстро.
Итак, набор бойскаута.
goToURL – обычно первое действие – переход по указанной ссылке
Пример:
<event type="goToURL">
<orderNumber>10</orderNumber>
<url>testURL</url>
</event>
fillingFields — Заполнение указанных элементов
Специальные теги:
- fields — контейнер сущности fiel
- field — содержит тег element
- element — указывается имя элемента из webPackage
- value — какое значение указать, имеет необязательный атрибут type (если елемент чекбокс, то указывается одно из значений: «check» или «uncheck»)
- field — содержит тег element
- атрибут type — указывается каким образом взять значение, необязательный, значение по умолчанию — «1»
- 1 — берется указанное значение
- 2 — в этом случае выполняется указанная JS функция из директории "\TestSuite\JS"! ВАЖНО — указывается название txt файла, без ".txt" (и я пока нашел применения js функциям пока только в таком виде – использую в одном месте для генерации случайного инн, однако спектр возможного применения широк)
- 3 — в этом случае указывается в качестве значения указывается запрос в БД, а программа подставляет в 1-ый результат данного запроса
Пример:
<event type="fillingFields">
<orderNumber>10</orderNumber>
<fields>
<field>
<element>test</element>
<value>test</value>
</field>
</fields>
</event>
checkElementsVisibility – проверяется, что указанные элементы присутствуют на форме (именно видимые, а не просто в DOM). В атрибуте field может быть указан или элемент из WebPackage или непосредственно xpath
Пример:
<event type="checkElementsVisibility">
<orderNumber>10</orderNumber>
<fields>
<field>
<element>test</element>
</field>
<field>
<xpath>test</xpath>
</field>
</fields>
</event>
checkElementsInVisibility – аналогичный checkElementsVisibility, но наоборот
clickElement – клик по указанному элементу
Пример:
<event type="clickElement">
<orderNumber>10</orderNumber>
<elementName>test</elementName>
</event>
checkInputValues – проверка введенных значений
Пример:
<event type="checkInputValues">
<orderNumber>10</orderNumber>
<fields>
<field>
<element>test</element>
<value>test</value>
</field>
</fields>
</event>
dbUpdate – выполнить апдейт в базе (oXygen странно реагирует на 1 ивент типа dbUpdate – не знаю что с ним сделать и не понимаю почему)
Пример:
<event type="dbUpdate">
<orderNumber>10</orderNumber>
<dbRequest>update что-то там</dbRequest>
</event>
CheckQueryResultWithUtilityValue — проверка введенного пользователем значения с значением из БД
Пример:
<event type="checkQueryResultWithUtilityValue">
<orderNumber>10</orderNumber>
<dbRequest>select ...</dbRequest>
<utilityValue>test</utilityValue>
</event>
checkFieldsPresenceByQueryResult — проверка наличия элементов на форме по xpath по паттерну. Если желаемы паттерн не указан, то поиск произойдет по паттерну .//*[text()[contains(normalize-space(.), "$")]], где вместо «$» будет значение из БД. При описании собственного паттерна, на то место, куда следует поставить значение из БД – надо указать «$». В моей системе есть так называемые гриды в которых бывают значения, которые как правило формируются из какой-то вьюхи. Этот ивент для проверки таких гридов
Пример:
<event type="checkFieldsPresenceByQueryResult">
<orderNumber>10</orderNumber>
<dbRequest>test</dbRequest>
<utilityValue></utilityValue>
</event>
Wait – все просто – ожидание указанного количества милисекунд. К сожалению, хоть это и принято считать костылем, скажу точно – иногда без него невозможно обойтись
Пример:
<event type="wait">
<orderNumber>10</orderNumber>
<utilityValue>1000</utilityValue>
</event>
scrollDown – скрол вниз от указанного элемента. Делается так: кликается по указанному элементу и типа нажимается клавиша «PgDn». В моих кейсах, где мне надо было скрольнуть вниз работает отлично:
Пример:
<event type="scrollDown">
<orderNumber>10</orderNumber>
<elementName>test</elementName>
</event>
userInput – ввод значения в указанный элемент. Единственный полуавтомат в моей автоматизации, используется только для капчи. Указывается элемент, в который надо ввести значение. Значение вводится в всплывшее диалоговое окошко.
Пример:
<event type="userInput">
<orderNumber>10</orderNumber>
<elementName>capch_input</elementName>
</event>
Про код
Значит, я старался сделать платформу в соответствии с принципами Чистой архитектуры дядюшки Боба.
Пакеты:
application – инициализация и запуск + конфиги и отчет (не ругайте меня за класс Report – вот что максимально на скорую руку, то максимально на скорую руку)
logic – ключевая логика + сервисы Селениума и БД. Тут же ивенты.
models – POJO на XML и все вспомогательные классы-объекты
utils – синглтоны для селениума и БД
Чтобы код запустить, надо скачать jdk 12 и везде указать, чтобы включились ее фишки. В Idea это делается через Project Structure –> Modules и Project. Также не забыть про Maven runner. И при запуске в бат файле дописать --enable-preview. Пример был.
Ну и, чтобы все еще запустилось, по мимо JDK потребуется скачать ojdbc драйвер и закинуть джарник в директорию “SprintAutoTest\src\lib”. Я его не предоставляю, т.к. у оракла там щас все серьезно – чтобы скачать надо зарегаться, но я уверен, все желающие так или иначе справятся (ну и проверьте, чтобы все папки были созданы, а то отчет не сохранится)
Резюме
Итак, мы имеем запускалку тестов, написание тестов на которую делается действительно быстро. За рабочую неделю мне удалось автоматизировать 1,5 часа ручной работы, которая выполняется роботом за 5 – 6 минут. Это примерно 3700 строчек конкатенированного тест кейса и 830 описанных элементов (более 4800 строчек). Цифры грубые, и так оно не меряется, но кто занимается автоматизацией должен понимать, что это весьма высокий показатель, тем более для недружелюбных к роботам систем. При этом я тестирую все – бизнес-логику, по ходу выполняю какие-то негативные тесты на корректность заполненных атрибутов, и как бонус проверяю полностью каждую веб-форму, по которой не лентяйничаю, и описываю все функциональные и ключевые элементы, в независимости нужны они мне или нет (Небольшое отступление – closeValidation я применяю в основном только при написании теста. Когда он стабилен, и понятно, что локаторы не пересекаются, я его отключаю для всех action, дабы процесс шел шустрее).
На первый взгляд кажется, что много строчек xml, однако на практике они генерируются полуавтоматически, при этом ошибку можно совершить только в непосредственно вводимых параметрах (т.к. по сути мы имеем 2 уровня валидации – 1-ый это xml схемы, 2-ой проверки наличия указанных форм и элементов при старте запуска TestCase).
Из минусов – нет четких границ тестов. Как таковые они отсутствуют и можно упрекнуть меня, что это просто запускалка макроса, а не тестов. Я оговариваю это так:
В платформе тесты концептуально с моей точки зрения делятся на несколько уровней абстракции:
- как касета + конечный результат — тест завершается если «порвалась пленка» + как рекомендация — это валидация конечного результата (то к чему мы приходим по окончании тестирования функциональности – т.е. валидация конечного результата бизнес-процесса)
- описание действий декомпозировано постранично (сущность action — это совокупность event в рамках одной страницы c промежуточной валидацией действий + начальная и конечная валидация страницы в автоматическом режиме)
- описанными ивентами, мы не определяем сами тесты, однако они есть – их определяет тест-дизайнер. Да и никто не мешает развернуть платформу так, чтобы каждый ивент представлял из себя тест – но именно от этого я пытался уйти. У меня есть пару ивентов, содержащих ком. тайну, которые я убрал – которые просто было удобнее описать как ивент, но сути дела не меняет
- каждое действие на странице представляет из себя unit тест (возвращающий единственный результат true или false)
Если сравнивать мой подход с чем-то, то минусы, например, популярного Cucumber и самой концепции BDD для меня более существенны (именно когда мы тестируем подобные моей системы):
- Мы не можем гарантировать выполнение тестов при тестировании нестабильного веб приложения. Т.е. в моем случаи для большинства тестов мы не можем гарантировать их исполнения, и они будут валиться на «When», что на мой взгляд не есть вообще приемлемо, если уж мы описываем тестирование набором тестов.
- Ну и везде приводится этот уже бесящий пример с логином. Да за всю мою практику вот именно в логине никогда ошибок не было никогда (хотя он конечно должен быть покрыт, это точно). И пример хорош, но для остальных тестов надо лепить бесконечные костыли из Given и When – в приведенной мной системе для реальных тестов потребуется 99% на описание промежуточных условий, пока мы к этому самому тесту дойдем – много мороки, мало сути, да еще и надо в код лезть.
Из того что я хотел бы сделать, над чем подумать, но еще не дошли руки:
- запуск не одного, а нескольких тестов последовательно в одном запуске
- думаю, прокачать сущности, чтобы можно было в них генерить значения по факту нового теста, и они сохранялись по ходу выполнения
- сделать централизованный версионный контроль. Не просто git, а, чтобы указывать какую версию теста запустить, ну или опять же умный модуль, который бы понимал какая версия актуальна, какая еще нет
- как я уже говорил, для запуска нового теста с новыми значениями, мне надо поменять 1 цифру, автоматизировать как бы и это – сделать на это умный модуль
- хоть меня и не сильно смущает, что у меня все локаторы хранятся в одном месте, все же по-хорошему надо бы сделать еще более user friendly структуру для их хранения
- не баловался с selenium server. Думаю, стоит задуматься об этом, как и возможность дальнейшей адаптации к CI, Team City и пр. вкусностям
Ну в общем-то и все. Прилагаю ссылочку на github: исходники.
Буду крайне рад конструктивной критике, надеюсь этот проект действительно будет полезен.