Use Test Factories for Dynamic Tests

Most of the time, we need normal unit tests for test automation:

@Test
public void testSearch() {
}

A normal unit test is a public method that does not return a value, it does not have parameters and uses the @Test annotation.

The unit test can have a parameter though if it is used with a data provider:

@DataProvider(name = "searchKeywords")
public Object[][] searchKeywords() {
return new Object[][] {
{ "iphone"}, { "ipad"}, { "iwatch"}  };
}

@Test(dataProvider = "searchKeywords")
public void testSearch(String keyword) {
}

What is a dynamic test then?

A dynamic test is a unit test of a test class that has a constructor.

The constructor may have a parameter or more and saves them as class members:

import org.testng.annotations.Test;

public class SampleTest {

private int n;

public SampleTest(int n) {
  this.n = n;
}

@Test
public void testScript() {
  System.out.println("test script: " + n);
}

}

The test class gets an integer parameter in the constructor and saves it in a member of the class.

The testScript() method displays the value of the n class member.

testScript() is a dynamic unit test because it belongs to a test class that uses a constructor with a parameter (or more).

How do you use such a unit test?

A test factory creates objects of the test class.

These objects are then executed in the background:

import org.testng.annotations.Factory;

public class FactoryTest {

  @Factory
  public Object[] createTests() {
    System.out.println("create the tests dynamically!");
    Object[] tests = new Object[10];

    for (int i = 0; i < 10; i++)
      tests[i] = new SampleTest(i);

    return tests;

}

}

The createTests method creates first an array to store of test objects of the SampleTest class.

It then creates multiple SampleTest objects and saves them in the array.

Finally, it returns the array of objects.

This is the result of executing the FactoryTest class:

[RemoteTestNG] detected TestNG version 6.14.2
create the tests dynamically!
test script: 0
test script: 1
test script: 5
test script: 3
test script: 9
test script: 2
test script: 7
test script: 6
test script: 8
test script: 4
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript
PASSED: testScript

===============================================
Default test
Tests run: 10, Failures: 0, Skips: 0
===============================================

We see in these results 10 tests being executed and passing.

The interesting part is that they were not executed in the order they were created.

When would dynamic tests be needed in Selenium test automation?

Let’s assume that we want to automate a test case for the https://www.thebay.com/ site.

If you mouse over the accessories option of the menu, multiple categories are displayed.

bay

We want to verify that when clicking on any of these categories, results are displayed.

We could get started with a test as follows:

import static org.testng.Assert.assertTrue;

import java.util.List;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class CategoriesTest {

@DataProvider(name = "accessoriesCategories")
public Object[][] createData1() {
  return new Object[][] {
    { "New Arrivals"},
    { "Best Sellers"},
    { "Online Only"},
    { "Watches"}
  };
}

@Test(dataProvider = "accessoriesCategories")
public void testCategoriesUsingDataProvider(String category) {

  HomePage homePage = new HomePage();
  homePage.open();

  Menu menu = homePage.openAccessoriesMenu();

  ResultsPage resultsPage = menu.selectCategory(category);
  assertTrue(resultsPage.hasResults(), "no results for " + category);

}

}

The categories are defined in a data provider and the test has a parameter for a category.

The test will be executed once for each category.

The test uses 3 classes.

HomePage.java

public class HomePage {

public void open() {
  System.out.println("open home page");
}

public Menu openAccessoriesMenu() {
  System.out.println("open accessories menu");
  return new Menu();
}

}

Menu.java

import java.util.Arrays;
import java.util.List;

public class Menu {

public ResultsPage selectCategory(String category) {
  System.out.println("category selected: " + category);
  return new ResultsPage();
}

public List getCategories() {
  System.out.println("get categories");

  return Arrays.asList( "New Arrivals",
                        "Best Sellers",
                        "Online Only",
                        "Watches");
  }

}

ResultsPage.java

public class ResultsPage {

public boolean hasResults() {
  return true;
}

public HomePage goBack() {
  System.out.println("go back to home page");
  return new HomePage();
}
}

The result of executing the test is below:

[RemoteTestNG] detected TestNG version 6.14.2
open home page
open accessories menu
category selected: New Arrivals
open home page
open accessories menu
category selected: Best Sellers
open home page
open accessories menu
category selected: Online Only
open home page
open accessories menu
category selected: Watches
PASSED: testCategoriesUsingDataProvider("New Arrivals")
PASSED: testCategoriesUsingDataProvider("Best Sellers")
PASSED: testCategoriesUsingDataProvider("Online Only")
PASSED: testCategoriesUsingDataProvider("Watches")

===============================================
Default test
Tests run: 4, Failures: 0, Skips: 0
===============================================

The solution works if the categories for the Accessories option do not change.

But what if they change all the time?

Obviously, we will not be able to use the data provider any longer so the test will change:

@Test
public void testCategoriesWithoutDataProvider() {
  HomePage homePage = new HomePage();
  homePage.open();

  Menu menu = homePage.openAccessoriesMenu();

  List < String > categories = menu.getCategories();

  for (String category: categories) {
    ResultsPage resultsPage = menu.selectCategory(category);
    assertTrue(resultsPage.hasResults(), "no results for " + category);

    homePage = resultsPage.goBack();
    menu = homePage.openAccessoriesMenu();
  }

}

The test is more complicated than before.

It gets all categories of the menu and then iterates through them. For each category, the test selects the category, verifies that there are results and then goes back to HomePage.

The result of executing the test is below:

[RemoteTestNG] detected TestNG version 6.14.2
open home page
open accessories menu
get categories
category selected: New Arrivals
go back to home page
open accessories menu
category selected: Best Sellers
go back to home page
open accessories menu
category selected: Online Only
go back to home page
open accessories menu
category selected: Watches
go back to home page
open accessories menu
PASSED: testCategoriesWithoutDataProvider

