Testing Websites with Dynamic Data with Selenium and XPath

This tutorial goes over how to test a website that uses dynamic data that can’t be known beforehand. We will be using the Bloomberg currency prices site which displays real time forex conversions.

Refer to a previous tutorial for how to setup Visual Studio Code with Selenium and Java and how to setup data driven test with TestNG and Selenium.

Bloomberg Currencies Site

The site we’ll be testing shows the real time currency prices as well as the real time changes in pips and percentages. When a pip change is positive from the previous price, the “change” and “net change” columns are highlighted in green. When a pip change is negative, the two columns are highlighted in red. The purpose of the test is to test that these two columns are working properly by testing that the color changes are correct (green for positive changes, red for negative changes).

Test Analysis – Determining Table Cell Colors

We need a way of checking that the background colors are correct for the Change and Net Change columns without visually seeing them. We can do this by examining CSS class values.

We start by inspecting a cell in the “change” column. We can do this by right clicking on the cell and selecting “Inspect” in Chrome to bring up Chrome Developer Tools.

The inspector shows us the HTML for the first table cell value for the Change column:

<span class="data-table-row-cell__value">0.0004</span>

If we move up to highlight the entire table cell (not just the value), we get HTML like this:

<td class="data-table-row-cell data-table-row-cell__next-value_up" 
  data-type="better" aria-label="better" next-value="+0.03%">
  <span class="data-table-row-cell__value">0.0004</span>
</td>

Now we know all the CSS attributes of each Change table cell. It’s now time to determine which one of them affects the green background color.

We’ll next inspect the HTML of a Change table cell value that is red and then compare it to the HTML of the green table cell to look for differences that we can test. Inspecting the AUD-USD currency row, which has a red Change table cell, we get the following HTML:

<td class="data-table-row-cell data-table-row-cell__next-value_down" 
  data-type="worse" aria-label="worse" next-value="-0.50%">
  <span class="data-table-row-cell__value">-0.0038</span>
</td>

We can see that the following CSS attributes are different between the two table cells:

  • class: data-table-row-cell__next-value_up / data-table-row-cell__next-value_down
  • data-type: better / worse
  • aria-label: better / worse

It’s now a matter of determining which combination of these CSS attributes has an affect on the color of the table cell. We can test this by changing the values of these CSS attributes directly in Chrome Developer Tools. By testing out the different combinations, we can see that it’s the data-type CSS class that affects the background. When this value is change from “better” to “worse”, the table cell changes from green to red:

We can use this information when writing our test to check that the table cell has the correct color.

Next we need to determine the same thing for the “Net Change” column and we repeat the process by right clicking a green table cell and selecting “Inspect”. We copy the outter HTML and get this:

<td class="data-table-row-cell hide-on-mobile" 
  data-type="better" aria-label="better">
  <span class="data-table-row-cell__value">+0.09%</span>
</td>

We can see the same CSS data-type class with the value “better” which we can change to “worse” to test that the cell changes to red, which it does.

We can use this as the basis for our tests by:

  • selecting all the green table cells and then asserting that the cell values are positive
  • selecting all the red table cells and then asserting that the cell values are negative

Test Analysis – Selecting Table Cells with XPath

We can use XPath to help select all the table cell elements for the Change and Net Change columns so we can test that the background colors are correct. When we right click on a table cell and select “Inspect” that will bring up Chrome Developer Tools. Pressing CTRL+F or Command+F will bring up the XPath selector console where we can test our XPath expressions.

By testing different XPath expressions we can see that this XPath expression selects all the green cells in the Change and Net Change columns:

//*[@data-type="better"]

Next we select all the red cells in the table with the following XPath expression:

//*[@data-type="worse"]

This approach will make our test more adaptable to changes so that even if new columns are added / removed or if the column order is changed, we will still find all the green and red table cells.

Now that we know how to select all the green and red table cells, we’re ready to write our tests.

Selenium Code

We will start by extracting all the green and red table cells and printing out their value. This will tell us that our test code is indeed finding the correct elements.


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
import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

public class BloombergCurrencyTests {
  WebDriver webDriver;
  @BeforeSuite
  public void setUpOnce() {
    System.setProperty("webdriver.chrome.driver", "/Users/misha/dev/selenium/bin/chromedriver");
    webDriver = new ChromeDriver();
  }

