Handling exceptions in Selenium WebDriver
During many years of working with Selenium, I have noticed that sometimes even experienced testers do not understand how to handle exceptions thrown by Selenium. Handling them on wrong way can cause with flaky tests. In this article I will focus on most popular exceptions and explain why they are throwing and how to fix them.
The most popular exception is StaleElementReferenceException. It occurs when we want to perform any method (e.g. getText()) on WebElement which was extracted (found) before but the element has been changed in the dom three in the meantime. I have seen many times that testers were adding waits before the row in which this exception was thrown. Unfortunately usually it is not solving the problem. I will explain why, on the timer which is visible on the page https://testcraftsmanship.com/selenium-playground/. Most of us is used to page object pattern and probably would create page with element defined as below with method to extracting the value of the timer.
@FindBy(id = "page-heading-timer") private WebElement pageHeadingTimer; public String headingTimerValue() { return pageHeadingTimer.getText(); }
When we will use it in our tests, it will be working as expected for most of the time, but from time to time it will throw
exception. Thing is that before executing pageHeadingTimer.getText() we have to lazy initialize pageHeadingTimer and time between
initialization and executing method is very short. You should imagine the situation in which we initialize the pageHeadingTimer
just in last millisecond and create the object for e.g. 10:21:00. When we execute the getText() method then we want to
get text from element which just changed in dom to 10:21:01. Previous element is not available anymore, so we are getting
StaleElementReferenceException.
The situation will be the same, if we won't be using page object pattern and to extracting timer value we will use the code listed below:
WebElement element = driver.findElement(By.id("page-heading-timer")); String timerValue = element.getText();
Here we initialize element with use of findElement method. The exception will be thrown when getText is executed on element
which is not available anymore but was available while executing method findElement. As in example above here exception will throw
very rare because timer has to change between executing findElement and getText methods. I guess that right now it should be
clear that adding waits before finding or getting text will not solve the problem.
To solve the problem we just need to handle this rare situation which is causing it. One way to handle this situation is
shown below:
public String headingTimerValue() { try { return element.getText(); } catch (StaleElementReferenceException e) { return element.getText(); } }
I know that it does not look nice, but I think that we can accept it for this very element that we know that can be reloaded in
dom from time to time. Important thing is that we should know why we are using this structure and not to apply it for every WebElement
we have in our framework.
Next exception called ElementNotInteractableException should be less problematic. We can imagine that it can be thrown when element we want to perform any operation on is not interactable. I can explain it based on the example from the page https://testcraftsmanship.com/selenium-playground/. On the dom three of the page we can find element with id close-btn. Unfortunately, the element is not visible and accessible on the web page. When we will click on [License conditions] button then this button (with id close-btn) will become visible and accessible - of course I will not happen just after clicking on [License conditions] button. We need to wait for content to be clickable. This is the listing before the fix:
@FindBy(id = "open-modal-window-btn") private WebElement openModalWindowButton; @FindBy(id = "close-btn") private WebElement closeButton; public PlaygroundPage(WebDriver driver) { super(driver); } public PlaygroundPage clickLicenseConditionsButton() { openModalWindowButton.click(); return this; } public PlaygroundPage clickCloseButton() { closeButton.click(); return this; }
In this example solution is super simple, and I guess that should be clear for everyone. Thing we need to add is the wait
for the element to be clickable. Updated methods which work correctly:
public PlaygroundPage clickLicenseConditionsButton() { openModalWindowButton.click(); return this; } public PlaygroundPage clickCloseButton() { new WebDriverWait(driver, Duration.ofSeconds(TIMEOUT_IN_SECONDS)) .until(ExpectedConditions.elementToBeClickable(closeButton)); closeButton.click(); return this; }
Reason of throwing NoSuchElementException exception is different from previous one but solution is quite similar. In this
very case element on which we want to perform any operation is not available in the dom three. Let's navigate once again to
the page https://testcraftsmanship.com/selenium-playground/. What we want to do is just click on the 'Actions' menu, then
on 'Append banner' submenu and after that check that the banner is visible. Problem in this scenario is that banner is
displayed with a small delay and before that it is not available in the doom three (you are not able to find element with
id 'success-alert'). Because there is no banner on the page (in the small amount of time before it will be displayed) and
we want to get its value we are getting NoSuchElementException. This is the listing before the fix:
@FindBy(id = "navbar-item-actions") private WebElement actionsMenu; @FindBy(id = "navbar-item-action-wait-for-alert") private WebElement showAlertMenuItem; @FindBy(id = "success-alert") private WebElement successAlert; public SeleniumPlaygroundHomePage clickShowAlertButton() { actionsMenu.click(); showAlertMenuItem.click(); return this; } public String getAlertText() { return successAlert.getText(); }
Solution in this case is similar to the previous one. We just need to add the wait
for the element to be visible. Updated methods which work correctly:
public SeleniumPlaygroundHomePage clickShowAlertButton() { actionsMenu.click(); showAlertMenuItem.click(); return this; } public String getAlertText() { return new WebDriverWait(DriverFactory.getDriver(),TIMEOUT_IN_SECONDS) .until(ExpectedConditions.visibilityOf(successAlert)) .getText(); }
Hope, it helps you to fix flaky tests.