Thursday, September 20, 2018

Automation Strategy: Keep Going With Page Objects (Don't Stop at Locators)

Stopping at the Simplest Implementation of POM

I don't love the Page-Object Model (POM).  Actually, I just don't love the way many teams implement it.  When they stop at giving their page elements locators, they end up with elements that look like

//great:
processReturnsButton = new element(driver, By.id("process_return_button"));
//sure, no problem (except I'd probably externalize the strings, see https://jws-testing-blog.blogspot.com/2018/09/automation-strategy-externalize-all.html)
processTheseItemsButton = new element(driver, By.xpath("//a[text()='Process These Items']"));
createReturnTextLabel = new element(driver, By.xpath("//span[text()='Create Return']"));
processReturnsTextLabel = new element(driver, By.xpath("//span[text()='Process Returns']"));
//not so great:
identificationNumber = new element(driver, By.xpath("//table[starts-with(@id,'details_table_')]/tbody/tr/th[text()='Identification Number']"));
orderTitleTextLabel = new element(driver, By.xpath("//*[@id=\"content\"]/form/div[1]/h2"));
userTextLabel = new element(driver, By.xpath("//table[@class='order_notes']/tbody/tr/th[text()='User']"));
roleTextLabel = new element(driver, By.xpath("//table[@class='order_notes']/tbody/tr/th[text()='Role']"));
noteTextLabel = new element(driver, By.xpath("//table[@class='order_notes']/tbody/tr/th[text()='Note:']"));
firstNoteAddedPstTimeTextLabel = new element(driver, By.xpath("//table[@class='order_notes']/tbody/tr[2]/td[1]"));
secondNoteAddedPstTimeTextLabel = new element(driver, By.xpath("//table[@class='order_notes']/tbody/tr[2]/td[4]"));
view raw locators01.cs hosted with ❤ by GitHub

Reusing Page Objects in Other Page Objects

It's these last few that get me. Any time I see redundant code it makes me cringe. Let's level-up:

//great:
processReturnsButton = new element(driver, By.id("process_return_button"));
//sure, no problem (except I'd probably externalize the strings, see https://jws-testing-blog.blogspot.com/2018/09/automation-strategy-externalize-all.html)
processTheseItemsButton = new element(driver, By.xpath("//a[text()='Process These Items']"));
createReturnTextLabel = new element(driver, By.xpath("//span[text()='Create Return']"));
processReturnsTextLabel = new element(driver, By.xpath("//span[text()='Process Returns']"));
//not so great:
identificationNumber = new element(driver, By.xpath("//table[starts-with(@id,'details_table_')]/tbody/tr/th[text()='Identification Number']"));
orderTitleTextLabel = new element(driver, By.xpath("//*[@id=\"content\"]/form/div[1]/h2"));
//fancy!
orderNotesTable = new element("//table[@class='order_notes']")
//re-use! Notice the * syntax
userTextLabel = new element(orderNotesTable, By.xpath("*/th[text()='User']"));
roleTextLabel = new element(orderNotesTable, By.xpath("*/th[text()='Role']"));
noteTextLabel = new element(orderNotesTable, By.xpath("*/th[text()='Note:']"));
firstNoteAddedPstTimeTextLabel = new element(orderNotesTable, By.xpath("*/tr[2]/td[1]"));
secondNoteAddedPstTimeTextLabel = new element(orderNotesTable, By.xpath("*/tr[2]/td[4]"));
view raw locators02.cs hosted with ❤ by GitHub

This can probably only be achieved if you've wrapped your Selenium tool's WebElement and WebDriver class. Hopefully you did this. It will take a little cleverness and adding some constructors, but it should be doable.

Dynamic Elements Using Methods, Not Variables

I find myself kicking the can down the road, or pushing the work down to a lower level like this all the time. "Make someone else do the work" I say. If I'm in the middle of writing a method and I think for one second that what I'm about to write could be used somewhere else, I try to encapsulate that work in a method. Better yet, I imagine what I'd pass into that method, what it would return to me, and continue writing my current method as if that helper method exists and keep going. Later on I'll write the helper methods into existence. 

In this case, while we're writing createAndViewOrder(), we don't want to worry about how we find the orderConfirmation element, we just want to assume it works and use it, so let's move it out of the method.  Before:

//avoid having element specifics in your functional page methods
class OrderPage
public void createAndViewOrder(orderID)
//create the order...
element orderConfirmation = driver.findElement(By.xpath("//div[text()='Order " + orderID + " is confirmed.']"));
orderConfirmation.waitUntilExists()
//view the order...
view raw locators04.cs hosted with ❤ by GitHub

After:

//make someone else do the work!
class OrderPage
public element confirmOrderCreation(orderID)
return driver.findElement(By.xpath("//div[text()='Order " + orderID + " is confirmed.']"));
public void createAndViewOrder(orderID)
//create the order...
confirmOrderCreation(orderID).waitUntilExists()
//view the order...
view raw locators05.cs hosted with ❤ by GitHub

Does this buy you a lot right now? Not really. But
  1. it might buy you something down the road during a refactor
  2. it makes your functional method easier to read
  3. it literally takes 15 extra seconds

Summary

It goes to a frame of mind that is good to get into when writing automation code - avoid UI specifics in functional methods, because you're likely to re-use that UI element again. There's a LOT of patterns when writing automation code from how your tests are constructed to how you find page objects to how you interact with the app, so you really have to be on guard and work against code duplication, this is just one of the ways.

No comments:

Post a Comment