Трудности автоматизации тестовых сценариев Selenium

20/06/23
Технические статьи
Трудности автоматизации тестовых сценариев Selenium

Введение

Компания Аурига уже долгое время занимается автоматизированным тестированием веб-приложений. Однако ранее каждая команда разработки начинала свою работу с базовых шагов, таких как создание проекта для автотестирования, настройка окружения и разработка микро-фреймворка на базе популярного инструмента Selenium.

Мы пришли к выводу о необходимости избавиться от излишних временных и ресурсных затрат на начальные этапы проекта и разработали шаблон проекта, который объединяет все необходимые компоненты для быстрого старта и более эффективного продвижения в разработке самих тестов.

За годы практики Ауриги наши команды неоднократно сталкивались с различными неочевидными ситуациями при использовании Selenium. Хотя эти ситуации не были сложными, они требовали дополнительных усилий для их решения. Мы приняли решение поделиться нашими знаниями о таких «сюрпризах», с которыми могут столкнуться разработчики при использовании Selenium, а также предложить советы и рецепты, чтобы помочь другим разработчикам избежать подобных проблем.

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

Идентификация элемента через предшественника того же уровня

Задача: получить WebElement для изображений (тэги img). Однако эти элементы не имеют каких-либо атрибутов, по которым их можно идентифицировать (например, id, class и т. д.).

<div> 
    <p>Valid image</p> 
    <img src="/images/Toolsqa.jpg"> 
    <br> 
    <br> 
    <p>Broken image</p> 
    <img src="/images/Toolsqa_1.jpg"> 
    <br><br><p>Valid Link</p> 
    <a href="http://demoqa.com">Click Here for Valid Link</a> 
    <br> 
    <br> 
    <p>Broken Link</p> 
    <a href="http://the-internet.herokuapp.com/status_codes/500">Click Here for Broken Link</a> 
</div> 

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

В данном случае рекомендуется использовать xpath для идентификации элементов на странице и ось «following-sibling», которая выбирает узлы одного уровня после текущего узла. Это позволит более устойчиво идентифицировать элементы, не зависящие от их абсолютного пути, и уменьшит вероятность нарушения работы тестов при изменении страницы.

Для получения доступа к первому изображению можно использовать следующий xpath:

//p[. = 'Valid image']/following-sibling::img[1]

Пример кода на java:

private WebElement getValidImageElement() { 
  return wait.until(ExpectedConditions.visibilityOfElementLocated( 
    By.xpath("//p[. = 'Valid image']/following-sibling::img[1]") 
  )); 
}

Для доступа к последующим img элементам достаточно будет просто поменять индекс с единицы на требуемое значение.

Формирование xpath для выбора нужной даты в календаре

Задача: установить определенную дату в календаре по выбору тестировщика и проверить, что она корректно отображается в поле ввода. Однако возникает проблема, связанная с тем, что значения параметров года, месяца и дня являются одним из ключей доступа к веб-элементам календаря.

Например, HTML-код выбора месяца в этом случае может выглядеть вот так:

<div class="react-datepicker__month-dropdown-container react-datepicker__month-dropdown-container--select" xpath="1"> 
    <select class="react-datepicker__month-select"> 
        <option value="0">January</option> 
        <option value="1">February</option> 
        <option value="2">March</option> 
        <option value="3">April</option> 
        <option value="4">May</option> 
        <option value="5">June</option> 
        <option value="6">July</option> 
        <option value="7">August</option> 
        <option value="8">September</option> 
        <option value="9">October</option> 
        <option value="10">November</option> 
        <option value="11">December</option> 
    </select> 
</div> 

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

public void selectYear(int year) { 
        waitDriver.until(ExpectedConditions.visibilityOfElementLocated(By.xpath( 
                "//select[@class='react-datepicker__year-select']/option[@value='" + Integer.toString(year) + "']"
        ))).click(); 
} 

Таким же способом формируется xpath в методе выбора месяца года:

public void selectMonth(int monthNum) { 
        waitDriver.until(ExpectedConditions.visibilityOfElementLocated(By.xpath( 
                "//select[@class='react-datepicker__month-select']/option[" + Integer.toString(monthNum) + "]"
        ))).click(); 
}

