Do not store element locators in property files

You have a small test automation project with about 50 test scripts and 10 page object classes.

Each page object class uses approximately 10 web elements.

The project has around 100 web element locators.

Where should the elements’ locators be stored?

Should they be stored in the page object classes, in enumerations, classes with static members or property files?

The last option, property files, seems the best since every time more locators are needed, you just change a text file and not the project’s code.

So for each page object class, you create a property text file and save all element locators in it.

The page classes become smaller this way.

What can be wrong in this case?

The project grew from the initial size to

  • 1000 test scripts
  • 100 page object classes
  • 3000 element locators

Lets say that we are working on a class such as the next one:

public class HomePage {

  WebDriver driver;
  Properties properties;

  public HomePage(WebDriver driver) {
    this.driver = driver;
    this.properties = new Properties();
    InputStream input = new FileInputStream("home.properties");
    this.properties.load(input);
  }

  public void search(String keyword) {

   By searchBoxXpath = By.xpath(properties.get("searchTextBox"));
   WebElement searchTextBox =  driver.findElement(searchBoxXpath);

   searchTextBox.clear();
   searchTextBox.sendKeys(keyword);

   String searchButtonXpath = By.xpath(properties.get("searchButton"))

   WebElement searchButton = driver.findElement(searchButtonXpath);

   searchButton.click();

  }
}

The constructor loads the content of the home.properties file in the properties variable.

The search() method uses the name of the element which is then searched in the properties file.

The home.properties file looks as follows:

searchTextBox = //input[@elementid = 'abc']
searchButton = //input[@elementid = 'def']

Now, we need to change the locators for both elements used in the HomePage class.

How do we do this?

  1. Copy the element name (“searchTextBox”) from the HomePage class
  2. Locate the home.properties file in the project
  3. Open the home.properties file
  4. Search for the element name (“searchTextBox”)
  5. Modify the xpath expression
  6. Save and close the property file

6 steps and a few minutes for changing the value of a locator!
Remember that the project has 3000 locators at this moment.

1st problem of using property files for locators:
it is very time consuming to change the value of a locator.

 

Lets look at the locators situation from a different perspective.

Lots of code has been added, changed and removed from the project in the last year.

How do you know which locators are no longer needed?

With the locators in property files, this is impossible to see immediately.

2nd problem of using property files for locators:
it is not possible to see which locators are no longer used by the page classes.

 

Finally, we are writing test automation code using a language such as Java or C#.

The code that implements the user interactions with each page is organized in classes.

A class should group data and methods together.

By having the locators in the property files, this rule is broken.

3rd problem of using property files for locators:
classes are used incorrectly as they have methods that do not work on class members.

What is the correct approach for saving locators?

Save the locators in the page classes together with the methods.

Doing this allows

  • easy change of a locator in a minimum of steps
  • easy identification of locators not used
  • page classes include locators which are used by class methods
Advertisements

How to work with cookies

Browsers use cookies to store user preferences such as

  • user location
  • number of results per page
  • sort order
  • language

Next time the user visits the site, the user preferences saved in the cookies are set automatically for the site.

Each cookie is defined by the following fields:

  • name
  • value
  • domain
  • path
  • expires
  • size
  • http
  • isSecure
  • sameSite

To take an example, for the Vancouver Public Library – Results Page page, the selected language is saved in the language cookie with the following fields:

  • name = language
  • value = en-CA
  • domain = vpl.bibliocommons.com
  • path = /
  • expires = the expiration date set by the site
  • size = 13
  • http =
  • isSecure = true
  • sameSite =

If the language is changed from english to french, the value of the cookie changes to fr-CA.

It is important to be able to work with cookies in Selenium scripts for scenarios such as

  1. user changes the language and verifies that the change is done correctly
  2. user changes the sort order and verifies that the change is done correctly
  3. user changes the results per page and verifies that the change is done correctly

The cookies support is provided by the WebDriver.Options interface that has the following methods:

void addCookie(Cookie cookie)
Add a specific cookie.

void deleteAllCookies()
Delete all the cookies for the current domain.

void deleteCookie(Cookie cookie)
Delete a cookie from the browser’s “cookie jar”.

void deleteCookieNamed(java.lang.String name)
Delete the named cookie from the current domain.

Cookie getCookieNamed(java.lang.String name)
Get a cookie with a given name.

java.util.Set getCookies()
Get all cookies for the current domain.

An example will clarify how to use these methods.

The following code sample does the following

  1. open a web page: https://vpl.bibliocommons.com/search?q=java&t=keyword
  2. verify that the default language is english (en-CA) by checking the value of the language cookie
  3. verify that the page title is correct for english
  4. change the language to french
  5. verify that the language is french (fr-CA) by checking the value of the language cookie
  6. verify that the page title is correct for french

 

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.Date;

public class Class1 {

WebDriver driver;
WebDriverWait wait;

@Before
public void setUp() {
  System.setProperty("webdriver.chrome.driver", 
  "c:/selenium/selenium3/chromedriver.exe");
  
  driver = new ChromeDriver();
  
  wait = new WebDriverWait(driver, 30);
}

@After
public void tearDown() {
  driver.quit();
}

@Test
public void testChangeLanguage() throws InterruptedException {
  openSite();
  assertEquals(currentLanguage(), "en-CA");
  assertTrue(isTitleCorrect()));

