How good is your Selenium Java code?

code-review

 

You probably know who Martin Fowler is.

He coined the term Page Object Model sometime in 2013:
https://martinfowler.com/bliki/PageObject.html

Yes, the page object model that you use in your Selenium test automation projects.

His contribution to software development goes way beyond the page object model since he wrote many useful books on topics such as code refactoring and design patterns.

He also said that

Any fool can write code that a computer can understand.
Good programmers write code that humans can understand.

This is what I want to focus on today, on the code written by good programmers.

Good programmers write code that other programmers can understand but also can change and maintain.

This is what we should aim at in our Selenium projects.

Our code should also be easy to understand, change and maintain.

 

How do you know if your code does all these?

What are the rules that your code should follow so that it is good?

Let’s see.

 

Write Clean Code

The code should be clean.

This means a lot of things.

1. The code should use clear names

Variables should have names that explain their purpose.
Methods should have names that either explain what the method does or the result returned by the method.

2. Use problem domain names

If you do automation for an e-commerce site, make sure that you have classes for concepts such as

  • product
  • user
  • basket
  • cart
  • order
  • invoice

3. Classes should be small

A class should contain an average of less than 30 methods.

4. Methods should be small

Methods should not have more than an average of 30 code lines.

5. Do one Thing

This applies to both classes and methods.
If a method does more than one thing, consider splitting it in 2.
If a class has more than one responsibility, consider breaking it in more classes.

6. Don’t Repeat Yourself 

Any duplication, inside a page object class, a page method, a test class or a test method should be avoided.

7. Explain yourself in code

Write code that is self-explanatory, that is so easy to understand so no comments are needed.

8. Make sure the code formatting is applied

Code formatted correctly is easier to read by other developers.

9. Use Exceptions rather than Return codes

If a method cannot fulfill its purpose, instead of returning an obscure error code,
throw an exception since the code is in an abnormal state.

10. Don’t return Null

There are many ways of avoiding returning null.
You can use Optional introduced in Java 8.
Or you can return an empty list.

Write Secure Code

1. Make class final if not being used for inheritance

Making the class final ensures that it is not extended.

All page classes and page element classes should be final.

2. Avoid duplication of code

3. Limit the accessibility of packages,classes, interfaces, methods, and fields

Parent classes (base page class, base test class) should be abstract.

All methods of a parent class should be declared as protected since they should only be used in the child classes.

Only a small number of methods of any class should be public.

If a class should be used only by other classes in the same package, use the default access modifier.

4. Validate inputs (for valid data, size, range, boundary conditions, etc)

Any public method should have its parameters checked for validity.

5. Avoid excessive logs

If you are logging tracing information for all page classes and their methods, when you run the whole suite of tests, you will get excessive logs that are very difficult to read.

This becomes even worse if the automated tests are run in parallel on different virtual machines.

6. Release resources (Streams, Connections, etc) in all cases

Consider using try/catch with releasing resources.

7. Purge sensitive information from exceptions (exposing file path, internals of the system, configuration)

8. Do not log highly sensitive information

9. Make public static fields final (to avoid caller changing the value)

10. Avoid exposing constructors of sensitive classes

Use a factory method instead of the constructor to create objects.

General Rules

1. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

2. Favor the use of standard exceptions

3. Don’t ignore exceptions

4. Check parameters for validity

5. Return empty arrays or collections, not nulls

6. Minimize the accessibility of classes and members

7. In public classes, use accessor methods, not public fields

All fields of a class should be private so that objects cannot be changed (stay imutable).
Set methods are bad for the same reason since they make an object mutable.

Get methods should be used to expose fields outside of the class.

8. Minimize the scope of local variables

Declare the variable just before you need it and not at the beginning of the method.

9. Refer to objects by their interfaces

Also called program to an interface.

10. Adhere to generally accepted naming conventions

11. Use enums instead of int constants

SortOrder.PriceAscending is always more clear than 1.

12. Beware the performance of string concatenation

Use StringBuilder class instead.

13. Avoid creating unnecessary objects

 

This is obviously an incomplete list.

But it should get you started on focusing more on the quality of your code.

Because, good programmers write good code and you want to be a good programmer.

Don’t you?

Advertisements

Extend, Dont Change

Keep-calm-and-don-t-change-7

