3

I have run into problems keeping my tests as object-oriented as possible. I have had to rewrite large portions of my code multiple times now, and I feel that there is a better approach to what I'm doing. I explain my design below.

Description:

I am using Selenium/Java to do some data integrity testing. The website being tested is used to create and manipulate batches. These batches have various properties such as name, ID number, different quantities, and ingredients. This data for each batch is supposed to change and be updated as you progress through the workflow of the website. The Selenium tests are supposed to simulate the workflow and track the batches to verify the data integrity of the website.

Current design:

I am using the Page Object Model to abstract the page classes from the test classes. In addition, I have a Batch class with all the properties and getters/setters for most properties. In all applicable page classes, I have a scrapeBatch() method which scrapes the batch information and returns a new Batch() object with those properties set. I then have an equals(Batch) method defined for the Batch object to verify that there are no discrepancies between two batches. The scrapeBatch() methods for the page classes right now use setters to assign the properties to a new Batch() object. In addition, different pages on the website display different properties.

Goal:

I would like to not have to use all the getters and setters to make the code more concise and to reuse more code wherever possible. I have read that using interfaces is the way to go, but I am having difficulty understanding how to define those interfaces in the most logical way. Any suggestions for ways to refactor my code would be greatly appreciated. I would be able to expand more on this too, if that is needed.

3 Answers 3

1

Java's built in Function interface allows me to avoid using getters. Instead of hardcoding each property in a separate field, I use a TreeMap to hold all the properties as property name and property value. A second TreeMap links properties to scraping methods. The data type is String but I could also use a TreeMap as the value type within the first Treemap, which would allow me to have repeating field such as ingredients or detail items.