===============================================
Default test
Tests run: 1, Failures: 0, Skips: 0
===============================================

The test does pretty much the same thing as before.

But instead of having one test for each category, we have 1 single test for all categories.

If the test fails, it will not be clear which category has a problem.

So this is what we want to fix.

We will simplify the test class so it does only the selection of the category:

import static org.testng.Assert.assertTrue;

import java.util.List;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SelectCategory {

private String category;

public SelectCategory(String category) {
this.category = category;
}

@Test
public void selectCategory() {
  HomePage homePage = new HomePage();
  homePage.open();

  Menu menu = homePage.openAccessoriesMenu();

  ResultsPage resultsPage = menu.selectCategory(category);
  assertTrue(resultsPage.hasResults(), "no results for " + category);

}
}

And we will use a test factory class that gets all categories and creates multiple objects for the SelectCategory class:

import java.util.List;

import org.testng.annotations.Factory;

public class SelectCategoryFactory {

@Factory
public Object[] testCategories() {
  System.out.println("start test factory");

  HomePage homePage = new HomePage();
  homePage.open();

  Menu menu = homePage.openAccessoriesMenu();

  List < String > categories = menu.getCategories();

  closeBrowser();

  Object[] tests = new Object[categories.size()];

  for (int i = 0; i < categories.size(); i++)
    tests[i] = new SelectCategory(categories.get(i));

  System.out.println("end of test factory");

  return tests;
}

private void closeBrowser() {
  System.out.println("close site");
}

}

The result of executing the test factory shows again 4 tests passing:

[RemoteTestNG] detected TestNG version 6.14.2
start test factory
open home page
open accessories menu
get categories
close site
end of test factory
open home page
open accessories menu
category selected: Watches
open home page
open accessories menu
category selected: Best Sellers
open home page
open accessories menu
category selected: New Arrivals
open home page
open accessories menu
category selected: Online Only
PASSED: selectCategory
PASSED: selectCategory
PASSED: selectCategory
PASSED: selectCategory

===============================================
Default test
Tests run: 4, Failures: 0, Skips: 0
===============================================

The test factory allows the initial test to be changed so that it works for dynamic categories. It also allows executing a test for each category.

Advertisements

Program to an interface with Selenium WebDriver

pexels-photo-755834.jpeg

Be Flexible With Java Interfaces – Pexels.com

 

Anyone can write code that a computer can understand.
Good programmers write code that humans can understand.

Martin Fowler

 

Testers and developers use the Selenium WebDriver library for implementing test automation for various websites.

They use Selenium WebDriver for its major purpose: to interact with a site in the browser.

 

But I believe that there is something else that we can use this library for.

We can use it to learn how to write good code.

Code that is easy to maintain.

Easy to extend, easy to use and understand.

 

What can you learn from the Selenium WebDriver code?

 

You can learn how to apply programming concepts such as factories.

Or how to extend code instead of modifying it with decorators.

Or how program to an interface.

 

Programming to an interface is the focus here.

 

What does it mean?

 

It means two things:

1. each class that you create should implement an interface

2. every time an object of the class is created, its type should be the interface that the class implements

 

This sounds pretty abstract so lets see if an example makes things simpler.

 

This is the example provided for interfaces in the java documentation.

In its most common form, an interface is a group of related methods with empty bodies.

A bicycle’s behavior, if specified as an interface, might appear as follows:

 

interface Bicycle {

  void changeCadence(int newValue);

  void changeGear(int newValue);

  void speedUp(int increment);

  void applyBrakes(int decrement);
}

 

To implement this interface, the name of the class would change to a particular brand of bicycle and you’d use the implements keyword in the class declaration.

The class would also have to provide an implementation for each method defined by the interface.
class TrekBicycle implements Bicycle {

  int cadence = 0;
  int speed = 0;
  int gear = 1;

  void changeCadence(int newValue) {
    cadence = newValue;
  }

  void changeGear(int newValue) {
    gear = newValue;
  }

  void speedUp(int increment) {
    speed = speed + increment;
  }

  void applyBrakes(int decrement) {
    speed = speed - decrement;
  }

  void printStates() {
    System.out.println("cadence:" +
      cadence + " speed:" +speed + " gear:" + gear);
 }

}

 

Implementing an interface allows a class to become more formal about its behavior.

If your class claims to implement an interface, all methods defined by that interface must appear in its source code before the class will successfully compile.

 

What is so special about a class implementing an interface?

 

When you create an object of the class, you can do it in 2 different ways:

TrekBicycle trekBike = new TrekBicycle();
Bicycle trekBike = new TrekBicycle();

 

The second way of creating the object provides a lot of flexibility in dealing with all bicycle objects .

If you have an additional Bicycle class, for example

class GTBicycle implements Bicycle {

  int cadence = 0;
  int speed = 0;
  int gear = 1.25;

  void changeCadence(int newValue) {
    cadence = newValue;
  }

  void changeGear(int newValue) {
    gear = newValue * 1.1;
  }

  void speedUp(int increment) {
    speed = speed + increment + 1.2;
  }

  void applyBrakes(int decrement) {
    speed = speed - decrement;
  }

  void printStates() {
    System.out.println("cadence:" +cadence + " speed:" +
   speed + " gear:" + gear);
  }

}


an object of the GTBicycle class can also be saved in a Bicycle variable:

Bicycle gtBike = new TrekBicycle();

 

Any object of a class than implements the Bicycle interface can be saved in a Bicycle variable.

 

Where have we seen this with Selenium WebDriver?

 

You can see interfaces everywhere:

 

1. When creating a driver object

A driver object for the Chrome browser is created as follows

WebDriver driver = new ChromeDriver();

 

WebDriver is an interface while ChromeDriver is a class that extends RemoteDriver which implement WebDriver.

 

