Всем привет, меня зовут Денис, я Software Developer Engineer in Test (SDET) в компании Bimeister. Я занимаюсь разработкой софта для тестирования — это фреймворки, автоматизированные тесты, настройка CI Pipeline’ов и многое другое. В статье расскажу, как мы победили исключение Stale Element Reference Exception при разработке нашего фреймворка, используя Selenium WebDriver и C#. Single Page Application — это одностраничное веб-приложение, в котором роутинг осуществляется на стороне клиента. Вместо того, чтобы отправлять запрос к серверу и выкачивать новый HTML-документ при переходе на новый URL, в SPA URL подменяется программно, а контент на странице размещается динамически средствами JavaScript. В процессе работы пользователю может показаться, что он запустил не веб-сайт, а desktop-приложение, так как оно мгновенно реагирует на все его действия без ощутимых задержек. Такого эффекта удается добиться с помощью современных web-фреймворков и библиотек: Angular, React, Vue и других. SPA имеет множество преимуществ для пользователя, но для автотестов — это узкое место, из-за которого возникает одна из самых частых ошибок при использовании Selenium WebDriver — Stale Element Reference Exception. Исключение Stale Element Reference Exception — это runtime-ошибка. Она возникает, когда код теста использует объект и в это время у объекта меняется состояние. Это может быть связано с обновлением страницы или событием, вызванным взаимодействием с пользователем, которое изменяет структуру документа. Измененный объект в памяти тестового приложения остался, ссылка на него действительна, но на странице этот объект уже отсутствует — реактивное приложение сформировало новый элемент DOM, поэтому обращение по имеющейся ссылке приведёт к исключению. WebDriver выбрасывает исключение “устаревшей” ссылки на элемент в одном из двух случаев, первый из которых встречается чаще, чем второй: элемент полностью удален — то есть больше не существует в DOM; элемент больше не связан с DOM — элемент существует по его локатору, но его ссылка устарела. Распространенная причина исключения — удаление элемента JavaScript-библиотекой и замена его другим с тем же ID или атрибутами. Хотя заменяющие элементы могут выглядеть идентично, они разные. WebDriver не определяет, что заменяющие элементы действительно соответствуют ожидаемым. Ошибка появляется именно при выполнении действий с элементом, когда элемент уже найден и мы пытаемся выполнить какую-либо операцию с ним. Например, кликнуть или ввести текст: Рассмотрим абстрактный пример по шагам, чтобы разобрать причину появления исключения. Представим, у нас есть дерево папок и мы хотим раскрыть определённую папку в дереве по её имени. Для этого мы написали бы примерный алгоритм: Сохраняем в переменную список всех элементов, которые будут найдены по тегу для каждой папки в дереве. В списке находим элемент с текстом, который совпадает с именем требуемой папки. Сохраняем этот элемент в переменную и вызываем .Click() по найденному элементу, чтобы раскрыть папку. Допустим, что за это время дерево полностью обновилось из-за особенностей реализации приложения. Если мы снова попытаемся вызвать .Click() по найденному элементу, то столкнёмся с исключением Stale ElementReference Exception. Хотя визуально ничего не изменилось, скрипты обновили структуру документа и элемента с нужной ссылкой уже не существует в DOM. Мы можем избавиться от исключения Stale Element Reference Exception несколькими способами: Самый простой способ — добавить явное ожидание через Thread.Sleep, прежде чем искать элемент. Но такой подход в действительности не решает проблему полностью: ссылка на элемент все равно может устареть прежде, чем выполнится действие; так как исключение может возникнуть в любом месте, придется добавлять явное ожидание перед каждым поиском элемента, что негативно отразится на производительности тестов, не увеличив их стабильность. Универсальный способ — обрабатываем исключение Stale Element Reference Exception в цикле с повторной инициализацией WebElement и с использованием Try Catch-блока. Обычно есть два метода с разной сигнатурой, так как мы можем выполнить действие или непосредственно на закрепленный элемент страницы по его локатору, например, элемент кнопка — или по элементу, найденным в цикле. Простая ситуация — когда мы передаём аргументом локатор. В этом случае пытаемся повторно найти новый элемент через .FindElement() в Try-блоке и проблема с “устаревшей” ссылкой на элемент будет побеждена: Сложная ситуация — когда в качестве аргумента передаётся сам IWebElement, так как нет никакого способа обновить состояние элемента. Правда нет, мы проверили. В этом случае помогает замыкание в передаче callback-функции, у которой будет вызываться клик: Так мы передаём аргументом функцию, которая дополнительно запросит для нас элемент по тегу, если будет поймано исключение Stale Element Reference Exception. Пример использования метода с такой сигнатурой: При таком решении внутри Browser.Click() метод .Click() выполняется по свежему элементу, полученному благодаря вызову callback-функции. Это помогает обработать ошибку, когда ссылка на элемент успевает устареть прежде, чем выполнится действие. При автоматизации тестирования с Selenium WebDriver современных SPA-приложений мы сталкиваемся с рядом проблем, поскольку DOM-элемент часто устаревает прежде, чем к нему обратиться. Например, скрипты могут обновить структуру DOM, из-за чего появится исключения Stale Element Reference Exception. Реализация необходимых методов для обработки такой ошибки поможет избавиться от flaky-тестов и повысить их стабильность, что, в конечном счёте, отразит состояние вашего приложения более достоверно.  Software Developer Engineer in Test (SDET)