The test automation code should be as professional as the application code.

It should be built using the same principles.

Such as “program to an interface” and “extend, dont change”.

 

How do you build it so that it follows such principles?

Let’s imagine that we have a class that formats a phone number with spaces between the area code, prefix and the line number.

For a phone number like 6143567891, the PhoneNumber class displays it as 614 356 7891.

import java.util.ArrayList;
import java.util.List;

public class PhoneNumber {
 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumber(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  }

  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  public String getValue() { 
    return String.format("%s %s %s", 
                         areaCode(), 
                         prefix(), 
                         lineNumber()); 
  } 

  public String areaCode() { 
    return String.format("%d%d%d", 
                         digits.get(0), 
                         digits.get(1), 
                         digits.get(2)); 
  } 

  public String prefix() { 
    return String.format("%d%d%d",   
                         digits.get(3), 
                         digits.get(4), 
                         digits.get(5)); 
  }

  public String lineNumber() { 
    return String.format("%d%d%d%d",   
                         digits.get(6), 
                         digits.get(7), 
                         digits.get(8), 
                         digits.get(9)); 
  } 

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

}

The following test displays a phone number formatted properly:

import java.util.Arrays;
import java.util.List;
import org.testng.annotations.Test;

public class TestClass {
  private final static List<Integer> TEN_DIGITS = 
          Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); 

  @Test 
  public void testNormalPhone() { 
    PhoneNumber phone = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone.getValue()); 
  } 
}

 

For a while, this phone template is the only template needed for the project.

Multiple classes use objects of the PhoneNumber class.

But things change and various other formats become useful such as:

(614) 356 7891 : the area code is in ()

614-356-7891 : the area code, prefix and line number are separated by –

604 356 7891 : the 604 prefix is used

778 356 7891 : the 778 prefix is used

 

How can the additional templates be implemented?

You may choose to make changes to the PhoneNumber class for the additional templates.

Since there is already a getValue() method used in many other classes, you leave it as is and create additional methods, one for each additional template:

import java.util.ArrayList;
import java.util.List;
 public class PhoneNumber { 

  private List<Integer> digits = new ArrayList<>(); 
  
  public PhoneNumber(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 

  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  public String getValue() { 
    return String.format("%s %s %s", 
                         areaCode(), 
                         prefix(), 
                         lineNumber()); 
  }
  public String getValueWithParanthesis() { 
   return String.format("(%s) %s %s", 
                        areaCode(), 
                        prefix(), 
                        lineNumber()); 
  } 

  public String getValueWithHyphens() { 
    return String.format("%s-%s-%s", 
                         areaCode(), 
                         prefix(), 
                         lineNumber()); 
  } 

  public String getValueWith604Prefix() { 
    return String.format("604 %s %s", 
                         prefix(), 
                         lineNumber()); 
  }
  public String getValueWith778Prefix() { 
    return String.format("778 %s %s", 
                         prefix(), 
                         lineNumber()); 
  } 

  public String areaCode() { 
    return String.format("%d%d%d", 
                         digits.get(0), 
                         digits.get(1), 
                         digits.get(2)); 
  } 

  public String prefix() { 
    return String.format("%d%d%d", 
                         digits.get(3), 
                         digits.get(4), 
                         digits.get(5)); 
  } 

  public String lineNumber() { 
    return String.format("%d%d%d%d", 
                        digits.get(6), 
                        digits.get(7), 
                        digits.get(8),  
                        digits.get(9)); 
  }
 
  @Override 
  public String toString() { 
    return this.digits.toString(); 
  } 
}

 

The test displays all possible formats:

import java.util.Arrays;
import java.util.List; import org.testng.annotations.Test; 
public class TestClass {
  private final static List<Integer> TEN_DIGITS = 
                Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); 

  @Test 
  public void testPhoneTemplates() { 

    PhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone1.getValue()); 

    PhoneNumber phone2 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone2.getValueWithParanthesis()); 

    PhoneNumber phone3 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone3.getValueWithHyphens()); 

    PhoneNumber phone4 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone4.getValueWith604Prefix()); 

    PhoneNumber phone5 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone5.getValueWith778Prefix()); 

  } 
}

 

This works.

But we just violated many principles that should be followed when writing code:

  • program to an interface