To create a driver for Firefox, we just change the class name:

WebDriver driver = new FirefoxDriver();

Same for the driver for Edge:

WebDriver driver = new EdgeDriver();

 

2. When finding a web element

WebElement button = driver.findElement(locator);

 

WebElement is an interface.

For this reason, you can save in a WebElement variable any type of html element: button, link, label, image, list, textbox, etc.

 

3. When typing a keyword in a textbox

All of the following ways of typing a keyword in a textbox work:

a. type a String value

WebElement searchBox = driver.findElement(textBoxId);
String keyword = "symphony";
searchBox.sendKeys(keyword);

 

b. type a StringBuilder value

WebElement searchBox = driver.findElement(textBoxId);
StringBuilder author = new StringBuilder()
                          .append("wolfgang")
                          .append(" ")
                          .append("amadeus")
                          .append(" ")
                          .append("mozart");

searchBox.sendKeys(author);

 

c. type a StringBuffer value

WebElement searchBox = driver.findElement(textBoxId);
StringBuffer topic = new StringBuffer().append("classical")
                                       .append(" ")
                                       .append("music");

searchBox.sendKeys(author);

 

d. type a String, a StringBuilder, a StringBuffer and a key

WebElement searchBox = driver.findElement(searchBoxId);
StringBuilder author = new StringBuilder()
                         .append("wolfgang")
                         .append(" ")
                         .append("amadeus")
                         .append(" ")
                         .append("mozart");

String space = " ";
StringBuffer topic = new StringBuffer()
                         .append("classical")
                         .append(" ")
                         .append("music");

String subTopic = "symphony";

searchBox.sendKeys(author, space, topic, space, subTopic, Keys.TAB);

 

Why do all these different ways of typing a keyword work?

 

It looks as if sendKeys may have one or more parameters and they can have different types: String, StringBuilder, StringBuffer, Keys.

The sendKeys signature explains it all:
void sendKeys(CharSequence... keysToSend)

 

The … explains why you can have one or more parameters.
… means that sendKeys can have a variable number of parameters of the same type.
All parameters must have the CharSequence type.

 

CharSequence explains why we can have so many classes as parameter types for sendKeys.

 

CharSequence is an interface in Java that is implemented by classes such as String, StringBuilder and StringBuffer.

This means that you can use any object of the String, StringBuffer and StringBuilder classes as a CharSequence variable.

 

What about Keys?

Keys is a Selenium enumeration which implements as well the CharSequence interface.

What should we learn from all this?

A few things …

1. Each class that we create should implement an interface.

2. When creating an object of the class, use the interface as its type.

 

How can you use interfaces in a Selenium project?

 

If you are interested in seeing how “programming to an interface” is done in a Selenium WebDriver project step by step, I have an ebook for you.

 

It will show you how to change code that does not use interfaces so that it follows the “programming to an interface” principle.

 

The result of applying this principle is that we will be able to replace 10 automated scripts that search for different categories (car, minivan, boat, rv, etc) with one script.

So, instead of having a search test for each category

public class TestClass {

   CarInfo carInfo = new CarInfo("audi", "a4");

   MinivanInfo minivanInfo = new MinivanInfo("nissan","navara", "pickup");

   BikesInfo bikesInfo = new BikesInfo(
                             "kawasaki", "gpz1000", "sports tourer");

   @Test
   public void canSearchForCars() {

     CarHome homePage = new CarHome();
     homePage.open();

     CarResults resultsPage = homePage.searchFor(carInfo);
     assertTrue(resultsPage.count() > 0);

   }

   @Test
   public void canSearchForMinivans() {

     MinivanHome homePage = new MinivanHome();
     homePage.open();

     MinivansResults resultsPage = homePage.searchFor(minivanInfo);
     assertTrue(resultsPage.count() > 0);

  }

  @Test
  public void canSearchForBikes() {

     BikesHome homePage = new BikesHome();
     homePage.open();

     BikesResults resultsPage = homePage.searchFor(bikesInfo);
     assertTrue(resultsPage.count() > 0);

  }


}

 

you can have

public class TestClass {

  String postalCode = "w1f7tu";

  @DataProvider(name = "vehicleInfo")
  public Object[][] vehicleInfo() {
    return new Object[][] {
      { "cars" , new CarInfo("BMW", "3 SERIES") },
      { "minivans", new MinivanInfo("NISSAN", "NAVARA", "Pickup")},
      { "bikes" , new MotorHomesInfo("AUTO-SLEEPERS", "2")},
    };
  }

  @Test(dataProvider = "vehicleInfo")
  public void canSearchForCategory(String category,
                                   VehicleInfo vehicleInfo) {

     HomePage homePage = openHomePage(category);
     homePage.open();

     ResultsPage resultsPage = homePage.searchFor(
                                     vehicleInfo, postalCode);
     assertTrue(resultsPage.count() > 0);
  }

}

 

Interested?

 

Send USD 15 to alex@alexsiminiuc.com through Paypal.
I will email you the book and answer all your questions about it, also by email.

 

6 things to avoid when learning selenium automation

dontdoit-1

1. Aiming at becoming a Selenium automation expert

This goal is so high that you will probably fail.

You should replace it with something more achievable like “i want to get a test automation job in 1 year”.

 

2. Selenium WebDriver certifications

Selenium WebDriver certifications are scams. Even if they would be good, they wont help you getting hired. When studying for a certification, you study for the exam that you will take and not for the subject.

Passing an exam is a very poor way of measuring your programming skills.

 

I met people who passed with 90% grades TOEFL (test of english as a foreign language) and TWE (test of written english) but speak and write very bad english.

 

3. Jumping into writing Selenium WebDriver tests before learning a programming language

Test automation with Selenium WebDriver is basically programming.