  selectLanguage("fr-CA");

  goToPage(2);
  assertEquals(currentLanguage(), "fr-CA");
  assertTrue(isTitleCorrect());
}

private void openSite() {
  driver.get("https://vpl.bibliocommons.com/search?q=java&t=keyword");
}

private void goToPage(int pageNumber) {
  By pageLocator = By.xpath("//a[@testid='link_page" + 
                            pageNumber + "']");
  wait.until(ExpectedConditions
    .elementToBeClickable(pageLocator))
    .click();
}

private void selectLanguage(String language) {
  By changeLanguageMenuLocator = By.id("biblio_language_trigger");
  By languageLocator = By.xpath("//a[contains(@href,'" + 
                                language + 
                                "')]");
  wait.until(ExpectedConditions
     .elementToBeClickable(changeLanguageMenuLocator))
     .click();
  
  wait.until(ExpectedConditions
     .visibilityOfElementLocated(languageLocator));
  
  wait.until(ExpectedConditions
     .elementToBeClickable(languageLocator))
     .click();
}

private boolean isTitleCorrect() {
  if (currentLanguage().equalsIgnoreCase("en-CA") )
   return wait.until(ExpectedConditions
     .titleIs("Search | Vancouver Public Library | BiblioCommons"));
  else
   return wait.until(ExpectedConditions
     .titleIs("Rechercher | Vancouver Public Library | BiblioCommons"));
}

private String currentLanguage() {
  return getCookieValue("language");
}

private String getCookieValue(String cookieName) {
  return driver.manage()
               .getCookieNamed(cookieName)
               .getValue();
}

}

The code should be very easy to understand.

It interacts with cookies in 1 place only to get the value of a cookie (getCookieValue() method.

Is this all we can do with cookies?

In addition to getting the value of a cookie, we can also delete a cookie or even create a new one.

The previous script is very slow as it has to change the language manually.

We could change the language value by modifying the cookie value instead of clicking the language element.

The benefit of doing this is that the script will be faster than before as it has to go through less pages.

See below the changed code.

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.Date;

public class Class1 {

WebDriver driver;
WebDriverWait wait;

@Before
public void setUp() {
  System.setProperty("webdriver.chrome.driver", 
                     "c:/selenium/selenium3/chromedriver.exe");
  
  driver = new ChromeDriver();
  
  wait = new WebDriverWait(driver, 30);
}

@After
public void tearDown() {
  driver.quit();
}

@Test
public void testChangeLanguageByAddingCookie() throws InterruptedException {
  openSite();
  assertEquals(getCookieValue("language"), "en-CA");
  assertTrue(isTitleCorrect());

  addCookie("language", "fr-CA");

  goToPage(2);
  assertEquals(getCookieValue("language"), "fr-CA");
  assertTrue(isTitleCorrect());
}

private void openSite() {
  driver.get("https://vpl.bibliocommons.com/search?q=java&t=keyword");
}

private void goToPage(int pageNumber) {
  By pageLocator = By.xpath("//a[@testid='link_page" + 
                            pageNumber + "']");
  
  wait.until(ExpectedConditions
    .elementToBeClickable(pageLocator))
    .click();
}

private void selectLanguage(String language) {
  By changeLanguageMenuLocator = By.id("biblio_language_trigger");
  By languageLocator = By.xpath("//a[contains(@href,'" + 
                                language + 
                                "')]");
  
  wait.until(ExpectedConditions
      .elementToBeClickable(changeLanguageMenuLocator))
      .click();
  wait.until(ExpectedConditions
      .visibilityOfElementLocated(languageLocator));
  
  wait.until(ExpectedConditions
     .elementToBeClickable(languageLocator))
     .click();
}

private void deleteCookie(String cookieName) {
  driver.manage()
        .deleteCookieNamed(cookieName);
}

private String currentLanguage() {
  return getCookieValue("language");
}

private boolean isTitleCorrect() {
  if (currentLanguage().equalsIgnoreCase("en-CA") )
   return wait.until(ExpectedConditions
    .titleIs("Search | Vancouver Public Library | BiblioCommons"));
  else
   return wait.until(ExpectedConditions
    .titleIs("Rechercher | Vancouver Public Library | BiblioCommons"));
}

private String getCookieValue(String cookieName) {
  return driver.manage()
               .getCookieNamed(cookieName)
               .getValue();
}

private void addCookie(String name, String value) {
  deleteCookie(name);

  Cookie cookie = new Cookie.Builder(name, value)
   .domain(".bibliocommons.com")
   .expiresOn(new Date(2022, 03, 13))
   .isHttpOnly(false)
   .isSecure(true)
   .path("/")
   .build();

  driver.manage().addCookie(cookie);
}

}

The script is very similar with the previous one.

The only change is that instead of using the site to change the language, the language cookie is first deleted and then recreated for the new language.