  @Test
  public void testCurrencies() {
    webDriver.get("https://www.bloomberg.com/markets/currencies");
    List greenWebElementList = webDriver.findElements(By.xpath("//*[@data-type=\"better\"]"));
    List redWebElementList = webDriver.findElements(By.xpath("//*[@data-type=\"worse\"]"));

    for (WebElement greenWebElement : greenWebElementList) {
      String greenCellText = greenWebElement.getText();
      System.out.println("greenCellText: " + greenCellText);    
    }

    for (WebElement redWebElement : redWebElementList) {
      String redCellText = redWebElement.getText();
      System.out.println("redCellText: " + redCellText);
    }
  }
}

This results in the following output:

greenCellText: 0.0016
greenCellText: +0.13%
greenCellText: 0.0600
greenCellText: +0.05%
greenCellText: 0.0025
greenCellText: +0.20%
greenCellText: 0.2600
greenCellText: +0.20%
greenCellText: 0.0012
greenCellText: +0.14%
greenCellText: 0.0092
greenCellText: +0.12%
redCellText: -0.0002
redCellText: -0.01%
redCellText: -0.0032
redCellText: -0.42%
redCellText: -0.0018
redCellText: -0.19%
redCellText: -0.0006
redCellText: -0.06%
redCellText: -3.3700
redCellText: -0.30%

===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================

We can see that all green cells have positive values while the red cells have negative values. Now it’s just a matter of parsing the text to determine if the cell values are positive or negative.

We can convert the strings to floats so we can check if the values are greater than or less than 0. However, this would require us to strip out the “+”, “-” and “%” characters because they will cause a NumberFormatException when using Float.parseFloat(String).

Another approach would be to check for the first character in the text. If it’s “-” we know the value is negative and if it’s “+” or “0” we know it’s positive. If the first character is any other value, the test will fail.

This is the final Selenium code for our test:


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
import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

public class BloombergCurrencyTests {
  WebDriver webDriver;
  @BeforeSuite
  public void setUpOnce() {
    System.setProperty("webdriver.chrome.driver", "/Users/misha/dev/selenium/bin/chromedriver");
    webDriver = new ChromeDriver();
  }

  @Test
  public void testCurrencies() {
    webDriver.get("https://www.bloomberg.com/markets/currencies");
    List greenWebElementList = webDriver.findElements(By.xpath("//*[@data-type=\"better\"]"));
    List redWebElementList = webDriver.findElements(By.xpath("//*[@data-type=\"worse\"]"));

    for (WebElement greenWebElement : greenWebElementList) {
      String greenCellText = greenWebElement.getText();
      boolean isPositive = greenCellText.startsWith("+") || greenCellText.startsWith("0");
      Assert.assertTrue(isPositive, "green cell text: " + greenCellText);
    }

    for (WebElement redWebElement : redWebElementList) {
      String redCellText = redWebElement.getText();
      boolean isNegative = redCellText.startsWith("-");
      Assert.assertTrue(isNegative, "red cell text: " + redCellText);
    }
  }
}

Improving Our Solution

We can improve our solution by:

  • making the test code look for green and red cell values ONLY for the Change and Net Change columns, no matter the column order in the table
  • give more detailed failure message about the exact row and column that failed an assertion, so it would be easier to find which table cell failed a test

To find the correct column index for the Change and Net Change columns, we first need to extract the table header row. Inspecting the cells on the table header row, we can see that each cell is using a “data-table-headers-cell” class. We get all the table header cells using the following XPath in Chrome Developer Tools:

//*[@class="data-table-headers-cell"]

Using this information, we can update our test as follows to extract all the correct column indices for table rows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  //figure out the column indices of Change and Net Change columns
  //by extracting the header row and seeing what column index these 2 columns have
  List <WebElement> headerWebElements = webDriver.findElements(By.className("data-table-headers-cell"));
  for(int i=0; i<headerWebElements.size(); i++) {
    WebElement headerWebElement = headerWebElements.get(i);
    if(CHANGE.equals(headerWebElement.getText())) {
      changeColumnIndex = i;
    }
    else if(NET_CHANGE.equals(headerWebElement.getText())) {
      netChangeColumnIndex = i;
    }
  }
  //make sure indices were set
  Assert.assertTrue(changeColumnIndex >= 0);
  Assert.assertTrue(netChangeColumnIndex >= 0);

Next we extract all the table data rows and look for the correct column index to check the Change and Net Change columns. Inspecting the table rows with Chrome Developer Tools, we can see that each row uses a “data-table-row” class. The XPath expression that selects each table row is:

//*[@class="data-table-row"]

Now that we extracted each table data row, we need to extract just the column values we are interested in. The problem, though, is that not all columns have the same tag name or class. For example the Currency column uses a <th> tag while the other columns use a <td> tag. We can get around this problem by getting all the child elements of each row and then going to the correct column index.

Getting all the child elements or each data row can be done with the following XPath:

child::*

Once we have extracted the correct columns, we need to check that they are green if the value is positive and red if the value is negative. We already know that data-type class value of “better” should be green with a positive value and a value of “worse” should be red with a negative value.

We can create a helper method to check for this. The helper method will also print the specific row and column where a failure occurs so we can quickly see which table cell value failed our test failed:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  private void checkColor(WebElement pWebElement, int pRowIndex, String pColumn) {
    String dataType = pWebElement.getAttribute("data-type");
    String cellText = pWebElement.getText();
    //matches any positive number that can begin with a "+" or a positive digit
    boolean isPositive = cellText.startsWith("+") || Character.isDigit(cellText.charAt(0));
    boolean isNegative = cellText.startsWith("-");

    //should be positive value for green
    if(dataType.equals("better")) {
      Assert.assertTrue(isPositive, "expected positive: " + cellText + " Row: " + pRowIndex + " Column: " + pColumn);
    }
    //should be negative value for red
    else if(dataType.equals("worse")) {
      Assert.assertTrue(isNegative, "expected negative: " + cellText + " Row: " + pRowIndex + " Column: " + pColumn);
    }
    else {
      Assert.fail("invalid data type: " + dataType);
    }
  }

We can put all the code together like this:


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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

public class BloombergCurrencyTests {
  WebDriver webDriver;
  @BeforeSuite
  public void setUpOnce() {
    System.setProperty("webdriver.chrome.driver", "/Users/misha/dev/selenium/bin/chromedriver");
    webDriver = new ChromeDriver();
  }

  @Test
  public void testCurrencies() {
    final String CHANGE = "CHANGE";
    final String NET_CHANGE = "NET CHANGE";

    int changeColumnIndex = -1;
    int netChangeColumnIndex = -1;

    webDriver.get("https://www.bloomberg.com/markets/currencies");

    //figure out the column indices of Change and Net Change columns
    //by extracting the header row and seeing what column index these 2 columns have
    List  headerWebElements = webDriver.findElements(By.className("data-table-headers-cell"));
    for(int i=0; i<headerWebElements.size(); i++) {
      WebElement headerWebElement = headerWebElements.get(i);
      if(CHANGE.equals(headerWebElement.getText())) {
        changeColumnIndex = i;
      }
      else if(NET_CHANGE.equals(headerWebElement.getText())) {
        netChangeColumnIndex = i;
      }
    }
   
    //make sure indices were set
    Assert.assertTrue(changeColumnIndex >= 0);
    Assert.assertTrue(netChangeColumnIndex >= 0);

    //extract all the data rows of the table
    List  dataRowWebElements = webDriver.findElements(By.className("data-table-row"));

    //go through each data row and check the color of the Change and Net Change columns
    for(int i=0; i<dataRowWebElements.size(); i++) {
      List dataColumnsWebElement = dataRowWebElements.get(i).findElements(By.xpath("child::*"));
      WebElement changeWebElement = dataColumnsWebElement.get(changeColumnIndex);
      WebElement netChangeWebElement = dataColumnsWebElement.get(netChangeColumnIndex);

      checkColor(changeWebElement, i, CHANGE);
      checkColor(netChangeWebElement, i, NET_CHANGE);
    }
  }

  private void checkColor(WebElement pWebElement, int pRowIndex, String pColumn) {
    String dataType = pWebElement.getAttribute("data-type");
    String cellText = pWebElement.getText();
    //matches any positive number that can begin with a "+" or a positive digit
    boolean isPositive = cellText.startsWith("+") || Character.isDigit(cellText.charAt(0));
    boolean isNegative = cellText.startsWith("-");

    //should be positive value for green
    if(dataType.equals("better")) {
      Assert.assertTrue(isPositive, "expected positive: " + cellText + " Row: " + pRowIndex + " Column: " + pColumn);
    }
    //should be negative value for red
    else if(dataType.equals("worse")) {
      Assert.assertTrue(isNegative, "expected negative: " + cellText + " Row: " + pRowIndex + " Column: " + pColumn);
    }
    else {
      Assert.fail("invalid data type: " + dataType);
    }
  }
}