Without knowing a programming language well, you will always have big obstacles in writing automation code. Selenium WebDriver is just a library that can be used through multiple languages.

Jumping to Selenium tests with no programming is similar with trying to cook lasagna with no cooking skills.

Shortcuts never work. There are no shortcuts to math, to learning french, to getting fit, to anything. You have to put the effort in and follow a process that takes time.

 

4. Looking for the best selenium book or online course

There is no such thing. You will find though many great books and online courses.

The best book for Selenium should include everything about Selenium, everything about the programming language (lets say Java), everything about an IDE (example: Eclipse), everything about unit testing, maven, code design, code refactoring, maven, etc.

Do you really think that it exists?

It does not.

To learn test automation with Selenium, you need to use a multitude of resources, books, blogs, youtube videos, online courses, webinars, etc.

Because the amount of information needed for an automation job is very high.

 

5. Learning by yourself

If you attempt learning by yourself, chances are high that you wont make it.

Find other people with the same goal, take instructor-led courses, find developers willing to help you, go to meetups, anything but being by yourself.

 

6. Selenium IDE

Record and play is not test automation

 

Use streams for lists of web elements

2179048[1]

Every time you need to find multiple elements in your Selenium code, you get a list of web elements, so a collection.

Streams and predicates, introduced in Java 8, are the best way of working with collections.

When you use them, you don’t only get code that is shorter and easier to understand but also better performance.

Why is this useful for you, a Selenium WebDriver developer?

It is useful because instead of getting texts and attributes of all elements (matched by the same locator) with

private List< String > textValues(By locator) {
  List elements = driver.findElements(locator);

  List< String > values = new ArrayList<>();

  for (WebElement e : elements)
    values.add(e.getText());

  return values;
}

private List< String > attrValues(By locator, String name) {
  List elements = driver.findElements(locator);

  List< String > values = new ArrayList<>();

  for (WebElement e : elements)
    values.add(e.getAttribute(name));

  return values;
}

you can use

private List< String > textValues(By loc) {
  return getValues(loc, e -> e.getText());
}

private List< String > attrValues(By loc, String name) {
  return getValues(loc, e -> e.getAttribute(name));

private List< String > getValues(
By loc, Function<WebElement,String > pred) {

  List< WebElement > elements = driver.findElements(loc);       

  List< String > values = elements.stream().map(pred)
                                  .collect(Collectors.toList());

  return values;


}

Read along to understand why the second code version is much better.

How to use collections the old way

Assume that you have a Result class for results of a page:

public class Result {

  private String name;
  private int price;
  private String owner;
  private boolean onlineOnly;

  public Result(String name, int price, String owner, boolean onlineOnly) {
    this.name = name;
    this.price = price;
    this.owner = owner;
    this.onlineOnly = onlineOnly;
  }

  public String name() {
    return this.name;
  }

  public int price() {
    return this.price;
  }

  public String owner () {
    return this.owner;
  }

  public boolean onlineOnly() {
    return this.onlineOnly;
  }

  @Override
  public String toString() {
    return name + " - " + price + " - " + owner + " - " + onlineOnly;
  }

}

You need also a Results class (for lists of results):

public class Results {

  List< Result > results;

  public Results(List results) {
    this.results = results;
  }

  public List< Result > get() {
    return this.results;
  }

  public Result getResult(int i) {
    return this.results.get(i);
  }

  public int size() {
    return this.results.size();
  }

  @Override
  public String toString() {
    return this.results.toString();
  }

}

So far, Results class has methods for returning

  • the list of results
  • an element of the list
  • the size of the list
  • a String value for the list

Pretty basic, so far.

We would like to add filtering methods to the Results class such as:

public Results filterByOnlineOnly() {
  List< Result > list = new ArrayList()<>;

  for (Result r : this.results)
    if (r.onlineOnly() == true)
      list.add(r);

  return new Results(list);
}

public Results filterByOwner(String name) {
  List< Result > list = new ArrayList()<>;

  for (Result r : this.results)
    if (r.owner().equals(name))
      list.add(r);

  return new Results(list);
}

public Results filterByName(String name) {
  List< Result > list = new ArrayList()<>;

  for (Result r : this.results)
    if (r.name().equals(name))
      list.add(r);

  return new Results(list);
}

public Results filterByPrice(int price) {
  List< Result > list = new ArrayList()<>;

  for (Result r : this.results)
    if (r.price() == price)
      list.add(r);

  return new Results(list);
}

So far, we have filtering methods by name, owner, price and online only.

But, what should we do if we need more filtering methods such as

  • by price in a range
  • with a keyword in the name
  • with a keyword in the name and a specific owner

Adding more methods to the Results class is not the solution.

The more filters we need, the more methods.

One thing that is easy to notice is that all filtering methods have very similar code.

The only exception is the condition that the list elements should match.

How to use collections with streams and predicates

We can replace all methods with one method that uses a predicate parameter (for the filtering condition).

public Results filterBy(Predicate predicate) {

  List< Result > list = this.results.stream()
                  .filter(predicate)
                  .collect(Collectors.toList());

  return new Results(list);
}

How does this work?

  1. First, the results list is converted to a stream by the stream() method.
  2. Then, the filter() method filters the stream with the predicate . This is done by going through each element of the stream, applying the predicate to each element and selecting only the elements that match the predicate.
  3. Finally, collect() collects all selected elements and returns them as another list.

Amazing, isn’t it?

How can this method be used?

Results products = results.filterBy(r -> r.onlineOnly() == true);

Results products = results.filterBy(r -> r.name().equals("IPHONE 8"));

Results products = results.filterBy(r -> r.price() == 500);

We can get really creative now:

Results list = results.filterBy(r -> r.name().contains("IPHONE"));

Results products = results.filterBy(r -> r.name().contains("IPHONE") &&
                                         r.price() > 500 &&
                                         r.price() < 1000 &&
                                         r.onlineOnly() == false);

What else can you do with streams?

How about creating the list of all names, or all prices or all owners?

public List < String > values(Predicate predicate) {
  List list = this.results.stream()
                          .map(predicate)
                          .collect(Collectors.toList());

  return list;
}

Notice that the filter() method is replaced by the map().
map() extracts the value as specified by the predicate.
The method returns a list of Strings since names and owners are Strings:

List< String > owners = results.values(r -> r.owner());

List< String > names = results.values(r -> r.names());

There are many other things that can be done with streams and predicates:

Results onlineProducts = results.stream()
                                .filter(Result::onlineOnly)
                                .skip(1)
                                .collect(Collectors.toList());

Results cheapProducts = results.stream()
                           .filter(r -> r.price() <= 100)                                               .filter(r -> r.owner().equals("Rogers"))
                           .collect(Collectors.toList());

List< String > distinctOwners = results.stream()
                             .map(Result::owner)
                             .distinct()
                             .collect(Collectors.toList());

int priceSum = results.stream()
                      .map(Result::price)
                      .reduce(0, Integer::sum);

assertTrue(results.stream()
                  .anyMatch(r -> r.owner().equals("Bell")));

Java 8 In Action is a great book for learning more on streams and predicates.

Find child elements with locator chaining

KIT518860_broad_chain_closeup_c_wikimedia.org_[1]

Have you ever had to find an element in another element?

Of course you have.

How did you do it?

 

Maybe you used a locator that searches for the parent element first and then for the child element in the parent.

 

Like in the following test:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By TITLE_LOCATOR = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]" +
    "//span[@class='title']");

  //driver variable, setUp() and tearDown() methods are skipped

  @Test
  public void test1() {
    driver.get(HOME_URL);
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java");

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement title = driver.findElement(TITLE_LOCATOR);
    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }

}

 

