Тестируем веб-страницу на незагружаемые картинки и другие ресурсы, используя Selenium и BrowserMob-Proxy в Java

Бывают случаи, когда от наших автоматизированных тестов на Selenium требуется не просто проверять реакцию UI-компонентов на пользовательский ввод, но также и то, были ли ресурсы, используемые на странице загружены с сервера корректно (нечто похожее на тестирование на "битые" ссылки). К примеру, нам могут быть интересны следующие типы ресурсов:

  • изображения

  • видео

  • CSS-файлы

  • JavaScript-файлы

  • другие типы

Подход, который мы будем рассматривать, также может оказаться полезным при автоматизированном тестировании некоторых AJAX-приложений, которые могут получать ошибочные ответы с сервера, выполняя асинхронные запросы к нему при нахождении самой страницы "в покое". В таких случаях упавшие запросы не обязательно будут отражаться на состоянии пользовательского интерфейса. Знания же, которые вы получите после прочтения этой статьи, помогут вам объединять различные подходы и различный дизайн, увеличивая таким образом эффективность ваших автоматизированных тестов.

Основные моменты, которые мы собираемся сегодня покрыть:

Давайте подробнее взглянем на каждый из них.

Пример: заставляем наш тест на Selenium падать при неудавшейся попытки загрузить на страницу картинку

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

Ниже представлен код обеих страниц.

Советую создать отдельный каталог для хранения этих страниц.

Страница с корректной ссылкой на изображение

<html>
  <head/>
  <body>
    <image src="https://webelement.click/assets/images/ru.png"/>
    <image src="https://webelement.click/assets/images/en.png"/>
  </body>
</html>

Страница с некорректной ссылкой на изображение

<html>
  <head/>
  <body>
    <image src="https://webelement.click/assets/images/ru.png"/>
    <image src="https://webelement.click/assets/images/eng.png"/>
  </body>
</html>

Сохраните первую страницу в файле good.html, а вторую - в файле bad.html (далее в статье я предполагаю, что мы разместили файлы страниц в каталоге /tmp - учитывайте это при переносе примера в свою среду).

Общее описание метода

К сожалению, Selenium сам по себе (в большинстве случаев) не способен распознать ошибочные ответы на http(s) запросы. Вы узнаете, что что-то пошло не так только если намеренно найдете элемент, содержащий искомый ресурс, и проверите некоторые свойства такого элемента. Однако данный метод будет работать далеко не во всех случаях.

Более надёжным и более удобным способом был бы перехват запросов, исходящих от браузера в момент обращения к ресурсам отображаемой страницы. Этого можно легко добиться, используя контролируемые прокси-сервера наподобие BrowserMob-Proxy. Наиболее простым способом подключить BrowserMob-Proxy к вашему проекту является указание соответствующей зависимости в вашем файле pom.xml (кончено же в таком случае ваш проект должен использовать инструмент управления зависимостями Maven). Ниже указана зависимость, используемая в моем примере:

<dependency>
  <groupId>net.lightbody.bmp</groupId>
  <artifactId>browsermob-core</artifactId>
  <version>2.1.5</version>
  <scope>test</scope>
</dependency>

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

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

Реализация тестов

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

package click.webelement.monitorrequests;

import net.lightbody.bmp.BrowserMobProxy;
import net.lightbody.bmp.BrowserMobProxyServer;
import net.lightbody.bmp.client.ClientUtil;
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

import java.util.ArrayList;
import java.util.List;

public class MonitorRequestsTest {

    WebDriver driver;
    static BrowserMobProxy proxy;
    static List<String> failedURIs = new ArrayList<>();

    @BeforeAll
    public static void globalSetup(){
        System.setProperty("webdriver.gecko.driver"
                , "/home/alexey/Desktop/Dev/webdrivers/geckodriver");
        proxy = new BrowserMobProxyServer();
        proxy.start(0);
        proxy.addResponseFilter((httpResponse, httpMessageContents, httpMessageInfo) -> {
            if(httpMessageInfo.getOriginalRequest().headers().get("Accept").contains("image/")){
                int responseCode = httpResponse.getStatus().code();
                if(responseCode >= 400 && responseCode < 600){
                    failedURIs.add(httpMessageInfo.getOriginalRequest().getUri());
                }
            }
        });
    }

    @BeforeEach
    public void setUp(){
        FirefoxOptions ffOptions = new FirefoxOptions();
        ffOptions.setProxy(ClientUtil.createSeleniumProxy(proxy));
        driver = new FirefoxDriver(ffOptions);
        failedURIs.clear();
    }

    @Test
    public void testGoodPage(){
        driver.get("file:///tmp/good.html");
    }

    @Test
    public void testBadPage(){
        driver.get("file:///tmp/bad.html");
    }

    @AfterEach
    public void tearDown(){
        if(driver != null){
            driver.quit();
        }
        if(!failedURIs.isEmpty()){
            Assertions.fail("There were resource loading issues " +
                    "during the test " +
                    "for the following resources: " + failedURIs.toString());
        }
    }

    @AfterAll
    public static void globalTearDown(){
        if(proxy != null){
            proxy.stop();
        }
    }

}