I use the iterable map TreeMap to store my properties as key-value pairs. (The property's name is the key, stored here as a string. Keys must be unique within the map.) I am out of the business of writing a getter for each property and instead I am loading up a map; the record can have any number of fields.

     Map<String, String> expectedMap = new TreeMap<>();
     Map<String, String> actualMap = new TreeMap<>();

I load the map by specifying the key and the value.

     map.put("someKey1", "expectedValue1");
     map.put("someKey2", "expectedValue2");
     // etc...

You mentioned that you are storing your data in a separate class. Your separate class can be modified to have a TreeMap field rather than individual fields.

The next step is to create another TreeMap list, which is a list of the scraping methods I want to use for my test. Java allows you to store a method reference as a variable.

    Map<String, Function<Page, String>> functionList = new TreeMap<>();

Now, for each test, I create a list of all scrape methods that I want to use to get my data. The method signature of the scrape method needs to match that of the Function<Page, String> interface: it's void and returns a string.

  functionList.put("someKey1", (Page page)-> page.getKey1());
  functionList.put("someKey2", (Page page)-> page.getKey2());

I loop over my function list to load up my data TreeMap. It extracts the value from Selenium using the page method and then adds it to the expected values map using the same key as was used to identify the function. So my long list of calls to page methods is replaced with this line of code.

    for (TreeMap.Entry<String, Function<Page, String>> entry : functionList) {
      expectedMap.put(entry.key(), entry.value().apply())
    }

After this prep the body of my tests can look like this.

    //arrange

    // to avoid repetition these load sequences can be looped over 
    // an array, a CSV,
    // stored in my data class, pulled from a database, etc.

    expectedMap.put("someKey1", "expectedValue1");
    expectedMap.put("someKey2", "expectedValue2");

    // this can be loaded elsewhere from a shared class.

    functionList.put("someKey1", (Page page)-> page.getKey1());
    functionList.put("someKey2", (Page page)-> page.getKey2());

    //act 

    for (TreeMap.Entry<String, Function<Page, String>> entry : functionList) {
      actualMap.put(entry.key(), entry.value().apply())
    }

    //assert

    // Note that equals() must be properly overridden here.

    if (!expectedMap.equals(actualMap)) throw new AssertionError;

If a String won't do it for me then I can plug in my own class, as long as I reference it elsewhere when declaring the TreeMap:

     Map<String, MyClass> expectedMap = new TreeMap<String, MyClass>();

If I use an array as my data type, I am aware that this will break the essential equals() method on the TreeMap, and I'll need to write my own code to verify the content is the same.

Here's the source I tested to proof this explanation. This test fails; if I want the test to pass, I change "expectedValue0" to "expectedValue2".

    import java.util.Map;
    import java.util.TreeMap;
    import java.util.function.Function;

    /**
     *
     * @author apdo
     */
    public class AbstractedData {

        /**
         * @param args the command line arguments
         */
        public static void main(String[] args) {
            TreeMap<String, String> expectedMap = new TreeMap<>();
            TreeMap<String, String> actualMap = new TreeMap<>();
            TreeMap<String, Function<Page,String>> functionList = new TreeMap<>();
            expectedMap.put("someKey1", "expectedValue1");
            expectedMap.put("someKey2", "expectedValue2");
            functionList.put("someKey1", (Page page)-> page.getKey1());
            functionList.put("someKey2", (Page page)-> page.getKey2());
            Page page = new Page();

             // act - Loop over the function list, looking up
             // corresponding values by key name from the actualMap

            for (Map.Entry<String, Function<Page, String>> entry : functionList.entrySet()) {
              actualMap.put(entry.getKey(), functionList.get(entry.getKey()).apply(page));
            }

            //assert - TreeMap handles the looping and comparison using equals()
            if (!expectedMap.equals(actualMap)) throw new AssertionError("Assertion Failed");  

        }
        public static class Page {
          String getKey1() { return "expectedValue1"; }
          String getKey2() { return "expectedValue0"; }

        }



    }
1

You are right to be concerned about getters/setters; they are an anti-pattern in OOP and should be avoided because they result in more tightly coupled code.

Instead invert the responsibility by using the tell don't ask idiom. In this case by using an expected data manager. Create a Batch class that reflects the class of expected data and pass this expectedData to the pageObject for validation. The page object scrapes the data items and passes the values to the Batch object for verification.

public class Batch {
    private Data expectedData;

    public void verify(Data actual) {
       assertEquals(expectedData, actual);
    }
}

When I use an expected data manager I give it a factory method to make the instance of the class, loading the data for the specific context by use of tag. e.g. '@scenarioName'.

public class MyPageObject extends AbstractPageObject {
    @FindBy(id="dataId")
    private WebElement pageItem;

    public void verify(Batch batchData) {
       batchData.verify(pageItem);
    }
}
-1

Interfaces are one way you could refactor most of this. Instead of a batch specific scrapeBatch you can write a generic scrapeBatch which does different things based on what interfaces your batch implements

public class Batch implements HasName{
  private String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

public Batch scrapeBatch(Batch batch) {
    if (batch instanceof HasName) {
        String name = driver.findElement(By.cssSelector(".name"));
        ((HasName) batch).setName;
    }
}

Another way is to use beans and reflections to invoke setters, basically defining what properties a batch has by having setProperty methods

public class Batch {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


public Batch scrapeBatch() throws InvocationTargetException, IllegalAccessException {
    Class batch = Batch.class;
    Batch newBatch = new Batch;
    Method[] allMethods = batch.getMethods();
    List<Method> setters = new ArrayList<>();
    for(Method method : allMethods) {
        if(method.getName().startsWith("set")) {
            setters.add(method);
        }
    }

    // just example, use switch or loop or something
    if (setters.get(0).getName() == "setName") {
        String name = driver.findElement(By.cssSelector(".name"));
        setters.get(0).invoke(newBatch, name);
    }

    return newBatch;
}

A third way which could make this all much easier for you is to create better interfaces in your web application. REST comes to mind. When the web application provides a way to request/display the batches as JSON and you create some mapping in your code, you can easily parse said JSON to java objects and do assertions on them. See for example object mapping with jackson. Also there are good selenium frameworks focussing on testing REST interfaces you could use.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.