Selenium 中的 StaleElementReferenceException
一、概述
StaleElementReferenceException
是我們在使用 Selenium 測試 Web 應用程序時遇到的常見錯誤。當我們引用過時的元素時,Selenium 會拋出StaleElementReferenceException
。由於頁面刷新或 DOM 更新,元素變得陳舊。
在本教程中,我們將了解 Selenium 中的StaleElementReferenceException
是什麼以及它發生的原因。然後,我們將看看如何在我們的 Selenium 測試中避免異常。
2. 避免StaleElementReferenceException
策略
為避免StaleElementReferenceException
,必須確保元素被動態定位並與之交互,而不是存儲對它們的引用。這意味著我們應該在每次需要時找到元素,而不是將它們保存在變量中。
在某些情況下,這種方法是行不通的,我們需要在再次與元素交互之前刷新元素。因此,我們的解決方案是捕獲StaleElementReferenceException
並在刷新元素後執行重試。我們可以直接在我們的測試中執行此操作,也可以在所有測試中全局執行此操作。
對於我們的測試,我們將為我們的定位器定義幾個常量:
By LOCATOR_REFRESH = By.xpath("//a[.='click here']");
By LOCATOR_DYNAMIC_CONTENT = By.xpath("(//div[@id='content']//div[@class='large-10 columns'])[1]");
對於設置,我們使用使用 WebDriverManager 的自動化方法。
2.1.生成StaleElementReferenceException
首先,我們將看一下以StaleElementReferenceException
結束的測試:
void givenDynamicPage_whenRefreshingAndAccessingSavedElement_thenSERE() {
driver.navigate().to("https://the-internet.herokuapp.com/dynamic_content?with_content=static");
final WebElement element = driver.findElement(LOCATOR_DYNAMIC_CONTENT);
driver.findElement(LOCATOR_REFRESH).click();
Assertions.assertThrows(StaleElementReferenceException.class, element::getText);
}
該測試通過單擊該頁面上的鏈接來存儲元素並更新 DOM。當重新訪問不再存在的元素時,將拋出StaleElementReferenceException
。
2.2.刷新元素
讓我們使用在重新訪問之前刷新元素的重試邏輯:
boolean retryingFindClick(By locator) {
boolean result = false;
int attempts = 0;
while (attempts < 5) {
try {
driver.findElement(locator).click();
result = true;
break;
} catch (StaleElementReferenceException ex) {
System.out.println(ex.getMessage());
}
attempts++;
}
return result;
}
每當發生StaleElementReferenceException
時,我們將使用存儲的元素定位器在再次執行單擊之前再次定位該元素。
現在,讓我們更新測試以使用新的重試邏輯:
void givenDynamicPage_whenRefreshingAndAccessingSavedElement_thenHandleSERE() {
driver.navigate().to("https://the-internet.herokuapp.com/dynamic_content?with_content=static");
final WebElement element = driver.findElement(LOCATOR_DYNAMIC_CONTENT);
if (!retryingFindClick(LOCATOR_REFRESH)) {
Assertions.fail("Element is still stale after 5 attempts");
}
Assertions.assertDoesNotThrow(() -> retryingFindGetText(LOCATOR_DYNAMIC_CONTENT));
}
我們看到我們需要更新測試,如果我們需要對許多測試執行此操作,這會很麻煩。幸運的是,我們可以在中央位置使用此邏輯,而無需更新所有測試。
3. 避免StaleElementReferenceException
通用策略
我們將為通用解決方案創建兩個新類: RobustWebDriver
和RobustWebElement
。
3.1. RobustWebDriver
首先,我們需要創建一個實現WebDriver
實例的新類。我們將把它寫成WebDriver
的包裝器。它調用WebDriver
方法,方法findElement
和findElements
將返回RobustWebElement
:
class RobustWebDriver implements WebDriver {
WebDriver originalWebDriver;
RobustWebDriver(WebDriver webDriver) {
this.originalWebDriver = webDriver;
}
...
@Override
public List<WebElement> findElements(By by) {
return originalWebDriver.findElements(by)
.stream().map(e -> new RobustWebElement(e, by, this))
.collect(Collectors.toList());
}
@Override
public WebElement findElement(By by) {
return new RobustWebElement(originalWebDriver.findElement(by), by, this);
}
...
}
3.2. RobustWebElement
RobustWebElement
是WebElement
的包裝器。該類實現WebElement
接口並包含重試邏輯:
class RobustWebElement implements WebElement {
WebElement originalElement;
RobustWebDriver driver;
By by;
int MAX_RETRIES = 10;
String SERE = "Element is no longer attached to the DOM";
RobustWebElement(WebElement element, By by, RobustWebDriver driver) {
originalElement = element;
by = by;
driver = driver;
}
...
}
我們必須實現WebElement
接口的每個方法,以便在拋出StaleElementReferenceException
時執行元素的刷新。為此,讓我們介紹一些包含刷新邏輯的輔助方法。我們將從這些重寫的方法中調用它們。
我們可以利用功能接口並創建一個幫助類來調用WebElement
的各種方法:
class WebElementUtils {
private WebElementUtils(){
}
static void callMethod(WebElement element, Consumer<WebElement> method) {
method.accept(element);
}
static <U> void callMethod(WebElement element, BiConsumer<WebElement, U> method, U parameter) {
method.accept(element, parameter);
}
static <T> T callMethodWithReturn(WebElement element, Function<WebElement, T> method) {
return method.apply(element);
}
static <T, U> T callMethodWithReturn(WebElement element, BiFunction<WebElement, U, T> method, U parameter) {
return method.apply(element, parameter);
}
}
在WebElement
中,我們實現了四個包含刷新邏輯的方法,並調用了之前介紹的WebElementUtils
:
void executeMethodWithRetries(Consumer<WebElement> method) { ... }
<T> T executeMethodWithRetries(Function<WebElement, T> method) { ... }
<U> void executeMethodWithRetriesVoid(BiConsumer<WebElement, U> method, U parameter) { ... }
<T, U> T executeMethodWithRetries(BiFunction<WebElement, U, T> method, U parameter) { ... }
click
方法將如下所示:
@Override
public void click() {
executeMethodWithRetries(WebElement::click);
}
我們現在需要為我們的測試更改的是WebDriver
實例:
driver = new RobustWebDriver(new ChromeDriver(options));
其他一切都可以保持不變, StaleElementReferenceException
不應再發生。
4。結論
在本教程中,我們了解到在 DOM 已更改且元素尚未刷新後訪問元素時可能會發生StaleElementReferenceException
。我們在測試中引入了重試邏輯,以便在發生StaleElementReferenceException
時刷新元素。
與往常一樣,所有這些示例的實現都可以在 GitHub 上找到。