Давайте теперь разобьём весь код примера на несколько частей и рассмотрим каждую по отдельности. Как вы уже могли заметить, кроме двух тестовых методов (которые на самом деле тривиальны) мы имеем в классе 4 блока: @BeforeAll, запускающийся единожды перед всеми тестами; @BeforeEach, запускающийся перед каждым тестом в отдельности; @AfterEach, который выполняется после каждого из тестов; @AfterAll, запускающийся единожды после завершения всех тестов.

Описываем подготовку к запуску всех тестов в @BeforeAll

Этот метод реализует основную идею нашего подхода. Здесь мы стартуем прокси-сервер, который может быть использован для всех наших тестов с одной и той же настройкой (поэтому нет нужды пересоздавать его каждый раз, когда начинается новый тест). В этот сервер мы добавляем логику мониторинга ответов на те запросы, которые через этот сервер проходят. Давайте взглянем на эту часть:

proxy.addResponseFilter((httpResponse, httpMessageContents, httpMessageInfo) -> {
    if(httpMessageInfo.getOriginalRequest().headers().get("Accept").contains("image/")){
        int responseCode = httpResponse.getStatus().code();
        if(responseCode >= 400 && responseCode < 600){
            failedURIs.add(httpMessageInfo.getOriginalRequest().getUri());
        }
    }
});

Используя метод addResponseFilter вы можете обрабатывать ответы сервера для того, чтобы, например, менять их в любом возможном аспекте. Такое изменение ответов будет осуществляться до того, как изменяемые ответы достигнут браузера-адресата. В нашем конкретном случае, мы собираемся реализовать логику наблюдения за кодом ответов, возвращаемых сервером в ответ на запросы, генерируемые нашими автоматизированными тестами.

Прежде всего, нам необходимо (на самом деле мне - вы можете реализовать свою собственную логику) ограничить проверки теми запросами, которые обращаются к ресурсам-изображениям. Существует несколько способов определить отвечает ли запрос подобному критерию. Я предпочитаю анализировать содержимое заголовка запроса Accept. Этим заголовком браузер информирует сервер о том, какой тип контента он готов принять в ответ на свой запрос.

После того как мы удостоверились, что в данный момент обрабатываем запрос, ожидающий изображения, мы проверяем код ответа на этот запрос на предмет попадания в промежуток от 400 (включительно) до 600 (исключительно). Таким способом мы определяем был ли запрос успешным (коды вида 4** означают, что запрос был некорректно сформирован на клиентской стороне, коды вида 5** указывают наличие проблем на стороне сервера)

Заключительным действием, выполняемым в данном блоке, является добавление проблемного пути (URI) в специальный список. Позже мы с этим списком еще столкнёмся.

Описываем подготовку к запуску каждого теста в отдельности в @BeforeEach

Типичным шагов, выполняемым в блоке @BeforeEach является пересоздание объекта WebDriver. В своем примере я придерживаюсь этой же логики. Однако в примере есть еще один момент, на котором стоит не надолго остановиться. Я говорю вот об этой строчке кода:

failedURIs.clear();

Перед началом каждого теста мы очищаем содержимое списка, содержащего проблемные URI. Таким образом, проблемы, диагностированные во время выполнения предыдущего теста не отразятся на следующем.

Описываем логику завершения каждого теста в отдельности в @AfterEach

В представленном дизайне тесты сами по себе описаны в типичном для Selenium ключе (как если бы требования анализировать http-трафик не было вовсе). Такие тесты могут упасть ввиду наличия обычных проблем, типичных для автоматизированных тестов на Selenium: например, отсутствие на странице ожидаемого компонента.

После того как тест заканчивается JUnit5 исполняет код, описанный в блоке @AfterEach. Этот код проверяет пуст ли список, о котором шла речь ранее. Ведь если он содержит элементы, это означает, что во время выполнения тестов, некоторые http-запросы не завершились успешно:

if(!failedURIs.isEmpty()){
    Assertions.fail("There were resource loading issues " +
            "during the test " +
            "for the following resources: " + failedURIs.toString());
}

В последнем случае, тест принудительно завершается со статусом FAILED. Стоит также обратить внимание на то, что этот кусок кода:

if(driver != null){
    driver.quit();
}

идёт в блоке первым. Это важный момент, потому как в случае наличия элементов в списке и завершения теста, выполнение программы прервется, а значит, WebDriver никогда не будет уничтожен.

Описываем логику общего завершения в @AfterAll и запускаем тесты

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

org.opentest4j.AssertionFailedError: There were resource loading issues during the test for the following resources: [/assets/images/eng.png]

	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:109)
	at click.webelement.monitorrequests.MonitorRequestsTest.tearDown(MonitorRequestsTest.java:60)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

Используя рассмотренный подход, вы сможете выполнять любой вид валидации http запросов в своих автоматизированных тестах на Selenium. Если после прочтения у вас всё ещё остались вопросы, присылайте их мне используя эту форму обратной связи. Я постараюсь ответить вам напрямую, а также скорректировать статью, опираясь на ваши отзывы.