Starting with the Page Object Model / Page Factory
Breaking news. Today is my 47th Birthday and it seems that the JUnit team have given me a birthday present with the announcement of JUnit 5.0 General Availability Release today. As of now, it is not in Maven central so I’m still with the release candidate, but I look forward to checking it out soon.
I have started off this blog looking at some of the basics of setting up and running basic tests using JUnit and Selenium WebDriver. I haven’t spent any time explaining how to find selectors on a page as I am assuming that you already have some experience with it. I may get around to discussing how I do this later, but for now I am going to focus on how you progress from simply scripting a test, to developing a maintainable test suite.
Why the Page Object Model?
My first attempt at using WebDriver in anger was to demonstrate that a large and complex website with an upgraded CMS was functionally identical to its replacement. It was basically a set of C# scripts with a huge amount of duplicated code: Relatively fast to produce, exactly repeatable and self documenting with screenshots it was perfect for the task that I was performing.
Imagine however that I wanted to repeat my testing a year later after some significant changes to the html. I would have a nightmare task to find all of the changes and make them in all the right places in my code. My script was almost impossible to maintain.
It would be much better if each element on each page is defined in exactly ONE place in the test code. If a test is broken due to an html change, once the new selector is found, it is changed in one place in the code and now all tests that use this changed element are working again.
For (I hope) obvious reasons I am going to start out with my homepage and so to define some test cases:
- Title text = “Alexander on Testing”
- Number of widgets = 5
- Number of articles <=5
The last of these is as I have set the homepage to display only 5 blog posts, even though this one today is actually number 5. I guess I’ll find out next week if I failed in the setup if this test fails!
A quick look in your browser’s developer tools and we can see that we can use the following selectors:
For Title text
driver.findElement(By.cssSelector(".site-title"))
For a list of the widgets on the page
driver.findElements(By.cssSelector(".widget"))
For a list of the articles on the page
driver.findElements(By.cssSelector(".post-content"))
Building my Page Object
Now we are finally writing some code in the Main (as opposed to Test) part of the codebase. With re-usability in mind I am going to define a Page interface with some generic methods:
package tech.alexontest.poftutor; public interface Page { String getPageTitle(); String getURL(); }
and an AbstractPage to hold the WebDriver and implement these generic methods:
package tech.alexontest.poftutor; import org.openqa.selenium.WebDriver; abstract class AbstractPage implements Page { private WebDriver driver; protected AbstractPage(final WebDriver driver) { this.driver = driver; } protected WebDriver getDriver() { return driver; } public String getURL() { return driver.getCurrentUrl(); } }
Now all that’s left is to produce a HomePage Object. In fact I am going to try two different implementations here, one using conventional Page Object Model, and one using Selenium’s Page Factory implementation so I will create a HomePage Interface.
package tech.alexontest.poftutor; import org.openqa.selenium.WebElement; import java.util.List; public interface HomePage { String getPageTitle(); List<WebElement> getWidgets(); List<WebElement> getArticles(); }
And my Page Object Model Implementation PomHomePage
package tech.alexontest.poftutor; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import java.util.List; public class PomHomePage extends AbstractPage implements HomePage { private WebDriver driver; private By titleBy = By.cssSelector(".site-title"); private By widgetsBy = By.cssSelector(".widget"); private By articlesBy = By.cssSelector(".post-content"); public PomHomePage(final WebDriver driver) { super(driver); this.driver = driver; } @Override public String getPageTitle() { return driver.findElement(titleBy).getText(); } @Override public List<WebElement> getWidgets() { return driver.findElements(widgetsBy); } @Override public List<WebElement> getArticles() { return driver.findElements(articlesBy); } }
And finally then we can change our test class to set up and use the PomHomePage object.
package tech.alexontest.poftutor; import org.junit.Before; import org.junit.Test; import org.openqa.selenium.WebDriver; import tech.alexontest.poftutor.infrastructure.AbstractSingleDriverTest; import tech.alexontest.poftutor.infrastructure.AbstractTest; import static org.assertj.core.api.Assertions.assertThat; public class HomePageTests extends AbstractTest { private HomePage homePage; @Before public void setupHomepage() { final WebDriver driver = getDriver(); driver.get("https://alexanderontesting.com/"); homePage = new PomHomePage(driver); } @Test public void titleIsCorrect() { assertThat(homePage.getPageTitle()) .isEqualToIgnoringCase("Alexander on Testing"); } @Test public void pageContainsFiveWidgets() { assertThat(homePage.getWidgets()) .size() .isEqualTo(5); } @Test public void pageContainsUpToFiveArticles() { assertThat(homePage.getArticles()) .size() .isLessThanOrEqualTo(5); } }
Note I have re-organised a bit here, taking the URL tests to their own test class. I have also added another @Before method to start the webdriver and instantiate the HomePage object. and sure enough it works.
A Page Factory Implementation
Selenium’s Page Factory uses annotations to reduce the amount of boilerplate code in Page Object classes. Rather than using ‘By’ fields, we use the @FindBy annotation on WebElement (or List<WebElement>) to indicate the location strategies we will use.
so:
private By titleBy = By.cssSelector(".site-title");
and
public String getPageTitle() { return driver.findElement(titleBy).getText(); }
becomes
@FindBy(css = ".site-title") private WebElement pageTitle;
and
public String getPageTitle() { return pageTitle.getText(); }
The class then in full
package tech.alexontest.poftutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import java.util.List; public class PfHomePage extends AbstractPage implements HomePage { @FindBy(css = ".site-title") private WebElement pageTitle; @FindBy(css = ".widget") private List<WebElement> widgets; @FindBy(css = ".post-content") private List<WebElement> articles; public PfHomePage(final WebDriver driver) { super(driver); } @Override public String getPageTitle() { return pageTitle.getText(); } @Override public List<WebElement> getWidgets() { return widgets; } @Override public List<WebElement> getArticles() { return articles; } }
Back in our test class we use our Page Factory object with the following:
homePage = PageFactory.initElements(driver, PfHomePage.class);
And of course it works without problem.
The last thing however is that using this form in my test requires knowledge of the implementation and I would rather hide that away, not least because I will be looking at how to use Google Guice for dependency injection soon. So I will just create a new PfHompage in the Test
homePage = new PfHomePage(driver);
and let PfHomePage initialise itself in its constructor.
public PfHomePage(final WebDriver driver) { super(driver); PageFactory.initElements(driver, this); }
You can of course find the code at https://github.com/AlexanderOnTesting/POFTutor.
Progress made this week:
- Created the first example Page objects for my Homepage, one using the classic Page Object Model, and a second using Selenium’s Page Factory functionality.
- Verified that all of this still works under JUnit 5 as discussed last week.
Lessons learnt:
- Whilst the Page Object Model works well, classes built using the Page Factory cut out a lot of boilerplate code to make things cleaner and easier to read.
That will do for now. I was torn between two ideas for next week. I was planning to be either looking at using Guice for Dependency Injection or looking a bit deeper into using the Page Factory, but as JUnit 5 has hit GA today, I might just take a look at its final shape and if this has changed anything!