In this case, the test opens the site, searches for a keyword and then finds the title of the first result.

The locator of the title includes the locator of the first result:

private final By TITLE_LOCATOR = By.xpath(
   "(//div[contains(@data-js, 'bib-list-item')])[1]" + 
   "//span[@class='title']");

(//div[contains(@data-js, ‘bib-list-item’)])[1]  is the locator of the first result.

//span[@class=’title’] is the locator of the first result’s title.

 

This seems like a decent approach but is it not.

If the code generates an exception when trying to find the title element, is the error about the title not being available or about the first result?

Since the locator combines both, it would be difficult to know.

 

We can change the code so that the parent element is found first and stored in a WebElement variable.

 

The child element is then searched inside of the parent element:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By RESULT_BOX_XPATH  = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]");

  private By TITLE_XPATH = By.xpath(
    ".//span[@class='title']"); 

  //driver variable, setUp() and tearDown() methods are skipped

  @Test
  public void test2() {
    driver.get(HOME_URL);   
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java"); 

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement result = driver.findElement(RESULT_BOX_XPATH);
    WebElement title = result.findElement(TITLE_XPATH);
    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }
}

 

This time, the code finds the result element and stores it in a WebElement variable:

WebElement result = driver.findElement(RESULT_BOX_XPATH);

Then, it searches for the title element inside of the result element:

WebElement title = result.findElement(TITLE_INNER_XPATH);

Notice that the title locator starts with a dot.

TITLE_XPATH = By.xpath(".//span[@class='title']");

This is important when searching an element in another element.

When the code works this way, in case of an error, it is very clear which element caused it.

 

This is better than before but it extends poorly.

What if you need to find an element included in another element included in another element included in another element?

For each parent element, you need to search for the element and store it in a variable.

Which leads to lots of duplication.

This duplication can be removed using the ByChained Selenium class.

See how the code looks now:

public class TestClass {

  private String HOME_URL = "http://www.vpl.ca/";
  private String RESULTS_URL = "vpl.bibliocommons.com/search";

  private By SEARCH_BOX_ID = By.id("edit-search");
  private By SEARCH_BUTTON_ID = By.id("edit-submit");

  private By RESULT_BOX_XPATH  = By.xpath(
    "(//div[contains(@data-js, 'bib-list-item')])[1]");

  //driver variable, setUp() and tearDown() methods are skipped

  private By TITLE_XPATH = By.xpath(".//span[@class='title']");

  @Test
  public void test3() {
    driver.get(HOME_URL);    
    assertEquals(driver.getCurrentUrl(), HOME_URL);

    WebElement searchBox = driver.findElement(SEARCH_BOX_ID);
    searchBox.sendKeys("java");  

    WebElement searchButton = driver.findElement(SEARCH_BUTTON_ID);
    searchButton.click();

    assertTrue(driver.getCurrentUrl().contains(RESULTS_URL));

    WebElement title = driver.findElement(
         new ByChained(RESULT_BOX_XPATH,
                TITLE_XPATH));

    String titleText = title.getText().toLowerCase();

    assertTrue(titleText.contains("java"));
  }  
}

The code to find the title looks rather weird:

WebElement title = driver.findElement(
         new ByChained(RESULT_BOX_XPATH,TITLE_XPATH));

It creates a new object for the ByChained class.

The object gets 2 parameters in the constructor for the locators of the parent and child elements.

Finding the element happens as follows:

  • the element for the 1st locator is searched
  • the element for the 2nd locator is searched in the first element

Using this approach scales well even if we have elements included in many other elements.

There are no additional WebElements that should be created and the code stays approximately the same size.

 


Tips and tricks are great.

Keep coming to this blog for more.

However, if you are interested in better and more detailed learning, this Kindle ebook will help you improve your Selenium code:

Improve Selenium Code with Automation Patterns: Page Object Model, Page Factory , Page Elements, Base Page, Loadable Component

I am using all these patterns daily and so can you.

