TSM - Object modeling of Selenium tests

Corina Pip - Senior QA Engineer

Usually, a test written in Selenium, Java and TestNG is meant to check the accuracy of items on a web page or that of a module on a web page. The classical approach of such a type of testing is represented by the high number of asserts, to compare all the desired properties to their expected values.

This approach has plenty of drawbacks, among which we could mention the difficult maintenance of tests, the waste of code-lines, lack of intelligibility. In order to avoid these drawbacks, a different approach of this type of tests is represented by the conception of tests based on the comparison of some objects.

Case study

Suppose the tested page or module is represented by a shopping cart, displayed on a shopping site. When shopping has been done, the cart contains a number of products. Each product, as shown on the page, contains: a label of its name, a description, an image, the price per product, the quantity of this product that has been put in the cart, the total price for this product (price per product * quantity) and a button by which the product can be taken out of the cart. Besides the purchased products, the cart also has: its own label, the total price of products, a link to continue shopping and a button for moving on to the next step, that of payment. The page may also contain other modules, such as a side module of suggestions for further shopping. This would be a minimal module, containing just a few products for which they would only display a name label, an image and its price.

The shopping cart would look like the one in the picture.

After adding all the desired products, the cart is ready to be tested in order to find out whether the information it displays is correct: the products shown are the ones that have been bought, each product is there in the desired quantity, the payment details are correct, etc.

Usually, the test created to check all these features of the cart would be a sequence of asserts. Even if someone wrote a method apart from the testing one, only for the checking through asserts (which would be used in several tests afterwards), the respective code lines would still be difficult to maintain and would not be efficiently written. In order to avoid the writing of these bushy and not too friendly tests, they can be object designed, as follows.

The Solution

Analysis

First, one should consider the overall image. What does the shopping cart page represent? A collection of different types of objects. Following the complex to simple structure, one can notice that the most complex object (the one containing all the rest) is the shopping cart. Its features are: the label, the list of purchased goods, a link, a button and a side module. Among the features, the label is represented in Java by a String, from the point of view of the test. The price can also be represented by a String. The list of products is actually a list containing "product" type objects. The displayed link is also an object, and so is the button or the side module.

Following the identification of the highest level objects, one can describe the "ShoppingCart" object as follows:

public class ShoppingCart {
   private String title;
   private List productList;
   private String totalPrice;
   private Button paymentButton;
   private Link shopMoreLink;
   private SuggestionsModule suggestionsModule;
}

Continuing the in depth analysis of the structure of objects, one can notice that: a product contains (or has as features) - an image (meaning another type of object), a label representing its name (a String), a descriptive text (another String), the price per piece (a String), the amount (a String), the total price of the product (a String) and a button (which represents an object that has already been mentioned as a feature of the cart). The object representation of the product can be done, according to the analysis, as follows:

public class Product {
   private String productLabel;
   private String productDescription;
   private Image image;
   private String pricePerItem;
   private String quantity;
   private String totalPricePerProduct;
   private Button removeButton;
}

The link mentioned within the cart may include, as a minimal set of properties, a label (a String, the text seen by the user on the display) and the URL which opens by clicking on the link (a String). Its object representation can be done like this:

public class Link {
  private String linkLabel;
  private String linkURL;
}

According to this logic, one can identify all the features of all the objects displayed on the page and these objects can be built by breaking them until they are decomposed to features that are Java objects or primitives.

Constructing the expected content

After completing the object structuring of the shopping cart, one has to understand how they will be used in tests. The first part of the test (or the part done before the test) is represented by adding the products in the cart. The test only has to check whether there are the right products in the cart.

In order to build the "shopping cart" object expected by the test (the expected content), one must create the constructor that assigns values of the specific features type to all its features. Thus, one passes to the constructor parameters which correspond to the features of the object, having the type of these features (for example, for a String type feature, a String is passed in the constructor; for an int, an int parameter is passed).

Starting from the simplest object, constructors are created. For the Link:

public Link(String linkLabel, String linkURL) {
  this.linkLabel = linkLabel;
  this.linkURL = linkURL;
}

It can be noticed here that the Link object has a label and an URL, both of the String type, whose value is instanced with the values received from the parameters of the constructor.

For the product, the following constructor is generated:

public Product(String productLabel, String productDescription, Image image, String pricePerItem, String quantity, String totalPricePerProduct, Button removeButton) {
   this.productLabel = productLabel;
   this.productDescription = productDescription;
   this.image = image;
   this.pricePerItem = pricePerItem;
   this.quantity = quantity;
   this.totalPricePerProduct = totalPricePerProduct;
   this.removeButton = removeButton;
}

For the shopping cart, the constructor looks like this:

public ShoppingCart(String title, List productList, String totalPrice, Button paymentButton, Link shopMoreLink, SuggestionsModule suggestionsModule) {
   this.title = title;
   this.productList = productList;
   this.totalPrice = totalPrice;
   this.paymentButton = paymentButton;
   this.shopMoreLink = shopMoreLink;
   this.suggestionsModule = suggestionsModule;
}

Based on the constructor, the following objects will be generated (those that will serve as "expected"), by passing to the constructor some values having the type of parameters which these will be assigned to:

public 	static final Link CONTINUE_SHOPPING_LINK = 
  new Link("Continue 	shopping", 
   "http://continue.shopping.com");
public 	static final Button GO_TO_PAYMENT_BUTTON = 
  new Button("Proceed 	to payment", 
   "http://some.url.com");
public static final Product LATTE_MACHIATTO_2 = 
 new Product("Latte Machiato", 
 "Classic latte machiato with a dash of cocoa on top", 
 Image.IMAGE_LATTE_MACHIATO, 
 "5 Ron", 
 "2", 
 "10 RON",
 Button.REMOVE_PRODUCT);
here, the image and button type objects were constructed by using the constructors specific to those objects

public 	static final Product CHOCO_FRAPPE_3 = 
  new Product("Choco-whip 	Frappe",
  "Frappe with a twist of whipped cream and chocolate syrup", 
  Image.IMAGE_CHOCO_FRAPPE, 
  "5 Ron",
  "3", 
  "15 RON", 
  Button.REMOVE_PRODUCT);
here, the image and button type objects were constructed by using the constructors specific to those objects.

public 	static final Product CHOCO_FRAPPE_3 = 
  new Product("Choco-whip 	Frappe",
  "Frappe with a twist of whipped cream and chocolate syrup", 
  Image.IMAGE_CHOCO_FRAPPE, 
  "5 Ron",
  "3", 
  "15 RON", 
  Button.REMOVE_PRODUCT);
here, the image and button type objects were constructed by using the constructors specific to those objects.

public static final ShoppingCart SHOPPING_CART = 
  new ShoppingCart("My Shopping Cart",
   ImmutableList.of(Product.LATTE_MACHIATTO_2,
     Product.CHOCO_FRAPPE_3, 
     Product.CARAMEL_MOCCACHINO_1),
   "30 RON",
   Button.GO_TO_PAYMENT_BUTTON, 
   Link.CONTINUE_SHOPPING_LINK, 
   SuggestionsModule.SUGGESTIONS_MODULE);
→ here, the object of the type "suggestions module" was constructed by using its specific constructor, and the list of products, the button and the link were exemplified above the line where the "shopping cart" object is constructed.

Constructing the actual content

To construct the actual object, namely to read the features of the objects directly from the page where they are displayed, a new constructor will be generated in each object, which takes as parameters either a WebElement or a list of WebElements, as many as necessary to generate the features of the object. WebElements represent the description of HTML elements in the Selenium specific format.

As an example, for the link object: the two features, the associated label and URL can be deduced in one single WebElement. A link type element is represented from the point of view of the HTML as an tag, having a "href" attribute (by the extraction of which, the URL is identified). By calling the getText() method of the Selenium library directly on the "a" element, the value of the label is obtained. Thus, the constructor based on the WebElement is described below and it instances the features of the object, extracting them from the corresponding HTML element:

public Link(WebElement element) {
  this.linkLabel = element.getText();
  this.linkURL = element.getAttribute("href");
}

For the construction of the actual object corresponding to a product, depending on the number of webElements necessary to obtain all the features, we will define a constructor which takes as a parameter either a webElement or a list of webElements. Supposing there is just a single element used, the constructor will look like this (for instance):

