Chapter 8 - Using Locators for Testing



Transcripted Summary

Now that we know all about web element locators, let's use them for test automation.

When it comes to automating web UI tests, there are two main categories of tools: codeless and coded.

"Codeless" tools enable testers to write test procedures using some sort of form, builder, or recorder without the use of a programming language.

These tools are ideal for manual testers who are great at testing but not strong at programming.

Popular codeless testing tools include Selenium IDE, Smartbear Testcomplete and TestCraft, but there are many more available. Another interesting tool that is halfway between coded and codeless is Cycle Automation, which provides out of the box Gherkin steps for Web UI interactions.

Whatever the tool, they all will need to find elements using locators. Some tools require the users to enter locators for desired elements, while other tools generate locators during test recording sessions.

If you're using a codeless tool, follow the tool stocks and best practices for how to handle locators and apply what you have learned in this course to write good locators when necessary.

"Coded" automation tools essentially mean test automation frameworks and programming packages. To use them, you will need software development skills for web UI testing.

For web UI testing Webdriver is the defacto standard protocol. In fact, many codeless and even other coded tools use WebDriver under the hood. There are a number of packages that either implement WebDriver directly, or bundle and improve an existing implementation.

The most popular package is by far Selenium WebDriver, but others include Appium, Protractor, and WebdriverIO. WebDriver can be used by any programming language for which it has bindings such as Java, Javascript, C#, Ruby and Python, and as part of any test framework such as JUnit and Nunit, Mocha, Cucumber or pytest.

Be careful with words though.

Webdriver is not a test framework itself, but rather an interface that can be used by frameworks. For example, I could write pytest methods in Python that make calls to Selenium WebDriver.

When writing test automation code, you as the programmer must handle your own locators.

Let's write a basic test together in Java using JUnit and Selenium Webdriver to see it all come together.

Our test will be a simple DuckDuckGo search. Simply go to the DuckDuckGo home page, search for a phrase and make sure the results page shows results related to the search phrase.

Here I already have a Java project with a test stub named SimpleWebTest.

Get the Code for this Lesson on Github

There are setup and cleanup methods for handling the WebDriver. Every time I write a new test, I start at the top with my steps and progress down into the code.

First, we'll need to load the DuckDuckGo home page, and that's a simple WebDriver call like this.

// Load the page
driver.get("https://www.duckduckgo.com");
1
2

Next, I need to find the elements for the search bar so I can enter the search phrase and click the search button.

I'll switch over to the DuckDuckGo home page and use Chrome DevTools to find the elements I need. Here's the input search bar. Luckily it has both an ID and a name. I'm going to use the name simply because it's shorter.

Back over to my Java code. I can write WebDriver calls like this:

WebElement searchInput = driver.findElement(By.name("q"));
1

And once I add the element, I can search for whatever I want.

wait.until(ExpectedConditions.visibilityOfElementLocated(By.name("q")));

WebElement searchInput = driver.findElement(By.name("q"));

searchInput.sendKeys("giant panda");
1
2
3
4
5

This will wait to get the element until the element has appeared on the page.

Now, I need to click the search button back over to the DuckDuckGo page. I want to find this little hourglass input button, here it has an ID, "search_button_homepage". Nice!

Back over to my Java code, I add the following:

// Click search button

WebElement searchButton = driver.findElement(By.id("search_button_homepage"));

searchButton.click();
1
2
3
4
5

Now, even though we have these two elements, there is actually an important step I forgot when I grabbed the locators. I didn't verify that these two were actually unique. So let's do that now, real quick so we don't get into trouble.

I'll switch over to DuckDuckGo home page again and I've got my Chrome DevTools. Let's make sure that that name Q is unique. I'll use a CSS selector name equals Q, and sure enough, that's the only one on the page.

[name= 'q']
1

I'll also double check my id via CSS selector:

#search_button_homepage
1

And again, one of one returned, I should be good to go.

After entering the search. I'll need to check the results on the results page.

However, I need to wait for the results page to load before I can get the links on it to make sure that each one contains the word "panda" to match our search phrase.

I'm going to do some more expected conditions


wait.until(ExpectedConditions.titleContains("giant panda"));

1
2
3

The next thing I want to wait for are the links themselves, so I'll need to go get a locator for those.

Back at DuckDuckGo, if I answer my search phrase, I can use Chrome DevTools to find these link elements and they're here. What I notice is that they are inside of this div for the different results bodies and then down as a descendant there's the hyperlink with the class of result a.

So, in this case I'll want to use a CSS selector to make sure I get the right elements. I'll grab the class name here. That was for a div element and then under that I want to make sure I get the proper result link and boom, this CSS selector here returns those 10 links that I need.

So back over to my Java code, putting it all together, this set of wait conditions looks like this:

// Wait for results to appear

wait.until(ExpectedConditions.titleContains("giant panda"));

wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("div.results_links_deep a.result__a")));
1
2
3
4
5

Once the results page displays all the results links, I'll want to check the text of each link to make sure it contains my search phrase.

In order to do that, I'll need to get a list of web element objects because there will be more than one in my page.

I'll say:

// Make sure each result contains the word "panda"

List<WebElement> resultLinks = driver.findElements(By.cssSelector("div.results_links_deep a.result__a"));

for (WebElement link : resultLinks) {
    assertTrue(link.getText().matches("(?i).*panda.*"));
}

1
2
3
4
5
6
7
8

Once I have the link objects, I can iterate through each one and do an assertion based on text matching. So, for each web element in my result links, assert true at the link get text matches a regular expression ignoring case for the phrase "panda".

NOTE: The complete code for this lesson, all put together, can be found below in the resources section.

Now that our search test is complete. Let's run it to make sure it works. Everything here is automated and bam, look at that passing test.

Although this code works, it does not follow best practices.

Take a look at these locators. This By.name locator for the search input is duplicated and so is the CSS selector for our results. Furthermore, the WebDriver calls are fairly low level and not very much of this is self-documenting.

The best practice for locators regardless of the language of the framework is to separate them as a concern. Locators will naturally cluster around related elements. Test for a feature, will likely use those clusters of locators repeatedly. Therefore, it makes sense to group locators together somewhere in the code, most likely in their own class or module so they can most easily be reused.

Arguably, the most popular design pattern for modeling Web UI interactions for test automation is called the page object model.

Using page object model a web page is modeled as a class.

For example, here's a page object that models the DuckDuckGo home page.


package pageobjects;

import org.openqa.selenium.By;

import org.openqa.selenium.WebDriver;

import org.openqa.selenium.support.ui.ExpectedConditions;

public class DuckDuckGoSearchPage extends AbstractPage {

    public final static By searchInput = By.name("q");

    public final static By searchButton = By.id("search_button_homepage");

    public DuckDuckGoSearchPage(WebDriver driver) {

        super(driver);

    }

    public void loadPage() {

        getDriver().get("https://www.duckduckgo.com");

    }

    public void search(String phrase) {

        getWait().until(ExpectedConditions.visibilityOfElementLocated(searchInput));

        getDriver().findElement(searchInput).sendKeys(phrase);

        getDriver().findElement(searchButton).click();

    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

The page object class contains a locator object for each element needed for testing. These should look familiar. Our search input by name and our search button by ID. Each locator is given that intuitive name. They should also be treated as immutable though sometimes it might be helpful to write a builder method for a locator in case the locator needs some sort of parameters.

The page object class also contains what we call action methods that use the locators to interact with the page under test. Here we can see our loadPage method loads the white page by URL and the search method not only waits for the search input to appear, but then gets the search input, sends the phrase and then clicks the search button all within one call.

These intuitive names make actions self-documenting.

Also, it's fairly common practice to have a base page like our abstract page here.


package pageobjects;

import org.openqa.selenium.WebDriver;

import org.openqa.selenium.support.ui.WebDriverWait;

public abstract class AbstractPage {

    private final static int TIMEOUT = 30;

    private WebDriver driver;

    private WebDriverWait wait;

    public AbstractPage(WebDriver driver) {

        this.driver = driver;

        this.wait = new WebDriverWait(driver, TIMEOUT);

    }

    public WebDriver getDriver() {

        return driver;

    }

    public WebDriverWait getWait() {

        return wait;

    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

This just provides some basic attributes to be shared by all pages. If we rewrite our test using page objects, we can see how much more readable and reusable the code becomes.

For larger test automation projects. I strongly recommend the screenplay pattern, which can be seen as a solid refactoring of the page object model.


Automation Bookstore

The screenplay pattern further separates concerns by splitting locators and interactions into separate classes for even greater reusability and composability. Checkout frameworks like Serenity BDD, and Serenity JS to learn more about the screenplay pattern.

You should always treat test automation code like living code.

Whenever the page under test changes it's locators, most likely we'll also need to be changed. This maintenance overhead is unfortunately unavoidable because testing is dependent upon the product. When locators are treated as separate concerns however, like with the aforementioned design patterns, then maintenance is straight forward. Just update the locators in one place.

Some tools and frameworks, like the new Selenium IDE, have a nifty feature whereby an element can have multiple locators for failover in case some locators don't work in the future. Other tools like Mabl and Testim claim to use AI to make locators "self-healing" whenever UI changes break tests.

You can also check out the Test Automation University course named AI for Element Selection, Erasing the Pain of Fragile Test Scripts, which teaches how to use AI for finding visual locators.

Regardless of the tool used locator maintenance will be inevitable.



Resources