1. 概述
在使用Selenium测试Web应用时,我们经常会遇到StaleElementReferenceException
这个常见错误。当元素变得过期(stale),即页面刷新或DOM更新后,Selenium会抛出此异常。
在这个教程中,我们将了解StaleElementReferenceException
在Selenium(Java Selenium与JUnit和TestNG)中的含义以及它为何会出现。然后,我们将探讨如何在我们的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上找到:https://github.com/eugenp/tutorials/tree/master/testing-modules/selenium。