public Product(WebElement element) {
  this.productLabel = element.findElement(
    By.cssSelector(someSelectorHere)).getText();
  
this.productDescription = element.findElement(
    By.cssSelector(someOtherSelectorHere)).getText();
  
this.image = new Image(element);
this.pricePerItem = element.findElement(
    By.cssSelector(anotherSelectorHere)).getText();
  
this.quantity = element.findElement(
    By.cssSelector(yetAnotherSelectorHere)).getText();
  
this.totalPricePerProduct = element.findElement(
    By.cssSelector(aSelectorHere)).getText();

this.removeButton = new Button(element);
}

It can be noticed that in the case of the product, in order to generate some features, we called the constructors of the corresponding type objects, namely the constructors that also take webElements as parameters. Basically, any constructor based on webElements calls only constructors having webElement type parameters for initiating its features. The features left are instanced according to the parameter given to the constructor. For instance, for the product Label, the getText() method of Selenium is called on an element relative to the element passed in the constructor.

After the definition of all the constructors based on webElements, one can also generate the constructor for the most complex of them, the shopping cart:

public ShoppingCart(List webElementList) {
  this.title = webElementList.get(0).getText;
  
this.productList = productList;
  this.totalPrice = webElementList.get(2).getText;
  this.paymentButton = new Button(webElementList.get(3));
  this.shopMoreLink = new Link(webElementList.get(4));
  this.suggestionsModule = suggestionsModule;
}

The Test

Following the definition of the objects and constructors, one can move on to the test writing step. The requirement of the test was to compare the shopping cart to an "expected" one, that is, to compare all the features of the shopping cart to those of the expected cart. These features are, in their turn, objects, so their features too should be compared to the features of some expected objects. Since the expected values were constructed by means of the first type constructor (the one with parameters having the type of features that are being instanced), and there is a constructor for the generation of the actual content (through the interpretation of the features of some webElements), the test that must be written contains one single assert. It will compare the expected features to the actual ones, by simply comparing the two objects. We should mention that, along with the definition of objects, one must also implement within each the equals() method - the one that verifies whether two objects are equal or not.

Thus, the test can be written as follows:

@Test
public void checkMyShoppingCart() {
  assertEquals(new ShoppingCart(theListOfWebElements), 
  SHOPPING_CART, 
 "There was an incorrect value in the shopping cart");
}

Of course, this test does not describe the steps necessary to the construction of the shopping cart (surfing on the shopping site and adding the products). These steps can be made within the test, if necessary, or in "@Before" type methods.

In case you want, for instance, the typing of the same content, but in different languages, you can pass a dataProvider to the test, which contains a parameter used in the test to change the language, as well as the expected value of the test in the respective language. In this case, the test will be the following:

@Test(dataProvider = "theDataProvider")
public void checkMyShoppingCart(
  String theLanguage, 
  ShoppingCart actualShoppingCartValuePerLanguage) {

    changeTheLanguageOnTheSite(theLanguage);
    assertEquals(new ShoppingCart(
      theListOfWebElements), 
      actualShoppingCartValuePerLanguage,
 "There was an incorrect value in the shopping cart");
}

The DataProvider used in this test will look like in the example below:

@DataProvider
public Object[][] theDataProvider() {
return new Object[][]{
   { "english", SHOPPING_CART},
   { "german", SHOPPING_CART_GERMAN },
   { "spanish", SHOPPING_CART_SPANISH}
};
}

Thus, suppose the shopping site is available in 20 languages, the translated test that checks the accurate display of the shopping cart will have a reduced number of code lines and it will be written only once, being, however, run on all the existing languages.

Benefits

The manner of writing tests by comparing the objects generated from webElements to those generated from objects and primitives has numerous benefits. Firstly, the test is a very short one, with a well-defined purpose, doing one thing only: comparing the actual values to the expected ones. The test in itself does not require a lot of maintenance, since following the changes in the page accessed by users, it is not the test that has to be altered, but the manner in which the compared features are generated. The alteration of the value of a label on the page only requires the alteration of the expected value corresponding to an object. This alteration is made in one single place, but a great number of tests benefit from it. Thus, the testing part is separated from the part of generating the expected values.

Another benefit is represented by the compact structure of the test, as it is not necessary to write numerous asserts, nor to pass many parameters to a method that has to check those values. Instead of those numerous parameters, one directly passes the object containing the features to be compared.