Используем кастомные условия с FluentWait в автоматизированном тестировании на Selenium WebDriver в языке Java

FluentWait обычно используется для ожидания событий, генерируемых в браузере при проведении автоматизированного тестирования. Однако, дизайн этого компонента предполагает возможность его использования с любым типом события, который в Java возможно определить. В этой статье мы посмотрим на несколько примеров того, как можно реализовать ожидание выполнения определенных условий, которые не поддерживаются в стандартной реализации класса #ExpectedConditions.

В статье мы не будем глубоко погружаться в то, как устроен FluentWait. Вместо этого мы сконцентрируемся на трёх примерах, перечисленных ниже. Если вам интересна тем детального рассмотрения этого механизма, рекомендую посетить мой пост о том как устроено взаимодействие FluentWait и ExpectedConditions в Selenium на языке Java. Эта статья - хорошая отправная точка для тех, кто не имеет большого опыта работы с ожиданиями в Selenium.

Вот примеры, которые мы покроем в статье:

  • Ожидание появления файла

  • Ожидание появления определенного количества элементов на странице

  • Ожидание появления определенной записи в логе браузерной консоли

Как реализовать ожидание появления файла через FluentWait на Selenium Java

Ниже представлен пример, демонстрирующий проверку появления файла с интервалом в 1 секунду и таймаутом, равным 30 секундам.

@Test
@DisplayName("Wait For File Appearance Test")
public void testFileExistence() {
    File fileToWait = new File("///PATH_TO_A_FILE/REQUIRED_FILE_NAME");
    Wait<File> fileWaiter = new FluentWait<>(fileToWait)
            .ignoring(IOException.class)
            .pollingEvery(Duration.ofSeconds(1))
            .withTimeout(Duration.ofSeconds(30));
    fileWaiter.until(file -> file.exists());
}

Код выглядит даже красивее, чем если бы мы использовали дополнительную обертку нашего условия вроде класса #ExpectedConditions. Когда мы создаем экземпляр объекта нашего вейтера, мы передаем ему объект File в качестве параметра конструктора, таким образом сообщая, что метод until будет работать с объектом Function (нашей условной функцией), принимающей файлы в качестве входного параметра.

Так как Function - это функциональный интерфейс, мы можем реализовать нужный метод в виде лямбда-выражения (что и демонстрируется в примере). Проще говоря, код примера означает, что файл, который мы передали в конструктор FluentWait будет опрашиваться на предмет существования пока не истечет время таймаута.

Как реализовать ожидание появления нескольких элементов через FluentWait на Selenium Java

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

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

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

class ElementCapacityCondition implements Function<By, Boolean> {

    SearchContext searchContext;
    int expectedCapacity;

    public ElementCapacityCondition(SearchContext searchContext, int expectedCapacity) {
        this.searchContext = searchContext;
        this.expectedCapacity = expectedCapacity;
    }

    @Override
    public Boolean apply(By by) {
        return searchContext.findElements(by).size() == expectedCapacity;
    }
}

В коде примера мы определяем наше условие как функцию, принимающую в качестве параметра объект By и возвращающую Boolean в качестве результата. Логика функции (то, что она выполняет) определено в методе apply(…​) - она совершает попытку найти элементы в указанном контексте поиска (может быть как WebDriver так и WebElement), посчитать их количество, а затем сравнить это количество с ожидаемым.

Теперь взглянем на то как воспользоваться таким условием в тесте:

@Test
@DisplayName("Wait For Number of Elements")
public void testMultipleElementsInCondition() {

    WebElement table = driver.findElmenet(By.xpath("//table"));
    Wait<By> elementsWaiter = new FluentWait<>(By.tagName("./tr"))
            .withTimeout(Duration.ofSeconds(10))
            .pollingEvery(Duration.ofSeconds(2));
    elementsWaiter.until(new ElementCapacityCondition(table, 10));

}

Сперва мы находим родительский элемент (здесь это делается для демонстрации преимущества работы с SearchContext по сравнению с работой просто с WebDriver). В нашем случае - это таблица на странице.

