О целесообразности Selenium WebDriverWait

Чем ближе я знакомлюсь с Selenium WebDriver, тем больше у меня возникает вопросов, почему тот или иной функционал выполнен так, а не иначе. В своем выступлении «Заморочки в Selenium WebDriver» Алексей Баранцев проливает свет на тонкости реализации этого инструмента автоматизации и разграничивает «баги» и «фичи». В видео вы найдете много интересного, но все же некоторые моменты остаются (по крайней мере для меня) в тени.

В этой статье я хочу обсудить часто используемый инструмент ожидания какого-либо события на странице, реализованный при помощи класса WebDriverWait и его основного метода Until. Я задаюсь вопросом, нужен ли вообще WebDriverWait и можно ли от него отказаться?

Размышления будут преподнесены в контексте С#, хотя я не думаю, что логика реализации этого класса будет хоть сколько отличаться для других языков программирования.

При создании экземпляра WebDriverWait в конструктор передается экземпляр драйвера, который сохраняется во внутреннем поле input. Метод Until предполагает делегат, входным параметром которого должен быть IWebDriver, экземпляром которого и является input.

Давайте посмотрим на исходный код метода Until. Костяк его логики — это бесконечный цикл с двумя условиями выхода из него: наступление нужного события или timeout. Дополнительными «плюшками» являются игнорирование предопределенных exceptions и возврат объекта, если в качестве TResult выступает не bool (об этом чуть позже).

Первый ограничение, которое я вижу — нам всегда требуется именно экземпляр IWebDriver, хотя внутри метода Until (если быть точным, то в качестве входного параметра для condition) мы могли бы вполне обойтись ISearchContext. Ведь в большинстве случаев мы ожидаем какого-то элемента или изменения его свойства и используем FindElement(s) для его поиска.

Я рискну заявить, что использование ISearchContext было бы даже более логичным, ведь клиентский код (класс) — это не только page object, который в поиске дочерних элементов отталкивается от корня страницы. Иногда это класс, описывающий некий составной элемент, у которого корнем является другой элемент страницы, а не сама страница. В качестве такого примера можно привести SelectElement, который принимает в конструктор ссылку на родительский IWebElement.

Вернемся к вопросу инициализации WebDriverWait. Это действие требует экземпляр драйвера. Т.е. нам всегда, так или иначе, в клиентский код необходимо извне пробрасывать экземпляр IWebDriver, даже если это класс некоего составного элемента (пример про SelectElement), который уже принимает «родителя». С моей точки зрения, это излишне.

Конечно, мы можем объявить класс по аналогии
SearchContextWait : DefaultWait<ISearchContext>
Но не будем торопиться. Он нам не понадобится.

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

var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
wait.Until( d => d.FindElements(By.XPath("locator")).Count > 0 );

Возникает вопрос, зачем внутри condition необходима «локальная» версия драйвера, если он всегда доступен из клиентского кода? Более того, это тот же самый экземпляр, переданный ранее через конструктор. Т.е. код может выглядеть как-то так:

var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
wait.Until( d => Driver.FindElements(By.XPath("locator")).Count > 0 );

Даже Simon Stewart в своем выступлении использует такой подход.

image

Он не пишет «d -> d.», а пишет «d -> driver.», т.е. переданный внутрь метода экземпляр драйвера попросту игнорируется. Но передавать его необходимо, ибо этого требует сигнатура метода!

Зачем же передавать драйвер вовнутрь condition метода? Возможно, чтобы изолировать поиск внутри этого метода, как это реализовано в ExpectedConditions? Посмотрите на реализацию метода TextToBePresentInElement. Или VisibilityOfAllElementsLocatedBy. Или TextToBePresentInElementValue. В них переданный драйвер даже не используется!

Итак, первая мысль — нам не нужен метод Until с параметром-делегатом, который принимает драйвер.

Давайте теперь разберемся, нужно ли методу Until возвращаемое значение? Если в качестве TResult выступает bool, то нет, не нужно. Ведь в случае успеха вы получите true, а в случае неудачи вы получите TimeoutException. В чем информативность такого поведения?

А если в качестве TResult выступает object? Предположим, такую конструкцию:

var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
wait.IgnoreExceptionTypes(typeof(NoSuchElementException));
var element = wait.Until(d => d.FindElement(By.XPath("locator")));

Т.е. мы не только ждем появления элемента, а и используем его (если дождались), тем самым убирая одно лишнее обращение к DOM. Хорошо.

Давайте посмотрим пристальнее на эти три строчки кода. Внутри реализации метода Until это сводится к некоему подобию (условный код)

try { FindElement } catch (NoSuchElementException) {}