При формировании xpath для выбора дня месяца могут возникнуть трудности с формированием значения самого дня. как в нашем случае, когда в HTML-коде день месяца выглядит следующим образом:

<div class="react-datepicker__day react-datepicker__day--003" tabindex="-1" aria-label="Choose Friday, February 3rd, 2023" role="option" aria-disabled="false" xpath="1">3</div> 

Блоки дней сгруппированы в недели, и для доступа к элементу используется идентификация через модификацию имени класса. Таким образом часть class="react-datepicker__day react-datepicker__day– остается неизменной , а далее необходимо подставить только номер дня. Для корректного отображения даты первые 9 дней месяца должны иметь два нуля в начале, а последующие дни, начиная с 10 числа, — только один. В качестве решения мы предлагаем написать дополнительный метод преобразования дня недели, в зависимости от значения, указанного тестировщиком. Метод должен вернуть значение «001», если тестировщик захочет выбрать 1 день месяца, или, например, «025», если 25-й:

private static String formatDay(int dayNum) { 
    String day; 
    if (dayNum >= 10) { 
        day = "0" + Integer.toString(dayNum); 
    } else { 
        day = "00" + Integer.toString(dayNum); 
    } 
    return day; 
} 

Итоговый метод выбора дня месяца выглядит вот так:

public void selectDay(int dayNum) { 
        waitDriver.until(ExpectedConditions.visibilityOfElementLocated(By.xpath( 
                "//div[contains(@class,'react-datepicker__day react-datepicker__day--" + formatDay(dayNum) 
                + "') and not(contains(@class,'react-datepicker__day--outside-month'))]"
        ))).click();
}

Использование синтаксиса Selenium для создания составной проверки

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

Однако, существует и более элегантное решение с использованием встроенных методов библиотеки Selenium. Например, для проверки двух событий после нажатия на ссылку, таких как изменение кода HTTP и его краткого описания, можно использовать следующий код HTML:

<p id="linkResponse" xpath="1">Link has responded with status <b>201</b> and status text <b>Created</b></p> 

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

waitDriver.until(ExpectedConditions.and(ExpectedConditions.textToBe(By.xpath("//p[@id='linkResponse']/b[1]"), code),     
    ExpectedConditions.textToBe(By.xpath("//p[@id='linkResponse']/b[2]"), status))); 

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

Поиск не отображаемого, но присутствующего элемента

При тестировании веб страницы может возникнуть ситуация, когда элемент присутствует на странице, но его отображение зависит от определенных условий, таких как нажатие на кнопку или ссылку. В таких случаях стандартный метод Selenium visibilityOfElementLocated не сработает, поскольку элемент фактически отсутствует на странице. Например, дочерний элемент для блока с id="progressBar" будет отображаться на странице только после наступления события, запускающего заполнение полосы прогресса.

<div id="progressBar" class="progress"> 
    <div role="progressbar" class="progress-bar bg-info" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">0%</div> 
</div> 

Для тестового сценария, в котором нужно проверить начальное значение прогресс бара, метод visibilityOfElementLocated вернет такое сообщение об ошибке:

Expected condition failed: waiting for visibility of element located by By.xpath: //div[@id='progressBar']/div  

Решением в данном случае является использование метода presenceOfElementLocated:

waitDriver.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@id='progressBar']/div"))); 

Этот метод вернет элемент, который не отображается на странице, но присутствует в DOM.

Использование Actions для эмуляции движения мыши

При проверке текста выпадающей подсказки (ToolTip) может возникнуть трудность, связанная с тем, что контейнер для подсказки отсутствует в DOM до того момента, пока пользователь не наведет курсор мыши на элемент. Для решения данной проблемы можно использовать класс «Actions» из библиотеки Selenium, позволяет эмулировать действия пользователя, таких как движение и нажатие кнопок мыши, нажатие кнопок клавиатуры. Мы написали функцию, используя класс Actions, которая эмулирует наведение курсора мыши на нужный элемент. Пример кода данной функции:

Actions action = new Actions(driver); 
action.moveToElement(waitDriver.until(ExpectedConditions.visibilityOfElementLocated(By.id("toolTipButton")))).build().perform(); 

После наведения курсора мыши у тестировщика на странице появлялась выпадающая подсказка и были реализованы методы проверки ее текста.

Как передвинуть слайдер с помощью Selenium

При тестировании слайдера в пользовательском интерфейсе (UI) может возникнуть вопрос о том, какие методы из библиотеки Selenium следует использовать. Ведь сам слайдер может иметь шкалу с определенными единицами измерения, такими как рубли, метры, мегабайты, в то время как метод dragAndDropBy в Selenium принимает значения в пикселях для перемещения элемента на странице.

Для решения этой проблемы мы предлагаем нижеприведенное выражение, которое позволяет преобразовать единицы измерения, которые мы хотим установить для слайдера по оси X, в пиксели. Основные вычисления выполняются с использованием типа данных double, а затем результат приводится к типу integer.

Actions action = new Actions(driver); 
actions.dragAndDropBy(slider, (int) (-sliderWidth / 2d + sliderWidth * targetValue / (maxValue - minValue)) - 1, 0).perform(); 

В данной формуле выражение -sliderWidth/2d указывает на начало шкалы слайдера в пикселях, а выражение sliderWidth * targetValue / (maxValue - minValue)) преобразовывает единицу измерения в пиксели.

Сценарий проверки скачивания файла

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

Метод waitForNewFilesInFolder

Этот метод ожидает наступление события «Скачать файл», а затем вызывает методы enumerateFilesInFolder и newFileCreatedInFolder. По завершении работы метод возвращает список новых файлов пользователю, если таковые имеются.

public Set<String> waitForNewFilesInFolder(File folder, WebDriverWait driverWait, Runnable trigger) { 
        Set<String> filesSnapshot = enumerateFilesInFolder(folder); 
        trigger.run(); 
        return driverWait.until(newFileCreatedInFolder(folder, filesSnapshot)); 
} 

Метод enumerateFilesInFolder

Данный метод создает список всех файлов в папке для скачивания, исключая файлы с расширениями part и crdownload. Это позволяет получить список файлов до начала загрузки и после ее завершения. Метод работает путем создания «снимка» текущего состояния папки и возвращения списка файлов в этом состоянии.

public Set<String> enumerateFilesInFolder(File folder) { 
    File[] files = folder.listFiles(f -> f.isFile() && !FilenameUtils.isExtension(f.getName(), "part") 
        && !FilenameUtils.isExtension(f.getName(), "crdownload")); 
    return Arrays.stream(files).map(x -> x.getAbsolutePath()).collect(Collectors.toSet()); 
}

Дочерний класс newFileCreatedInFolder

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

public ExpectedCondition<Set<String>> newFileCreatedInFolder(File inFolder, Set<String> previousSnapshot) { 
    return new ExpectedCondition<Set<String>>() { 
            @Override 
            public Set<String> apply(WebDriver unused) { 
                Set<String> currentSnapshot = enumerateFilesInFolder(inFolder); 
                Set<String> ret = currentSnapshot.stream().filter(x -> !previousSnapshot.contains(x)) 
                    .collect(Collectors.toSet()); 
                return ret.isEmpty() ? null : ret; 
            } 
            @Override 
            public String toString() { 
                return String.format("New files created in folder \"%s\"", inFolder); 
            } 
    }; 
} 

Выводы

В данной статье мы рассмотрели некоторые трудности, с которыми можно столкнуться при автоматизации тестовых сценариев на Java с использованием Selenium. Следует отметить, что список проблем, указанных в статье, не является исчерпывающим, так как каждый проект может иметь свои собственные особенности и вызывать уникальные проблемы.

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

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

Свяжитесь с нами напрямую

Офисы

Москва

117587, Варшавское ш., д. 125, стр. 16А

Ростов-на-Дону

344002, пр. Буденновский, д. 9, офис 305

Нижний Новгород

603104, ул. Нартова, д. 6, корп. 6, офис 829