What are the differences between Selenium testers and Selenium developers?

wir[1]

What follows is inspired from reality.

It is not a product of my imagination.

 

Tester: Selenium is a tool like QTP.

Developer: Selenium is a Java library for web test automation.

 

Tester: I will automate test cases by recording them with Selenium IDE.
Developer: Selenium IDE is not an automation tool.

 

Tester: I dont need to write code like developers. I used to write Visual Basic code and that should be enough for automation.
Developer: The automation code should be at the level of production application code.

 

Tester: Every time my test needs a verification, I do it in IF/ELSE statements.
Developer: Verifications are done with JUNIT or TESTNG assertions.

 

Tester: Who cares if all code is in the test method? It works and it looks good to me.
Developer: I will first write an automation framework.
Then I will create the page object classes that are used in the test methods.

 

Tester: Explicit waits and expected conditions are not reliable and dont work well for me. I have created my own.
Developer: Explicit waits and expected conditions work well. I extend them in custom classes when needed.

 

Tester: Look, my test works! That is all I care about! I can start a new test!
Developer: My test should work but also be easy to understand, short and maintainable.

 

Tester: All my libraries are imported to the project. If I need new versions, I remove the old libraries and import the new ones. I used Maven long time ago and dont remember much about it.
Developer: I use Maven for managing dependencies.

 

Tester: All tests run from my computer every night. I run them anytime someone needs them.
Developer: Anyone can run my tests from Jenkins. They are scheduled to run every night.

 

Tester: My tests run in parallel on my computer.
Developer: My tests run in parallel on all available Jenkins slaves.

 

Tester: I love using static methods and static variables.
Developer: Static sucks. I avoided it as much as possible.

 

Tester: My project is very cool. I have a utility class for all common purpose methods. All page classes inherit from the utility class. This makes my page classes shorter.
Developer: I use composition to break the page classes in page element classes.

 

Tester: All my page element locators are in property text files.
Developer: If the page element locators are outside of the page classes, the class encapsulation is broken.

 

Do you have anything else that should be added to the list?

Please tell in the comments so I can add more.

 


 

Want to be more than a Selenium tester?

Start with this Kindle book on automation patterns that can make your Selenium code better:

Improve Selenium Code with Automation Patterns:

Page Object Model, Page Factory, Page Elements, Base Page, Loadable Component

 

amazon book

 

 

Use a custom driver class instead of utility classes

How can I create my own driver class in Selenium?
When learning test automation, you will want your test methods to be executed in multiple browsers such as Firefox and Chrome.

A Firefox driver is created with the FirefoxDriver class:

WebDriver driver = new FirefoxDriver();

A Chrome driver is created with the ChromeDriver class:

WebDriver driver = new ChromeDriver();

Both driver objects use the WebDriver type but are instantiated by different classes.

Now, you dont want to change the class from FirefoxDriver to ChromeDriver and back every time you want to run the tests in another browser.

Instead, you would like to create the driver based on the browser name:

WebDriver driver = new BrowserDriver(browserName);

How do we do this?

We create a custom driver class that implements the WebDriver interface and overrides its methods.

public final class BrowserDriver implements WebDriver {  

   private WebDriver driver;
   private final String browserName;
   private final int timeout = 30; 
   private final String chromeDriverPath = "./src/chromedriver.exe";  

   public BrowserDriver(String browserName) { 
      this.browserName = browserName; 
      this.driver = createDriver(browserName); 
   }  

