Now that we know all about locators, let’s use locators for testing.
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. Most of the codeless tools I know are built by software companies and charge fees to use, but one that is free and open source is Selenium IDE.
“Coded” automation tools are test automation frameworks and programming packages. To use them, you will need good software development skills. There are several options available these days for coded Web UI testing.
The “big three” these days are:
There are other great tools as well, such as WebdriverIO, Robot Framework, Puppeteer, TestCafe, and Nightwatch. For a while, Protractor was hot, but now it’s deprecated. For mobile automation, there are Appium, Espresso, and XCUI. All these tools work in slightly different ways and span different programming languages.
There are even “semi-codeless” tools that expect a little bit of programming sensibilities but handle a lot of implementation details for you. A good example of this is the Cycle platform from Cycle Labs, which provides out-of-the-box Gherkin steps for end-to-end testing across different kinds of systems.
Whatever kind of tool you pick, they all will need to find elements using locators, and usually, that means you, as the tester, must figure them out.
Let’s write a basic test together in Java using JUnit and Selenium WebDriver to see it all come together. Even though this code will use Selenium with Java, the principles apply for any language and browser automation tool.
Example code:
https://github.com/AndyLPK247/tau-locators-java-webdriver
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");
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"));
And once I add the element, I can say searchInput
, sendKeys
, whatever phrase I want. I'm also going to add a wait
condition.
// Enter search phrase
wait.until(ExpectedConditions.visibilityOfElementLocated(By.name("q")));
WebElement searchInput = driver.findElement(By.name("q"));
searchInput.sendKeys("giant panda");
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();
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’]
I'll also double check my id
via CSS selector:
#search_button_homepage
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"));
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")));
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.*"));
}
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, search 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();
}
}
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;
}
}
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.
Page objects are okay for small-scale test automation projects, but for larger projects, I strongly recommend the Screenplay Pattern. Screenplay is like a SOLID refactoring of page objects. The Screenplay Pattern separates concerns by splitting locators and interactions into separate classes for even greater reusability and composability.
There are a few major Screenplay implementations available:
I’m partial to Boa Constrictor – that’s my open source project!
Whatever pattern you use, you should always treat test automation code like living code. Whenever the page under test changes, its locators most likely will 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, like with the aforementioned design patterns, then maintenance is straightforward: just update the locators in one place. Some tools and frameworks, like Selenium IDE, have a nifty feature whereby an element can have multiple locators for failover, in case some locators don’t work. Other tools use AI to make locators “self-healing” whenever UI changes break tests. Regardless of the tool used, locator maintenance will be inevitable.
Well everyone, that’s the end of the course! Thank you so much for completing it.
Again, my name is Andy Knight, and I’m the Automation Panda. Be sure to read my blog and connect with me on Twitter and LinkedIn.
Web element locators can be troublesome, but now you have the strategies to make them work for you. Have fun automating your tests!