Затем мы создаем вейтер FluentWait, передавая желаемый объект By в качестве параметра конструктора (таким образом, FluentWait затем воспользуется сохраненным объектом, передав его в объект нашей условной функции, когда мы вызовем метод until). Мы также конфигурируем ряд свойств нашего вейтера, такие как интервал опроса и таймаут.

Последнее, что мы делаем - стартуем наш вейтер, вызывая его метод until. В качестве параметра к этому методу мы передаём объект нашей условной функции, которую в свою очередь создаем с поисковым контекстом table и ожидаемым числом элементов. FluentWait знает, что объект должен содержать метод apply, принимающий By в качестве параметра. Таким образом, он берет сохраненный на этапе своего создания объект By и вызывает метод apply у нашей условной функции с этим сохраненным объектом.

Как реализовать ожидание записи в логе консоли браузера через FluentWait на Selenium Java

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

Disclaimer: работа с браузерной консолью в Selenium - это зачастую непростая, а иногда и вовсе не решаемая задача. Дело в том, что несмотря на то, что в Selenium есть возможность вытягивать логи из браузера, то, насколько хорошо эта функция работает (и работает ли вообще) зависит от непосредственной реализации того WebDriver с которой работает ваш код. Например GeckoDriver не поддерживает эту функциональность. Поэтому в своём примере я буду использовать ChromeDriver. Этот момент следует учитывать при реализации подхода из примера.

Для начала, давайте подготовим демо-страничку, которая будет выводить в лог некое случайное сообщение с заданным интервалом:

<html>
  <head>
    <script>
      setInterval(
        function(){
          var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
          console.info(characters.charAt(Math.floor(Math.random() * characters.length)).concat(' TEST_MSG'));
        }, 1000
      );
    </script>
  </head>
  <body>
    Testing console message...
  </body>
</html>

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

class ConsoleMessageCondition implements Function<WebDriver, Boolean> {

    String expectedMessage;

    public ConsoleMessageCondition(String expectedMessage) {
        this.expectedMessage = expectedMessage;
    }

    @Override
    public Boolean apply(WebDriver driver) {
        LogEntries currentEntries = driver.manage().logs().get(LogType.BROWSER);
        System.out.println("Log entries: " + currentEntries.getAll().toString());
        return currentEntries.getAll().stream().anyMatch(logEntry -> logEntry.getMessage().contains(expectedMessage));
    }

}

Это условие построено на тех же принципах, что и предыдущее. Но в отличие от предыдущего случая, мы задаем в качестве типа входного параметра для условия WebDriver потому что функциональность извлечения логов доступна только через него.

Каждый раз когда FluentWait будет проверять выполняется ли наше условие, метод apply будет вытаскивать все логи доступные с момента предыдущей попытки. Будучи "упакованными" в объект LogEntries эти записи можно обрабатывать (в том числе и проверять на вхождение заданных последовательностей) используя стримы для большего синтаксического комфорта.

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

Итак, мы реализовали наше условие. Но этого пока не достаточно для того, чтобы заставить всё работать. Перед тем как запустить тест, нам следуем сконфигурировать наш WebDriver таким образом, чтобы он знал о нашем намерении читать логи из браузера. Делается это так:

@BeforeEach
public void setUp() {
    ChromeOptions options = new ChromeOptions();
    LoggingPreferences logPrefs = new LoggingPreferences();
    logPrefs.enable(LogType.BROWSER, Level.ALL);
    options.setCapability("goog:loggingPrefs", logPrefs);
    driver = new ChromeDriver(options);
}

Вот теперь точно всё. Давайте взглянем на тест:

@Test
public void testLogInBrowserConsole() {
    driver.get("file:///path_to_saved_html_page_from_example");
    Wait<WebDriver> logMessageWaiter = new FluentWait<>(driver)
            .pollingEvery(Duration.ofSeconds(1))
            .withTimeout(Duration.ofSeconds(30));
    logMessageWaiter.until(new ConsoleMessageCondition("K TEST_MSG"));
}

Так как наша демо-страница генерирует сообщения с фактором случайности, тест будет проходить успешно либо падать с некоторой вероятностью.

Если у вас остались вопросы, задавайте их тут. Я постараюсь дополнить статью опираясь на ваши замечания.