When creating a phone number object, it should be saved in a variable that uses an interface as type:

IPhoneNumber phoneNumber = new PhoneNumber();

Instead, we save the phone number object in a variable with the PhoneNumber type:

PhoneNumber phoneNumber = new PhoneNumber();
  • extend, not change

We changed the PhoneNumber class by adding 4 additional methods.

A class should not be changed since this may create issues in all other classes that use its objects.

  • single responsibility

The PhoneNumber class started with having one responsibility, to format a phone number.

Now, the class has 5 responsibilities by providing 5 types of formatting.

 


 

What do we do?

How do we change the code so all these 3 principles are followed?

 


 

  1. Create a IPhoneNumber interface

import java.util.List;
 public interface IPhoneNumber { 

   public String getValue(); 

   public List<Integer> getDigits(); 

   public String areaCode(); 

   public String prefix();
   public String lineNumber(); 

}

 

2. Create a class for each phone number template and  implement the IPhoneNumber interface

 

PhoneNumber class

import java.util.ArrayList;
import java.util.List; 
public class PhoneNumber implements IPhoneNumber { 

  private final String phoneTemplate = "%s %s %s"; 

  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumber(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 
  @Override
  public String getValue() { 
    return String.format(phoneTemplate,               
                         areaCode(),  
                         prefix(), 
                         lineNumber()  ); 
  } 

  @Override
  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  @Override
  public String areaCode() { 
    return String.format("%d%d%d",               
                         digits.get(0), 
                         digits.get(1),  
                         digits.get(2)); 
  } 

  @Override
  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3), 
                         digits.get(4), 
                         digits.get(5)); 
  } 

  @Override
  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6), 
                         digits.get(7),
                         digits.get(8),
                         digits.get(9)); 
  } 

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

}

 

PhoneNumberWithParanthesis class

import java.util.ArrayList;
import java.util.List; 
public class PhoneNumberWithParanthesis implements IPhoneNumber { 

  private final String phoneTemplate = "(%s) %s %s"; 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumberWithParanthesis(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 
  public String getValue() { 
    return String.format(phoneTemplate,               
                         areaCode(),  
                         prefix(), 
                         lineNumber()); 
  } 

  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  public String areaCode() { 
    return String.format("%d%d%d",               
                         digits.get(0),               
                         digits.get(1),               
                         digits.get(2)); 
  } 

  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3),               
                         digits.get(4),               
                         digits.get(5)); 
  } 

  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6),               
                         digits.get(7),               
                         digits.get(8),              
                         digits.get(9)); 
  } 

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

}

 

PhoneNumberWithHyphens class

import java.util.ArrayList;
import java.util.List; 
public class PhoneNumberWithHyphens implements IPhoneNumber { 

  private final String phoneTemplate = "%s-%s-%s"; 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumberWithHyphens(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 
  @Override
  public String getValue() { 
    return String.format(phoneTemplate,               
                         areaCode(),  
                         prefix(), 
                         lineNumber()); 
  } 

  @Override
  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  @Override
  public String areaCode() { 
    return String.format("%d%d%d",               
                         digits.get(0),               
                         digits.get(1),               
                         digits.get(2)); 
  } 

  @Override
  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3),               
                         digits.get(4),               
                         digits.get(5)); 
  } 

  @Override
  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6),               
                         digits.get(7),               
                         digits.get(8),              
                         digits.get(9)); 
  } 

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

 

PhoneNumberStartingWith604 class

import java.util.ArrayList;
import java.util.List;
 public class PhoneNumberStartingWith604 implements IPhoneNumber { 

  private final String phoneTemplate = "%s %s %s"; 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumberStartingWith604(List<Integer> digits) { 

    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 
  
    this.digits = digits; 
  }

  @Override   public String getValue() { 
    return String.format(phoneTemplate,               
                         areaCode(),  
                         prefix(), 
                         lineNumber()); 
  } 

  @Override
  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  @Override
  public String areaCode() { 
    return "604"; 
  } 

  @Override
  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3),               
                         digits.get(4),               
                         digits.get(5)); 
  } 

  @Override
  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6),               
                         digits.get(7),               
                         digits.get(8),              
                         digits.get(9)); 
  } 

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

}

 

PhoneNumberStartingWith778 class