   private WebDriver createDriver(String browserName) { 
      if (browserName.toUpperCase().equals("FIREFOX") 
         return firefoxDriver(); 

      if (browserName.toUpperCase().equals("CHROME") 
         return chromeDriver();  

      throw new RuntimeException ("invalid browser name"); 
   } 

   private WebDriver chromeDriver() { 
      if (!new File(chromeDriverPath).exists()) 
        throw new RuntimeException
                    ("chromedriver.exe does not exist!"); 

      try { 
        System.setProperty("webdriver.chrome.driver", 
                           chromeDriverPath); 
        return new ChromeDriver(); 
      } 

      catch (Exception ex) { 
        throw new RuntimeException
              ("couldnt create chrome driver"); 
      } 
   } 

   private WebDriver firefoxDriver() { 
      try { 
         return new FirefoxDriver(); 
      } 
      catch (Exception ex) {         
         throw new RuntimeException
              ("could not create the firefox driver"); 
     } 
   } 

   @Override 
   public String toString() { 
      return this.browserName; 
   } 

   public WebDriver driver() { 
      return this.driver; 
   }  

   @Override 
   public void close() { 
      driver().close();   
   }  

   @Override 
   public WebElement findElement(By locator) { 
      return driver().findElement(locator); 
   }  

   @Override 
   public List findElements(By arg0) { 
      return driver().findElements(arg0); 
   }  

   @Override 
   public void get(String arg0) { 
      driver().get(arg0); 
   }  

   @Override 
   public String getCurrentUrl() { 
      return driver().getCurrentUrl(); 
   }  

   @Override 

   public String getPageSource() { 
      return driver().getPageSource(); 
   }  

   @Override 
   public String getTitle() { 
      return driver().getTitle(); 
   }  

   @Override 
   public String getWindowHandle() { 
      return driver().getWindowHandle(); 
   }  

   @Override 
   public Set getWindowHandles() { 
     return driver().getWindowHandles(); 
   }  

   @Override 
   public Options manage() { 
      return driver().manage(); 
   }  

   @Override 
   public Navigation navigate() { 
      return driver().navigate(); 
   }  

   @Override 
   public void quit() { 
     driver().quit(); 
   }  

   @Override 
   public TargetLocator switchTo() { 
      return driver().switchTo(); 
   }  

   @Override 
   public  X getScreenshotAs(OutputType target
   throws WebDriverException { 
      return ((TakesScreenshot) driver())
                    .getScreenshotAs(target);
   } 

}

 

The only method that is not defined in the WebDriver interface is createDriver().
createDriver() creates a driver object based on the browser name and stores it in the driver variable.

All other methods just override the WebDriver interface methods.

In the test method, you can now use the new BrowserDriver class instead of FirefoxDriver and ChromeDriver:

WebDriver driver = new BrowserDriver(“CHROME”);

driver.get(“http://www.bestbuy.com”);

WebElement searchBox = driver.findElement(searchBoxLocator);

 

The custom driver class can add as well new methods.

 

Lets say that you want to have find element methods that use the WebDriverWait and ExpectedConditions classes:

public WebElement findVisibleElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
                .until( visibilityOfElementLocated (locator));
   return element;
}

public WebElement findClickableElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
               .until( elementToBeClickable (locator));
   return element;
}

public WebElement findHiddenElement(By locator) {
   WebElement element = new WebDriverWait(driver(), timeout)
              .until( presenceOfElementLocated (locator));
   return element;
}

You may be tempted to create a utility class, move these methods there and then use the utility class as a parent for the page classes.

Resist this temptation as it leads to bad things 🙂

Avoid utility classes in your automation projects.

More about utility classes very soon on this blog.

Instead of the utility class, you can move these methods to your custom driver class.

public final class BrowserDriver implements WebDriver {  

   private WebDriver driver; 
   private final String browserName; 
   private final int timeout = 30; 
   private final String chromeDriverPath = "./src/chromedriver.exe"; 

   public BrowserDriver(String browserName) { 
      this.browserName = browserName; 
      this.driver = createDriver(browserName); 
   }  

......................................................................

   @Override 
   public WebElement findElement(By locator) { 
     WebElement element = new WebDriverWait(driver(), timeout) 
                        .until( visibilityOfElementLocated(locator)); 
     return element; 
   }  

   @Override 
   public List findElements(By arg0) { 
     return driver().findElements(arg0); 
   }

   public WebElement findClickableElement(By locator) { 
      WebElement element = new WebDriverWait(driver(), timeout) 
                       .until( elementToBeClickable(locator)); 
      return element; 
   } 

   public WebElement findHiddenElement(By locator) { 
      WebElement element = new WebDriverWait(driver(), timeout) 
                      .until( presenceOfElementLocated(locator)); 
      return element; 
 }

}

How to deal with windows authentication popups

auth-required-basic

 

If you dont like Bugs Bunny, maybe you should not continue reading.

 

What is the first thing that I do every morning at work?

Check the results of the automation scripts executed in Jenkins over night.

I want to know how many scripts passed or failed because of the work done in the previous day.

Most days, some scripts pass and some fail.

But recently, most scripts failed.

This was very unusual so I looked into it right away.

All failures followed the same pattern: the scripts failed on opening the site in the browser.

 

Bugs-Bunny-Looking-Shocked

 

 

A sample script looks like this:

@Test
public void canBrowseThroughPages() {

  HomePage homePage = new HomePage(driver);
  homePage.open();

  ResultsPage resultsPage = homePage.search(keyword);

  assertTrue(resultsPage.resultCount() > 0);
 
  resultsPage = resultsPage.selectNextPage();

  assertTrue(resultsPage.isCurrentPage(2));
  assertTrue(resultsPage.resultCount() > 0);

}

 

The error happened in all scripts on the second line:
homePage.open();

The error info was

java.lang.object.RuntimeException:
home page didnt load correctly:
expected url = http://www.testsite.com
actual url =

 

I looked at the code of the open() method:

public class HomePage {

  private WebDriver driver;
  private WebDriverWait wait;

  private String url = "http://www.testsite.com";

  public HomePage(WebDriver driver) {
    this.driver = driver;
    wait = new WebDriverWait(this.driver, 30);
  }

  public void open()

    try {
      this.driver.get(url);
    }
    catch (Exception ex) {
     throw new RuntimeException("home page cannot be opened!");
    }

    try {
     wait.until(urlContains(url));
    }
    catch (Exception ex) {
     throw new RuntimeException("home page didnt load correctly - " +
             "expected url = " + url +
             " - " +
             "actual url = " + this.driver.getCurrentUrl());
    }
  }

 

I could not find anything wrong in the method.

It tries to open the url.
If it cannot, it throws an exception.

If the url is opened, it tries to verify the page url.
If it cannot, it throws an exception.

 
So, I looked again at the exception info:

java.lang.object.RuntimeException:
home page didnt load correctly;
expected url = http://www.testsite.com
actual url =

 

The actual url is empty which means that the site was not loaded.

But, why is the second exception thrown instead of the first one?

If driver.get(url) does not work, why doesn’t it generate an exception?

 

bugs bunny puzzled

Strange, isn’t it?

 

Next, I execute the script locally in Eclipse and it works.

So, the script works locally but has an error when executed in Jenkins.

 

What is the issue?

I look at the Jenkins execution details and get the name of the Jenkins slave where the script ran.

I start a remote connection to that server, open Chrome and load the site url in it.

And surprise!

A windows authentication popup is displayed!!!

That’s why the second exception was thrown instead of the first.

When executing driver.get(url), before the url is launched in the browser, the windows authentication popup is displayed.

driver.get() passes and the code continues with the url validation.

Since the authentication popup is still displayed, the page is not loaded and the url validation fails.

So the authentication popup is the culprit.

 

How can I disable the windows authentication popup?

Resetting the chrome browser settings does not help.

I can try creating the chrome driver with options and adding the site url to the chrome authentication whitelist.

This works manually.

When added to the code, the authentication popup is still there:

ChromeOptions options = new ChromeOptions();
options.addArguments("auth-server-whitelist='www.testsite.com'");
ChromeDriver driver = new ChromeDriver(options);

 
What else can I do?

Nothing goes right ……………….

So …………..

 

nothing goes right

 

Lets go left 🙂

 

I can include the account’s username and password in the url:

public class HomePage {

  private WebDriver driver;
  private WebDriverWait wait;

  private String url = "http://www.testsite.com";

  public HomePage(WebDriver driver) {
    this.driver = driver;
    wait = new WebDriverWait(this.driver, 30);
  }

  public void open() {

    try {
      this.driver.get(urlWithAccount(url));
    }
    catch (Exception ex) {
      throw new RuntimeException("home page cannot be opened");
    }

    try {
      wait.until(ExpectedConditions.urlContains(url);
    }
    catch (Exception ex) {
      throw new RuntimeException("home page didnt load correctly - " +
                                 "expected url = " + url +
                                 " - " +
                                 "actual url = " +                                                             this.driver.getCurrentUrl());
    }

  }

  private urlWithAccount(String url) {
     String username = System.getProperty("username");
     String password = System.getProperty("password");

     String newUrl = url.replace("http://",
                                 "http://" +
                                 username +
                                 ":" +
                                 password +
                                 "@");

     return newUrl;
  }

 

With the new method, the url would changes from

http://www.testsite.com

to

http://username:password@www.testsite.com

 

Rerunning the Jenkins scripts again showed that the problem is resolved.

 

thats all folks

How to use locators with parameters

Sometimes, you need to interact in automation scripts with web elements that have similar HTML code.

For example, you may want to automate the following test case:

  1. open site
  2. search for keyword
  3. check that the results page is displayed
  4. select the 2nd page of results
  5. check that the 2nd page is displayed
  6. select the 3rd page of results
  7. check that the 3rd page is displayed

 

The elements for the 2nd and 3rd pages have similar HTML code (A tag, HREF and DATE-PAGE attributes):

page 2
<a href=”/catalog/category.aspx?category=29059;page=2″ data-page=”2″>2</a>

page 3
<a href=”/catalog/category.aspx?category=29059;page=3″ data-page=”3″>3</a>

 

There are a few options for selecting similar page elements:

  1. use a locator for each page element
  2. use a method that creates the locator for each page locator
  3. use a locator pattern with a parameter

 

The code for option 1 is below:

import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.openqa.selenium.By;
import static org.openqa.selenium.By.*;
import org.openqa.selenium.WebElement;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;

public class BestBuyTest extends BaseTest {

String homePageUrl = "http://www.bestbuy.ca/";

By searchTextBoxLocator = By.id("ctl00_MasterHeader_ctl00_uchead_GlobalSearchUC_TxtSearchKeyword");

By searchButtonLocator = By.className("icon-search");

String resultsPageUrl = "http://www.bestbuy.ca/en-CA/category/";

By page2Locator = By.xpath(
"//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" + "//a[@data-page='2']");

By page3Locator = By.xpath(
"//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='3']");

@Test
public void TestPaging1() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (page2Locator));

  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
          elementToBeClickable
           (page3Locator));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private void openSite() {
  driver.get(homePageUrl);

  if (wait.until(
       urlToBe(homePageUrl))== false )

    throw new RuntimeException
      ("The home page is not loaded correctly!");
}

private void searchBy(String keyword) {
  WebElement searchTextBox = wait.until(
                               elementToBeClickable
                                (searchTextBoxLocator));

  searchTextBox.sendKeys("ipad");

  WebElement searchButton = wait.until(
                             elementToBeClickable
                              (searchButtonLocator));

  searchButton.click();

  if (wait.until(
       urlContains
        (resultsPageUrl))== false )

     throw new RuntimeException
     ("The results page is not loaded correctly!");
}

private boolean isPageDisplayed(int pageNumber) {
  return wait.until(
          urlContains
           ("page=" + pageNumber));
}

}

This option is not good because of the duplicated page locators.

 

Option 2 uses a method that generates the page locator:

@Test
public void TestPaging2() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (pageLocator(2)));

  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
           elementToBeClickable
            (pageLocator(3)));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private By pageLocator(int page) {
String locatorText = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='" +page"']";