Т.е. исключение будет генерироватьcя каждый раз, пока элемент не появится в DOM. Поскольку генерация exception довольно дорогостоящее событие, то я бы предпочел его избегать, особенно в тех местах, где это не представляет труда. Мы можем переписать код следующим образом:

var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
var elements = wait.Until(d => d.FindElements(By.XPath("locator")));

Т.е. мы используем FindElements, который не генерирует исключение. Подождите, а будет ли эта конструкция ждать появления элементов? НЕТ! Потому что, если посмотреть в исходный код, выполнение бесконечного цикла завершает сразу, как только condition возвращает не null. А FindElements в случае неудачи возвращает пустую коллекцию, но никак не null. Т.е. для списка элементов использование Until не имеет смысла.

Хорошо, со списком понятно. Но все-таки, как вернуть найденный элемент и не генерировать исключение? Код может выглядеть таким образом:

var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
var element = wait.Until(d => d.FindElements(By.XPath("locator")).FirstOrDefault());

В этом случае на каждой итерации цикла мы будем не просто получать список IWebElement (который может быть пустым), а и пробовать извлекать из него первый элемент. Если элементы все еще не отображены на странице, мы получим null (default значение для object) и перейдем к следующей итерации цикла. Если же элемент найден, мы выйдем из метода и переменная element будет проинициализирована возвращаемым значением.

И все же, вторая мысль — возвращаемое значение метода Until не используется в большинстве случаев.

Передаваемое значение излишне, возвращаемое значение не используется. В чем же полезность Until? Только лишь в цикле и периодичности вызова condition метода? Этот подход уже реализован в C# в методе SpinWait.SpinUntil. Единственное его отличие, что он не генерирует исключение по timeout. Это можно исправить следующим образом:

public void Wait(Func<bool> condition, TimeSpan timeout)
{
	var waited = SpinWait.SpinUntil(condition, timeout);
	if (!waited)
	{
		throw new TimeoutException();
	}
}

Т.е. эти несколько строк кода в большинстве случаев заменяют логику работы целого класса WebDriverWait. Стоят ли усилия результата?

Обновление

В комментариях к статье пользователь KSA сделал дельное замечание по поводу отличия SpinUntil от Until в плане частоты выполнения condition. Для WebDriverWait это значение регулируемо и по умолчанию равно 500 миллисекунд. Т.е. в методе Until присутствует задержка между итерациями цикла. В то время как для SpinUntil логика слегка усложнена и зачастую ожидание не превышает 1 миллисекунды.

На практике это выливается в ситуацию, когда при ожидании элемента, появляющегося в течении 2 секунд, метод Unitl выполняет 4 итерации, а метод SpinUntil — около 200 или более.

Давайте откажемся от SpinUntil и перепишем метод Wait следующим образом.

public void Wait(Func<bool> condition, TimeSpan timeout, int evaluatedInterval = 500)
{
	Stopwatch sw = Stopwatch.StartNew();
	while (sw.Elapsed < timeout)
	{
		if (condition())
		{
			return;
		}
		Thread.Sleep(evaluatedInterval);
	}
	throw new TimeoutException();
}


Мы добавили несколько строк кода, и в тоже время стали ближе к логике метода Until.
Поделиться публикацией