import java.util.ArrayList;
import java.util.List;
 public class PhoneNumberStartingWith778 implements IPhoneNumber { 

  private final String phoneTemplate = "%s %s %s"; 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumberStartingWith778(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 
  @Override
  public String getValue() { 
    return String.format(phoneTemplate,               
                         areaCode(),  
                         prefix(), 
                         lineNumber()); 
  } 

  @Override
  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  @Override
  public String areaCode() { 
    return "778"; 
  } 

  @Override
  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3),               
                         digits.get(4),               
                         digits.get(5)); 
  } 

  @Override
  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6),               
                         digits.get(7),               
                         digits.get(8),              
                         digits.get(9)); 
  } 

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

}

 

The test displays all possible templates for a phone number:

import java.util.Arrays;
import java.util.List; import org.testng.annotations.Test; 
public class TestClass {
  private final static List<Integer> TEN_DIGITS = 
              Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); 

  @Test 
  public void testPhoneTemplates() { 

    IPhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone1.getValue()); 

    IPhoneNumber phone2 = new PhoneNumberWithParanthesis(TEN_DIGITS); 
    System.out.println(phone2.getValue()); 

    IPhoneNumber phone3 = new PhoneNumberWithHyphens(TEN_DIGITS); 
    System.out.println(phone3.getValue()); 

    IPhoneNumber phone4 = new PhoneNumberStartingWith604(TEN_DIGITS); 
    System.out.println(phone4.getValue()); 

    IPhoneNumber phone5 = new PhoneNumberStartingWith778(TEN_DIGITS); 
    System.out.println(phone5.getValue()); 
  } 
}

 

The code is better because

  • each class has one responsibility only (for a single phone template)
  • each class implements an interface; each phone object, even if created with a class, is saved in a variable with the IPhoneNumber interface

 

But, looking at all classes, it is easy to see that have so much duplicated code.

 


 

What do we do next?

A different approach is needed.

We started with a PhoneNumber class that formats a phone number using a template.

We would like not to modify this class but extend it so that we can format the phone number with other templates.

Decorators provide this behaviour.

A decorator allows extending the functionality of a class without making any changes to the class.

We need a few steps for the new implementation:

  1. We keep the same IPhoneNumber interface

import java.util.List; 
public interface IPhoneNumber { 

  public String getValue(); 

  public List<Integer> getDigits(); 

  public String areaCode(); 

  public String prefix(); 

  public String lineNumber(); 
}

 

2. We keep the PhoneNumber class in its original shape.

The PhoneNumber class implements the IPhoneNumber interface:

import java.util.ArrayList;
import java.util.List; 
public class PhoneNumber implements IPhoneNumber { 

  private final String phoneTemplate = "%s %s %s"; 
  private List<Integer> digits = new ArrayList<>(); 

  public PhoneNumber(List<Integer> digits) { 
    if (digits.size() != 10) 
      throw new RuntimeException("there are less than 10 digits!"); 

    this.digits = digits; 
  } 

  @Override
  public List<Integer> getDigits() { 
    return this.digits; 
  } 

  @Override
  public String getValue() { 
    return String.format(phoneTemplate,  
                         areaCode(),  
                         prefix(), 
                         lineNumber()); 
  } 
  @Override
  public String areaCode() { 
    return String.format("%d%d%d",               
                         digits.get(0),               
                         digits.get(1),               
                         digits.get(2)); 
  } 

  @Override 
  public String prefix() { 
    return String.format("%d%d%d",               
                         digits.get(3),               
                         digits.get(4),               
                         digits.get(5)); 
  } 

  @Override 
  public String lineNumber() { 
    return String.format("%d%d%d%d",               
                         digits.get(6),               
                         digits.get(7),               
                         digits.get(8),              
                         digits.get(9)); 
  } 

}

 

3.  Create an abstract class named PhoneDecorator:

import java.util.List; 
public abstract class PhoneDecorator implements IPhoneNumber { 
  private IPhoneNumber decoratedPhone; 

  public PhoneDecorator(IPhoneNumber phone) { 
    decoratedPhone = phone; 
  } 

  public String getValue() { 
    return decoratedPhone.getValue(); 
  } 

  public List<Integer> getDigits() { 
    return decoratedPhone.getDigits(); 
  } 

