Этой статьей мы продолжаем серию публикаций о том, как мы автоматизировали в одном из крупных проектов ЛАНИТ процесс ручного тестирования (далее – автотесты) большой информационной системы (далее – Системы) и что у нас из этого вышло.
Как эффективно организовать иерархию классов? Как распределить пакеты по проектному дереву? Как сделать так, чтобы забыть о мердж-конфликтах при команде в 10 человек? Эти вопросы всегда стоят при старте новой разработки и на них никогда не хватает времени.
В этой статье мы описываем структуру классов и организацию кода, которая позволила нам небольшими силами разработать более полутора тысяч end-2-end UI тестов на базе Junit и Selenium для крупной системы федерального значения. Более того, мы ее успешно поддерживаем и постоянно дорабатываем существующие сценарии.
Здесь вы сможете найти практическое описание структуры иерархии базовых классов автотестов, разбиения проекта по функциональной модели java-packages и шаблоны-образцы реальных классов.
Статья будет полезна всем разработчикам, которые разрабатывают автотесты на базе Selenium.
Эта статья является частью общей публикации, в которой мы описывали, как небольшой командой выстраивали процесс автоматизации UI тестирования и разрабатывали для этого фреймворк на базе Junit и Selenium.
Предыдущие части:
- Часть 1. Организационно-управленческая. Зачем нам была нужна автоматизация. Организация процесса разработки и управления. Организация использования
- Часть 2. Техническая. Архитектура и технический стек. Детали реализации и технические сюрпризы
Реализация базового класса для всех тестов и JUnit RuleChain
Концепция разработки автотестов, как было показано в предыдущей статье (Часть 2. Техническая. Архитектура и технический стек. Детали реализации и технические сюрпризы), базируется на идее фреймворка, при которой для всех автотестов предоставляется набор системных функций – они бесшовно интегрируются и дают возможность разработчикам автотестов концентрироваться на конкретных вопросах бизнес-реализации тест-классов.
Фреймворк включает следующие функциональные блоки:
- Rules – инициализация и финализация тестовых инфраструктурных компонентов как инициализация WebDriver и получение видеотеста. Более подробно описаны далее;
- WebDriverHandlers – вспомогательные функции для работы с веб-драйвером как исполнение Java Script или доступ к логам браузера. Реализованы как набор статических state-less методов;
- WebElements – библиотека типовых веб-элементов или их групп, которая содержит требуемую cross-function функциональность и типовое поведение. В нашем случае к такой функциональности относится возможная проверка завершения асинхронных операций на стороне веб-браузера. Реализованы как расширения веб-элементов из библиотек Selenium и Selenide.
Инициализация тестового окружения. Rules
Ключевым классом для всех тест-классов является BaseTest, от которого наследуются все тест-классы. BaseTest-класс определяет Junit «ранер» тестов и используемый RuleChain, как показано далее. Доступ из прикладных тестовых классов к функциям, предоставляемым rule-классами, осуществляется через статические методы rule-классов.
Образец кода BaseTest представлен на следующей врезке.
@RunWith(FilterTestRunner.class)
public class BaseTest {
private TemporaryFolder downloadDirRule
= new TemporaryFolder(getSomething().getWorkingDir());
@Rule
public RuleChain rules = RuleChain
.outerRule(new Timeout(TimeoutEnum.GLOBAL_TEST_TIMEOUT.value(),
TimeUnit.SECONDS))
.around(new TestLogger())
.around(new StandStateChecker())
.around(new WaitForAngularCreator())
.around(downloadDirRule)
.around(new DownloaderCreator(downloadDirRule))
.around(new EnvironmentSaver())
.around(new SessionVideoHandler())
.around(new DriverCreator(downloadDirRule))
.around(new BrowserLogCatcher(downloadDirRule))
.around(new ScreenShooter())
.around(new AttachmentFileSaver())
.around(new FailClassifier())
.around(new PendingRequestsCatcher());
// набор методов провайдеров общесистемных данных из проперти файлов
final protected SomeObject getSomething() {
return Something.getData();
}
…
}
FilterTestRunner.class – расширение BlockJUnit4ClassRunner, обеспечивает фильтрацию состава исполняемых тестов на базе регулярных выражений по значению специальной аннотации Filter(value = «some_string_and_tag»). Реализация приведена далее.
org.junit.rules.Timeout – используется для ограничения максимального продолжения тестов. Должна устанавливаться первой, так как запускает тест в новой ветке.
TestLogger – класс, который позволяет тесту логировать события в формате json для использования в ELK-аналитике. Обогащает события данными теста из org.junit.runner.Description. Также дополнительно автоматически генерирует события для ELK в формате json для начала-завершения теста с его длительностью и результатом
StandStateChecker – класс, который проверяет доступность веб-интерфейса целевого стенда ДО инициализации веб-драйвера. Обеспечивает быструю проверку, что стенд доступен в принципе.
WaitForAngularCreator – класс, который инициализирует веб-драйвер хэндлер для контроля завершения асинхронных операций ангуляра. Используется для индивидуальной настройки «особых» тестов c длительными синхронными обращениями.
org.junit.rules.TemporaryFolder – используется для задания уникальной временной папки для хранения файлов для операций загрузки и выгрузки файлов через веб-браузер.
DownloaderCreator – класс, который обеспечивает поддержку операций выгрузки во временную директорию файлов, загруженных браузером и записанных через Sеlenoid видеофункцию.
EnvironmentSaver – класс, который добавляет в Allure-отчет общую информацию о тестовом окружении.
SessionVideoHandler – класс, который выгружает файл видеотеста, при его наличии, и прикладывает к отчету Allure.
DriverCreator – класс, который инициализирует WebDriver (самый главный класс для тестов) в зависимости от установленных параметров – локальный, solenoid или ggr-selenoid. Дополнительно класс исполняет набор обязательных для нашего тестирования Java Scripts. Все правила, которые обращаются к веб-драйверу, должны инициализироваться после этого класса.
BrowserLogCatcher – класс, который считывает Severe сообщения из лога браузера, журналирует их для ELK (TestLogger) и прикладывает к Allure-отчету.
ScreenShooter – класс, который для неуспешных тестов снимает скриншот экрана браузера и прикладывает его к отчету аллюр как WebDriverRunner.getWebDriver().getScreenshotAs(OutputType.BYTES)
AttachmentFileSaver – класс, который позволяет приложить к Allure-отчету набор произвольных файлов, требуемых по бизнес-логике тестов. Используется для прикладывания файлов выгруженных или загружаемых в систему.
FailClassifier – особый класс, который пытается в случае падения теста определить, было ли это падение вызвано инфраструктурными проблемами. Проверяет наличие на экране (после падения) особых модальных окон типа «Произошла системная ошибка №ХХХХХХХХХ», а также системных сообщений типа 404 и тому подобное. Позволяет разделить упавшие тесты на бизнес-падения (по сценарию) или системные проблемы. Работает через расширение org.junit.rules.TestWatcher.#failed метод.
PendingRequestsCatcher – еще один особый класс, который пытается дополнительно классифицировать, было ли падение вызвано незавершенными, зависшими или очень длительными рест-сервисами между ангуляром и веб-фронтендом. Дополнительно к функциональному тестированию дает возможность определять проблемные и зависающие рест-сервисы при больших нагрузках, а также общую стабильность релиза. Для этого класс логирует в ELK все события с зависшими рест-запросами, которые он получает, запуская специальный js в браузере через открытый веб-драйвер.
Шаблон реализации тест-класса
package autotest.test.<sub-system>;
@Feature("Развернутое название подсистемы как в TMS")
@Story("Развернутое название теста согласно TMS")
@Owner("фамилия автоматизатора вносящего последние правки в тест кейс")
@TmsLink("Номер теста согласно тест линку. Соответствует настроенному шаблону")
public class <Сквозной номер тест кейса>_Test extends BaseTest {
/**
* Объявляем логин от которой проходит целевой тест
**/
Login orgTest;
/** Объявляем все логины участвующие в тесте как вспомогательные **/
Login loginStep1;
...
Login loginStepN;
/**
* Здесь перечисляются бизнес-объекты которые требуются для проведения теста - Тестовая сцена
* ... для всех требуемых бизнес объектов
**/
/**
* Инициализация тестовой сцены
* Для каждого бизнес объекта необходимо вывести в отчет инициализированное значение
* Utils.allure.message("Бизнес имя объекта в контексте тест-сценария", business_object)
* Если класс инициализируется как null, то его выводят в отчет с указанием на каком шаге он будет заполнен.
* Далее этот тип объекта должен быть дополнительно выведен в отчет в методах preconditions или actions
* Utils.allure.message("Номер созданного документа заполняемого на шаге Х", documentNumber)
**/
@Step("Инициализация тестовых объектов")
private void init(Login login) {
some_business_object = // создание требуемого объекта в или вне зависимости от login
Utils.allure.message("Бизнес имя объекта в контексте тест-сценария", some_business_object)
// ... для всех требуемых бизнес объектов
/** Получаем значения вспомогательных логинов */
loginStep1 = LoginFactory.get(_Some_Login_);
...
loginStepN = LoginFactory.get(_Some_Login_);
}
/**
* Реализация конкретного теста
**/
@Test
@Filter("Название теста для использования фильтрации на уровне JUnit")
@DisplayName("Развернутое название теста согласно TMS")
public void <Сквозной номер тест кейса>_<Полномочие>_<Уникальный_номер_проверки>_Test() {
// Получаем значение тестовой организации
orgTest = LoginFactory.get(_Some_Login_);
// Инициализируем тестовые данные в зависимости от логина
init(orgTest);
// Выполняем шаги тестовых сценариев-предусловий. Шаги предусловия не должны зависеть от значения логина
preconditions();
// Выполняем шаги целевого тестового сценария
actions(orgTest);
}
/**
* Выполнение требуемого набора активности для предусловий теста
**/
@Step("Предварительные условия")
protected void preconditions() {
loginStep1.login();
new SomeAction().apply(someTestObject1, ..., someTestObjectN);
Utils.allure.message("Получено значение для - Бизнес имя объекта в контексте тест-сценария", someTestObjectN)
...
}
/**
* Метод содержит декларативный перечень операций тестового сценария
*/
@Step("Шаги теста")
protected void actions(Login testLogin) {
testLogin.reLogin();
// Выполнение требуемой активности или набора активностей основного теста
new SomeAction().apply(someTestObject1, ..., someTestObjectN);
}
}
Шаблон реализации класса тест-сценария
package autotest.business.actions.some_subsystem;
public class SomeAction {
// ИНИЦИАЛИЗИРУЕМ СТРАНИЦЫ
PageClassA pageA = new PageClassA();
PageClassB pageB = new PageClassB();
@Step("Полное наименование тестового сценария согласно TMS")
@Description("Краткое описание тестового сценария согласно TMS")
public void apply(someTestObject1, ..., someTestObjectN) {
//Шаги сценария по TMS
step_1(...);
step_2(...);
...
step_N(...);
}
@Step("Наименование шага 1 тестового сценария согласно TMS")
private void step_1(...) {
pageA.createSomething(someTestObject1);// just as an example create
}
@Step("Наименование шага 2 тестового сценария согласно TMS")
private void step_2(...) {
pageA.checkSomething(someTestObject1);// just as an example
}
...
}
Реализация класса фильтрации тестов FilterTestRunner
Здесь показана реализация расширения BlockJUnit4ClassRunner для фильтрации тестов на основании произвольных наборов тегов.
/**
* Custom runner for JUnit4 tests.
* Provide capability to do additional filtering executed test methods
* accordingly information that could be provided by {@link FrameworkMethod}
*/
public class FilterTestRunner extends BlockJUnit4ClassRunner {
private static final Logger LOGGER = Logger.getLogger(FilterTestRunner.class.getName());
public FilterTestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
/**
* This method should be used if you want to hide filtered tests completely from output
**/
@Override
protected List<FrameworkMethod> getChildren() {
//Получаем экземпляр фильтра, который сравнивает заданный фильтр со значением аннотации @Filter по требуемой логике
TestFilter filter = TestFilterFactory.get();
List<FrameworkMethod> allMethods = super.getChildren();
List<FrameworkMethod> filteredMethod = new ArrayList<>();
for (FrameworkMethod method: allMethods) {
if (filter.test(method)) {
LOGGER.config("method [" + method.getName() +"] passed by filter [" + filter.toString() + "]" );
filteredMethod.add(method);
} else {
LOGGER.config("method [" + method.getName() +"] blocked by filter [" + filter.toString() + "]" );
}
}
return Collections.unmodifiableList(filteredMethod);
}
/**
* This method should be used if you want to skip filtered tests but no hide them
@Override
protected boolean isIgnored(FrameworkMethod method) {
…
if (filter.test(method)) {
return super.isIgnored(method);
} else {
return true;
}}
*/
}
В следующей части я расскажу о том, как мы реализовали процесс выгрузки файла из контейнера с браузером в тестовый фреймворк, и решили вопрос с поиском имени загруженного браузером файла.
Кстати, будем рады пополнить свою команду. Актуальные вакансии вот здесь.
Автор: Андрей Радосельский