return By.xpath(locatorText);
}

The pageLocator() method creates the locator using a page number parameter.

The locator is created by concatenating 3 parts:

  • the beginning of the xpath expression (before the page number)
  • the page number
  • the ending of the xpath expression

 

private By pageLocator(int page) {
String locatorText = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='"page"']";

return By.xpath(locatorText);
}

With the pageLocator() method, there is no need of having a locator for each page element.

But the locator text was moved inside of the method.

And the method’s code is pretty complicated.

 

Option 3 is using a locator pattern with a parameter:

String pageLocatorTemplate = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='%d']";

@Test
public void TestPaging3() throws Exception {

  openSite();
  searchBy("ipad");

  WebElement page = wait.until(
                     elementToBeClickable
                      (pageLocatorFromTemplate(2)));
  page.click();

  assertTrue(isPageDisplayed(2));

  page = wait.until(
          elementToBeClickable
           (pageLocatorFromTemplate(3)));

  page.click();

  assertTrue(isPageDisplayed(3));

}

private By pageLocatorFromTemplate(int page) {
  return By.xpath(
          String.format
           (pageLocatorTemplate, page));
}

We use now a locator pattern with a parameter (%d):

String pageLocatorTemplate = "//div[@id='ctl00_CP_ctl00_ctl01_ProductSearchResultListing_topPaging']" +
"//a[@data-page='%d']";

 

A method is still needed for creating the locator.

The method has a pageNumber parameter which is substituted to the parameter (%d) of the locator pattern by the String.format() method:

private By pageLocatorFromTemplate(int page) {
  return By.xpath(
          String.format
           (pageLocatorTemplate, page));
}

The 3rd solution is the best of the 3 presented.

 

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