  public String areaCode() { 
    return decoratedPhone.areaCode(); 
  } 

  public String prefix() { 
    return decoratedPhone.prefix(); 
  } 

  public String lineNumber() { 
    return decoratedPhone.lineNumber(); 
  }

}

The abstract class PhoneDecorator will be used as parent for all specific decorator classes.

We will have one decorator class for each new template:

  • Phone With Paranthesis
  • Phone With Hyphens
  • Phone With 604 Prefix
  • Phone With 778 Prefix

 

4. Create the specific decorators

 

PhoneWithParanthesisDecorator class

public class PhoneWithParanthesisDecorator extends PhoneDecorator {    public PhoneWithParanthesisDecorator(IPhoneNumber phone) {      super(phone);    }    public String getValue() {      String phoneTemplate = "(%s) %s %s";      return String.format(phoneTemplate,                                         super.areaCode(),                           super.prefix(),                                        super.lineNumber());    } }

 

PhoneWithHyphensDecorator class

public class PhoneWithHyphensDecorator extends PhoneDecorator { 
  
  public PhoneWithHyphensDecorator(IPhoneNumber phone) { 
    super(phone); 
  } 

  public String getValue() { 
    String phoneTemplate = "%s-%s-%s"; 
    
    return String.format(phoneTemplate,               
                         super.areaCode(),  
                         super.prefix(),   
                         super.lineNumber()); 
  } 

}

 

PhoneWith604PrefixDecorator class

public class PhoneWith604PrefixDecorator extends PhoneDecorator { 

  public PhoneWith604PrefixDecorator(IPhoneNumber phone) { 
    super(phone); 
  } 

  public String getValue() { 
    String phoneTemplate = "%s %s %s"; 

    return String.format(phoneTemplate,               
                         "604", 
                         super.prefix(),   
                         super.lineNumber()); 
  }

}

 

PhoneWith778PrefixDecorator class

public class PhoneWith778PrefixDecorator extends PhoneDecorator { 

  public PhoneWith778PrefixDecorator(IPhoneNumber phone) { 
    super(phone); 
  } 

  public String getValue() { 
    String phoneTemplate = "%s %s %s"; 

    return String.format(phoneTemplate, 
                         "778",  
                         super.prefix(),  
                         super.lineNumber()); 
  }

}

 

Each decorator only implements the change about a specific template.

It does this by encapsulating an IPhoneNumber object and then overriding the getValue() method of the PhoneDecorator class.

There is no code duplication between the phone decorator classes.

They do not have code for anything else than the getValue() method.

 

5. Wrap the PhoneNumber objects with specific decorators to use other phone templates

The test method puts everything together:

import java.util.Arrays;import java.util.List; import org.testng.annotations.Test; 
public class TestClass {
   private final static List<Integer> TEN_DIGITS = 
          Arrays.asList(4, 6, 1, 2, 3, 2, 6, 7, 4, 5); 

  @Test 
  public void testPhoneTemplates() { 

    IPhoneNumber phone1 = new PhoneNumber(TEN_DIGITS); 
    System.out.println(phone1.getValue()); 

    IPhoneNumber phone2 = new PhoneWithParanthesisDecorator( 
                             new PhoneNumber(TEN_DIGITS)); 
    System.out.println(phone2.getValue()); 

    IPhoneNumber phone3 = new PhoneWithHyphensDecorator(
                             new PhoneNumber(TEN_DIGITS)); 
    System.out.println(phone3.getValue()); 

    IPhoneNumber phone4 = new PhoneWith604PrefixDecorator(
                             new PhoneNumber(TEN_DIGITS)); 
    System.out.println(phone4.getValue()); 

    IPhoneNumber phone5 = new PhoneWith778PrefixDecorator(
                             new PhoneNumber(TEN_DIGITS)); 
    System.out.println(phone5.getValue()); 

  }

}

 


 

What did we get when using decorators?

  1. No changes are made to the original class
  2. We program to an interface; all phone number objects are saved in IPhoneNumber variables
  3. Every time we need a different phone template than the one implemented in the PhoneNumber class, we create a decorator for it.
  4. To use the new template, we “decorate” the PhoneNumber object by wrapping it with a decorator object

 

 

 

 

 

 

 

 

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.

Why do static variables and methods suck?