Комментарии 16

    0
    Вас не смущает, что SpinUntil будет вызывать condition практически без интервалов?
      –1
      Дельное замечание. Это единственная проблема этого подхода? :) У меня есть решение, я завтра обновлю статью.
        0
        Спасибо за замечание. Обновил.
        +1
        То, что Simon Stewart использует в своем примере не ту переменную, скорее опечатка, а не правило. Не стоит на это обращать внимание.

        Буду говорить за Java. Вообще вроде как всё логично.
        Класс WebDriverWait расширяет класс FluentWait, который является реализацией интерфейса Wait. В интерфейсе Wait определен один метод — until, принимающий Funtion<? super T, V>, т.е. функцию с входным параметром T и выходным V. И это вроде как сделано не просто так, мы не должны завязываться на внешние переменные(доступные из «клиентского кода»), по этому в функцию передаем всё необходимое для её выполнения. Слабое связывание.

        WebDriverWait в качестве T использует интерфейс WebDriver:
        public class WebDriverWait extends FluentWait<WebDriver>

        добавляет в igoring NotFoundException.class, т.е. игнорирует практически все исключения, которые могу быть вызваны при работе с WebDriver, а также переопределяет метод timeoutException, добавляя в сообщение диагностическую информацию по текущей сессии WebDriver'а.
        Что из этого следует? А то, что в случае если WebDriver не нужен в контексте ожидания, используйте FluentWait с необходимым входным параметром.
          +1
          И все же, вторая мысль — возвращаемое значение метода Until не используется в большинстве случаев.

          Так и не понял откуда такой вывод. Если возвращает bool значение, то не нужно — тут ок. Если элемент, то нужно, чтобы еще раз не опрашивать драйвер. По этой логике в большинстве случаев мы хотим получать bool, но это же не так. Для поиска каждого элемента нужно ожидание. И в большинстве случаев мы все же будем использовать возвращаемое значение. Или я что-то не уловил?
            0
            Кстати да, забыл про это написать. Полностью поддерживаю!
              –1
              Перефразируя свою мысль, я бы сказал, что возвращаемое значение только ради FindElement и существует. В случае bool и FindElements оно не несет смысловой нагрузки.
                0
                Вы совсем не правы. В классе ExpectedConditions есть еще как минимум один метод, возвращающий не Boolean и не WebElement, это метод alertIsPresent. Еще есть метод numberOfElementsToBeMoreThan, который прекрасно справляется со своей задачей и возвращает List элементов, есть visibilityOfAllElementsLocatedBy/visibilityOfAllElements, которые также нормально возвращают List элементов с проверкой на пустоту списка.

                Возвращаемое значение не всегда требуется, но есть больше количество кастомных conditions с возвращаемыми объектами различных классов.
                  0
                  Я отнюдь не претендую на избавление ExpectedConditions или изменение WebDriverWait. Я говорю о том, что в один момент я понял, что «тяну» за собой экземпляр драйвера только лишь потому, что он мне нужен в WebDriverWait. И вместо этого я попробовал найти альтернативу.

                  Я скажу более, экземпляр драйвера вообще не нужен внутри Page Object (как тебе такое, Илон Маск? (с)). Вполне можно обойтись ISearchContext. Но в реализации этой идеи есть два камня преткновения: WebDriverWait и Actions. И если с первым решение есть, то со вторым надо еще побороться.
                    0
                    Не понимаю, какая разница SearchContext или WebDriver? Ну усекли вы возможности до 2 методов, а WebDriver то никуда не делся.
                    Всё взаимодействие с браузером осуществляется через него.
                    Когда вы ищите что-то из контекста WebElement'а, вы всё равно возвращаетесь к WebDriver'у.
                      0
                      IWebDriver остается на уровне класса базового теста. Он может передаваться в условные Navigation (используется .Navigate().GoToUrl()), в какие-то другие helpers (скажем, JavaScript executor), но в page object драйвер приходит в качестве ISearchContext. Это позволяет мне иметь один базовый класс SearchContextWrapper и для page object (всей страницы, «родитель» — корень DOM, IWebDriver) и для element object (логической части страницы, «родитель» — один из элементов страницы, IWebElement). И IWebElement, и IWebDriver реализуют ISearchContext. И на уровне SearchContextWrapper я не разделяю кем именно он является, ибо это уже становится не важным.

                      А если передавать IWebDriver, то приходится делить на что-то типа PageObject(IWebDriver parent) и PageElement(IWebElement paren, IWebDriver driverWillUseForWait). Можно, конечно, упороться и сделать PageElement(By paren, IWebDriver driverWillUseForSearchAndWait), но зачем, если есть ISearchContext.

                      Как-то так. А вообще это тема отдельной статьи :)
                        0
                        Это всё понятно, но почему бы не сделать свою реализацию, принимающую SearchContext, а не сомневаться в целесообразности в WebDriverWait?
                          0
                          Потому что есть случаи, когда он нецелесообразен :) Именно эту мысль я пытался донести.
                      0
                      Зачем вообще оставлять привязку к селениуму в задаче «подождать чего-то»?

                      Одинакого плохо оставлять и IWebDriver и ISearchContext.

                      Не всегда Wait будет привязан к поиску чего-то.
                        0

                        Так для этого и существует FluentWait. Это просто готовое решение одной проблемы, не нравится, пишите свое. Честно, я вообще не понял суть статьи, проблема высосана из пальца

                0
                Весь пакет Support является опциональным и просто взглядом разработчиков на то какие дополнительные вещи можно делать. ExpectedConditions в C# вообще использовать не стоит и аргументировать ими тоже — они deprecated и были просто пережитком того что в старых версиях Java не было лямбд.

                Как по мне — дейтсивтельно лишним является только передача самого объекта IWebDriver внутрь ожидания, особенно если вы пишите какие-то абстрации вокруг драйвера.

                Для себя сделал аналогичную реализацию, с немного другими фичами — github.com/MrHant/tiver-fowl.Waiting
                Там нет протянутого IWebDriver, добавляется конфигурация, и прозрачное логирование. Так же вам может быть интересно возможность делать «Extended Wait» где проверка сразу не будет отваливаться по таймауту, а подождёт подольше и потом отметит тест как Warning (только для NUnit).

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое