How to handle exceptions and synchronization methods with Selenium WebDriver API

0
4625
10 min read

One of the areas often misunderstood, but is important in framework design is exception handling. Users must program into their tests and methods on how to handle exceptions that might occur in tests, including those that are thrown by applications themselves, and those that occur using the Selenium WebDriver API. In this article, we will see how to do that effectively.

Let us look at different kinds of exceptions that users must account for:

  • Implicit exceptions: Implicit exceptions are internal exceptions raised by the API method when a certain condition is not met, such as an illegal index of an array, null pointer, file not found, or something unexpected occurring at runtime.
  • Explicit exceptions: Explicit exceptions are thrown by the user to transfer control out of the current method, and to another event handler when certain conditions are not met, such as an object is not found on the page, a test verification fails, or something expected as a known state is not met. In other words, the user is predicting that something will occur, and explicitly throws an exception if it does not.
  • WebDriver exceptions: The Selenium WebDriver API has its own set of exceptions that can implicitly occur when elements are not found, elements are not visible, elements are not enabled or clickable, and so on. They are thrown by the WebDriver API method, but users can catch those exceptions and explicitly handle them in a predictable way.
  • Try…catch blocks: In Java, exception handling can be completely controlled using a try…catch block of statements to transfer control to another method, so that the exit out of the current routine doesn’t transfer control to the call handler up the chain, but rather, is handled in a predictable way before the exception is thrown.

Let us examine the different ways of handling exceptions during automated testing.

Implicit exception handling

A simple example of Selenium WebDriver implicit exception handling can be described as follows:


  1. Define an element on a page
  2. Create a method to retrieve the text from the element on the page
  3. In the signature of the method, add throws Exception
  4. Do not handle a specific exception like ElementNotFoundException:
// create a method to retrieve the text from an element on a page

@FindBy(id="submit")

protected M submit;

public String getText(WebElement element) throws Exception {

return element.getText();

}

// use the method

LoginPO.getText(submit);

Now, when using an assertion method, TestNG will implicitly throw an exception if the condition is not met:

  1. Define an element on a page
  2. Create a method to verify the text of the element on a page
  3. Cast the expected and actual text to the TestNG’s assertEquals method
  4. TestNG will throw an AssertionError
  5. TestNG engages the difference viewer to compare the result if it fails:
// create a method to verify the text from an element on a page
@FindBy(id="submit")
protected M submit;
public void verifyText(WebElement element,
String expText)
throws AssertionError {
assertEquals(element.getText(),
expText,
"Verify Submit Button Text");
}
// use the method
LoginPO.verifyText(submit, "Sign Inx");
// throws AssertionError
java.lang.AssertionError: Verify Text Label expected [ Sign Inx]
but found [ Sign In]
Expected : Sign Inx
Actual : Sign In
<Click to see difference>

TestNG difference viewer

When using the TestNG’s assertEquals methods, a difference viewer will be engaged if the comparison fails. There will be a link in the stacktrace in the console to open it. Since it is an overloaded method, it can take a number of data types, such as String, Integer, Boolean, Arrays, Objects, and so on. The following screenshot displays the TestNG difference viewer:

TestNG difference viewer:

Explicit exception handling

In cases where the user can predict when an error might occur in the application, they can check for that error and explicitly raise an exception if it is found. Take the login function of a browser or mobile application as an example. If the user credentials are incorrect, the app will throw an exception saying something like “username invalid, try again” or “password incorrect, please re-enter”.

The exception can be explicitly handled in a way that the actual error message can be thrown in the exception. Here is an example of the login method we wrote earlier with exception handling added to it:

@FindBy(id="myApp_exception")
protected M error;
/**
* login - method to login to app with error handling
*
* @param username
* @param password
* @throws Exception
*/
public void login(String username,
     String password)

throws Exception {
if ( !this.username.getAttribute("value").equals("") ) {
this.username.clear();
}
this.username.sendKeys(username);
if ( !this.password.getAttribute( "value" ).equals( "" ) ) {
this.password.clear();
}
this.password.sendKeys(password);
submit.click();
// exception handling
if ( BrowserUtils.elementExists(error, Global_VARS.TIMEOUT_SECOND) ) {
String getError = error.getText();
throw new Exception("Login Failed with error = " + getError);
}
}

Try…catch exception handling

Now, sometimes the user will want to trap an exception instead of throwing it, and perform some other action such as retry, reload page, cleanup dialogs, and so on. In cases like that, the user can use try…catch in Java to trap the exception. The action would be included in the try clause, and the user can decide what to do in the catch condition.

Here is a simple example that uses the ExpectedConditions method to look for an element on a page, and only return true or false if it is found. No exception will be raised: 

/**
* elementExists - wrapper around the WebDriverWait method to
* return true or false
*
* @param element
* @param timer
* @throws Exception
*/
public static boolean elementExists(WebElement element, int timer) {
try {
WebDriver driver = CreateDriver.getInstance().getCurrentDriver();
WebDriverWait exists = new WebDriverWait(driver, timer);
exists.until(ExpectedConditions.refreshed(
ExpectedConditions.visibilityOf(element)));
return true;
}
catch (StaleElementReferenceException |
TimeoutException |
NoSuchElementException e) {
return false;
}
}

In cases where the element is not found on the page, the Selenium WebDriver will return a specific exception such as ElementNotFoundException. If the element is not visible on the page, it will return ElementNotVisibleException, and so on. Users can catch those specific exceptions in a try…catch…finally block, and do something specific for each type (reload page, re-cache element, and so on):

try {
....
}
catch(ElementNotFoundException e) {
// do something
}
catch(ElementNotVisibleException f) {
// do something else
}
finally {
// cleanup
}

Synchronizing methods

Earlier, the login method was introduced, and in that method, we will now call one of the synchronization methods waitFor(title, timer) that we created in the utility classes. This method will wait for the login page to appear with the title element as defined. So, in essence, after the URL is loaded, the login method is called, and it synchronizes against a predefined page title. If the waitFor method doesn’t find it, it will throw an exception, and the login will not be attempted.

It’s important to predict and synchronize the page object methods so that they do not get out of “sync” with the application and continue executing when a state has not been reached during the test. This becomes a tedious process during the development of the page object methods, but pays big dividends in the long run when making those methods “robust”. Also, users do not have to synchronize before accessing each element. Usually, you would synchronize against the last control rendered on a page when navigating between them.

In the same login method, it’s not enough to just check and wait for the login page title to appear before logging in; users must also wait for the next page to render, that being the home page of the application. So, finally, in the login method we just built, another waitFor will be added:

public void login(String username,
String password) throws Exception {
 
BrowserUtils.waitFor(getPageTitle(),
getElementWait());
 
if ( !this.username.getAttribute("value").equals("") ) { this.username.clear();
}
 
this.username.sendKeys(username);
 
if ( !this.password.getAttribute( "value" ).equals( "" ) ) { this.password.clear();
}
 
this.password.sendKeys(password); submit.click();
// exception handling
if ( BrowserUtils.elementExists(error,
Global_VARS.TIMEOUT_SECOND) ) {

 
String getError = error.getText();
throw new Exception("Login Failed with error = " + getError);
}
 
// wait for the home page to appear
BrowserUtils.waitFor(new MyAppHomePO<WebElement>().getPageTitle(),
getElementWait());
}

Table classes

When building the page object classes, there will frequently be components on a page that are common to multiple pages, but not all pages, and rather than including the similar locators and methods in each class, users can build a common class for just that portion of the page. HTML tables are a typical example of a common component that can be classed.

So, what users can do is create a generic class for the common table rows and columns, extend the subclasses that have a table with this new class, and pass in the dynamic ID or locator to the constructor when extending the subclass with that table class.

Let’s take a look at how this is done:

  1. Create a new page object class for the table component in the application, but do not derive it from the base class in the framework
  2. In the constructor of the new class, add a parameter of the type WebElement, requiring users to pass in the static element defined in each subclass for that specific table
  3. Create generic methods to get the row count, column count, row data, and cell data for the table
  4. In each subclass that inherits these methods, implement them for each page, varying the starting row number and/or column header rows if <th> is used rather than <tr>
  5. When the methods are called on each table, it will identify them using the WebElement passed into the constructor:
/**
*  WebTable Page Object Class
*
*  @author Name
*/
public class WebTablePO { private WebElement table;

 
/** constructor
*
*  @param table
*  @throws Exception
*/
public WebTablePO(WebElement table) throws Exception { setTable(table);
}
 
/**
*  setTable - method to set the table on the page
*
*  @param table
*  @throws Exception
*/
public void setTable(WebElement table) throws Exception { this.table = table;
}
 
/**
*  getTable - method to get the table on the page
*
*  @return WebElement
*  @throws Exception
*/
public WebElement getTable() throws Exception { return this.table;
}
 
....

Now, the structure of the class is simple so far, so let’s add in some common “generic” methods that can be inherited and extended by each subclass that extends the class:

// Note: JavaDoc will be eliminated in these examples for simplicity sake
 
public int getRowCount() {
List<WebElement> tableRows = table.findElements(By.tagName("tr"));
 	
return tableRows.size();
}
 
public int getColumnCount() {
List<WebElement> tableRows = table.findElements(By.tagName("tr")); WebElement headerRow = tableRows.get(1);
List<WebElement> tableCols = headerRow.findElements(By.tagName("td")); 
return tableCols.size();

}
 
public int getColumnCount(int index) {
List<WebElement> tableRows = table.findElements(By.tagName("tr")); WebElement headerRow = tableRows.get(index);
List<WebElement> tableCols = headerRow.findElements(By.tagName("td"));
 
return tableCols.size();
}
 
public String getRowData(int rowIndex) {
List<WebElement> tableRows = table.findElements(By.tagName("tr")); WebElement currentRow = tableRows.get(rowIndex);
 
return currentRow.getText();
}
 
public String getCellData(int rowIndex, int colIndex) { List<WebElement> tableRows = table.findElements(By.tagName("tr")); WebElement currentRow = tableRows.get(rowIndex);
List<WebElement> tableCols = currentRow.findElements(By.tagName("td")); WebElement cell = tableCols.get(colIndex - 1);
 
return cell.getText();
}

Finally, let’s extend a subclass with the new WebTablePO class, and implement some of the methods:

/**
*  Homepage Page Object Class
*
*  @author Name
*/
public class MyHomepagePO<M extends WebElement> extends WebTablePO<M> {
 
public MyHomepagePO(M table) throws Exception { super(table);
}
 
@FindBy(id = "my_table") protected M myTable;
 
// table methods
public int getTableRowCount() throws Exception { WebTablePO table = new WebTablePO(getTable()); 
return table.getRowCount();

}
 
public int getTableColumnCount() throws Exception { WebTablePO table = new WebTablePO(getTable()); return table.getColumnCount();
}
 
public int getTableColumnCount(int index) throws Exception {
WebTablePO table = new WebTablePO(getTable()); return table.getColumnCount(index);
}
 
public String getTableCellData(int row, int column) throws Exception {
WebTablePO table = new WebTablePO(getTable()); return table.getCellData(row, column);
}
 
public String getTableRowData(int row) throws Exception {
WebTablePO table = new WebTablePO(getTable()); return table.getRowData(row).replace("\n", " ");
}
 
public void verifyTableRowData(String expRowText) { String actRowText = "";
int totalNumRows = getTableRowCount();
 
// parse each row until row data found
for ( int i = 0; i < totalNumRows; i++ ) {
if ( this.getTableRowData(i).contains(expRowText) ) { actRowText = this.getTableRowData(i);
break;
}
}
 
// verify the row data try {
assertEquals(actRowText, expRowText, "Verify Row Data");
}
 
catch (AssertionError e) {
String error = "Row data '" + expRowText + "' Not found!"; throw new Exception(error);
}
}
}

We saw, how fairly effective it is to handle object class methods, especially when it comes to handling synchronization and exceptions.

You read an excerpt from the book Selenium Framework Design in Data-Driven Testing by Carl Cocchiaro. The book will show you how to design your own automation testing framework without any hassle.

Selenium Framework Design in Data driven Testing

LEAVE A REPLY

Please enter your comment!
Please enter your name here