I believe they are very full at times.

I got this comment on a LinkedIn post for the differences between Selenium testers and Selenium developers.

It is worth providing an answer.

Now, I am not a Java expert, far from it.

But others are.

For example, Simon Stewart.

Yes, that Simon Stewart who created Seleniumn WebDriver.

This is from his blog:

“Singletons? Static Methods? Also No.
Singletons (in the traditional “implemented as a static field in a class” sense, not in the “ideally we’d only have one of these” sense) destroy our ability to have fun and write tests that can run in parallel, slashing our potential productivity. Also, it leads people to start using the Service Locator pattern instead of Dependency Injection, and we take DI as an article of faith (see above), mainly because it facilitates TDD by making collaborators clear, like we (also) said above.”

So, static methods? Also no.

Do you need more reasons against static variables and methods?

They promote code that is not object oriented.

 

Object oriented code is about objects.

 

Every method is used on an object.

 

Since static variables or methods are for the class but not the class’s objects, you are writing code that does not use objects.

 

Have a look at the following articles for more details:

Still not convinced?

Read this topic from Stack Overflow.

 

Still in doubt?

Continue to use them.

How to reduce code duplication using predicates and lambda expressions

Many sites allow their clients to search for information and then browse through the results.

Each result has multiple attributes such as

  • title
  • price
  • status

 

results attributes

 

In test automation projects, it is important to have the ability of filtering the results by different attributes.

For example, filter the results with price greater than 0.

Or filter the results that are available only online.

Or filter the results that have a keyword included in the title.

 

How can this filtering be implemented?

public class Result {

  private String title;
  private int price;
  private String status;

  public Result(String title, int price, String status) {
    this.title = title;
    this.price = price;
    this.status = status;
  }

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

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

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

}

Result class is used for creating an object for each result returned for the user search.

The results filtering happens in the test class:

 

public class TestClass {

List<Result> results;

List<Result> withPriceGreaterThan0(List<Result> list) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.price() > 0)
     newResults.add(r);
  return newResults;
}

List<Result> withPriceEqualTo0(List<Result> list) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.price() == 0)
     newResults.add(r);
  return newResults;
}

List<Result> byStatus(List<Result> list, String status) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.status().toLowerCase().contains(status.toLowerCase()))
     newResults.add(r);
  return newResults;
}

List<Result> byKeyword(List<Result> list, String keyword) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (r.title().toLowerCase().contains(keyword.toLowerCase()))
     newResults.add(r);
  return newResults;
}

@Before
public void setUp() {
  Result r1 = new Result("Otter Box", 54, "Not sold online");
  Result r5 = new Result("Virgin iPhone", 0, "Not sold online");
  Result r7 = new Result("Apple iPhone", 450, "Not sold online");
  Result r2 = new Result("Rogers iPhone", 130, "Sold out online");
  Result r6 = new Result("SanDisk iXpand", 125, "Sold out online");
  Result r8 = new Result("Koodo iPhone", 280, "Sold out online");
  Result r3 = new Result("Fido iPhone", 400, "Available online only");
  Result r4 = new Result("TELUS iPhone", 0, "Available online");

  results = Arrays.asList(r1, r2, r3, r4, r5, r6, r7, r8);
}

@Test
public void test1() {

  for(Result r: withPriceGreaterThan0(results))
    assertTrue(r.price() > 0);

  for(Result r: withPriceEqualTo0(results))
    assertTrue(r.price() == 0);

  for(Result r: byStatus(results, "AVAILABLE ONLINE"))
    assertTrue(r.status().toLowerCase().contains("available online"));

  for(Result r: byKeyword(results, "IPHONE"))
    assertTrue(r.title().toLowerCase().contains("iphone"));

}

}

The filtering is being done by the following methods:

List<Result> withPriceGreaterThan0(List<Result> list
List<Result> withPriceEqualTo0(List<Result> list)
List<Result> byStatus(List<Result> list, String status
List<Result> byKeyword(List<Result> list, String keyword)

All get a List<Result> parameter, browse the list with a for statement and select the results that match a specific condition.

The code of the 4 filtering methods is pretty much identical with the only difference being the condition that the results are compared against:

r.price() > 0

r.price() == 0

r.status().toLowerCase().contains(status.toLowerCase())

r.title().toLowerCase().contains(keyword.toLowerCase())

 

How can we have 1 filtering method instead of 4?

One of the new features of Java 8 is lambda expressions.

lambda expressions

Lambda expressions and predicates will help us replace the 4 methods with 1.

Lambda expressions allow functionality (code) to be used as a method parameter in addition to objects.

A lambda expression can be assigned to a Predicate object which can be used as value for a method parameter.

If you are not familiar with lambda expressions yet, this is a good introduction.

Our code can be rewritten with predicates and lambda expressions as follows:

import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import org.junit.Before;
import org.junit.Test;

public class TestClass {

List<Result> results;

List<Result> filter(List<Result> list, Predicate<Result> p) {
  List<Result> newResults = new ArrayList<>();
  for (Result r : list)
    if (p.test(r))
      newResults.add(r);
  return newResults;
}

@Before
public void setUp() {
 ..............................
}

@Test
public void test1() {

  Predicate<Result> priceGreaterThan0 = (r) -> r.price() > 0;
  for(Result r: filter(results, priceGreaterThan0))
    assertTrue(r.price() > 0);

  for(Result r: filter(results, (r) -> r.price() == 0))
    assertTrue(r.price() == 0);

  for(Result r: filter(results, (r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true))
    assertTrue(r.status().toLowerCase().contains("available online"));

  Predicate<Result> keywordInTitle = (r) ->   r.title().toUpperCase().contains("IPHONE") == true;
  for(Result r: filter(results, keywordInTitle))
    assertTrue(r.title().toLowerCase().contains("iphone"));

  for(Result r: filter(results, priceGreaterThan0.and(keywordInTitle))) {
    assertTrue(r.price() > 0);
    assertTrue(r.title().toLowerCase().contains("iphone"));
  }

}

}

In this version of the code, all conditions are expressed as lambda expressions and can be assigned or not to Predicate objects:

Predicate<Result> priceGreaterThan0 = (r) -> r.price() > 0;

Predicate<Result> keywordInTitle = (r) -> r.title().toUpperCase().contains("IPHONE") == true;

(r) -> r.price() == 0))

(r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true))

Since the conditions are expressed as lambda expressions that can be assigned to a Predicate<Result> object, we can have 1 filtering method only that gets 2 parameters:

  • list of results
  • predicate

 

Can we make the code simpler?

The predicates can be moved to a separate class as follows:

import java.util.function.Predicate;

import Result;

public class Predicates {

  public static Predicate<Result> priceGreaterThan0() {
    return (r) -> r.price() > 0;
  }

  public static Predicate<Result> priceEqualTo0() {
    return (r) -> r.price() == 0;
  }

  public static Predicate<Result> availableOnline() {
    return (r) -> r.status().equalsIgnoreCase("AVAILABLE ONLINE") == true;
  }

  public static Predicate<Result> soldOutOnline() {
    return (r) -> r.status().equalsIgnoreCase("SOLD OUT ONLINE") == true;
  }

  public static Predicate<Result> titleIncludes(String keyword) {
    return (r) -> r.title().toUpperCase().contains(keyword) == true;
  }

}

The test class becomes straightforward when the predicates are in their own class:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;

import Result;

public class TestClass {

List<Result> results;

List<Result> filter(List<Result> list, Predicate<Result> p) {
   List<Result> newResults = new ArrayList<>();
   for (Result r : list)
     if (p.test(r)) 
       newResults.add(r);
   return newResults;
 }

@Before
public void setUp() {

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

}

@Test
public void test1() {

for(Result r: filter(results, Predicates.priceGreaterThan0()))
  assertTrue(r.price() > 0);

for(Result r: filter(results, Predicates.priceEqualTo0()))
  assertTrue(r.price() == 0);

for(Result r: filter(results, Predicates.availableOnline()))
  assertTrue(r.status().toLowerCase().contains("available online"));

for(Result r: filter(results, Predicates.titleIncludes("iphone")))
  assertTrue(r.title().toLowerCase().contains("iphone"));

for(Result r: filter(results, Predicates.priceGreaterThan0()
.and(Predicates.titleIncludes("iphone")))) {
  assertTrue(r.price() > 0);
  assertTrue(r.title().toLowerCase().contains("iphone"));
}

}

}