Page objects, chaining and passing web driver
When we create our Selenium Web Driver framework we need to design many things. I assume that we want to use Page objects but we can decide if we want to use chaining in our tests. If answer for that question is 'yes' then next question appears... How to deal with passing web driver object between page objects? In this article I will describe how to create framework which uses Page objects, chaining and pass the driver under the hood.
To make this framework working as expected we need a few elements. The first and most important is the interface which will be creating page object and initializing all its elements. This interface - lets call it PageInitializer - will be implemented in all page object classes and test classes to give us possibility to create page object in simple and consistent way.
package com.testcraftsmanship.model.base; import org.openqa.selenium.WebDriver; import org.openqa.selenium.support.PageFactory; import java.lang.reflect.InvocationTargetException; public interface PageInitializer { default <T extends BasePage> T newInstance(Class<T> clazz) { try { T page = clazz.getConstructor(WebDriver.class).newInstance(getDriver()); PageFactory.initElements(getDriver(), page); return page; } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("Error while creating new page instance", e); } } WebDriver getDriver(); }
When the interface is ready we have to implement it in all test classes and page
object classes so the easiest way is to do it in the base class which will be extended
by every test class or page object class. The BasePage class will be extended by every
page object in our framework we have to have constructor with WebDriver as an argument
and possibility of getting the WebDriver.
package com.testcraftsmanship.model.base; import org.openqa.selenium.WebDriver; public abstract class BasePage implements PageInitializer { private final WebDriver driver; public BasePage(WebDriver driver) { this.driver = driver; } @Override public WebDriver getDriver() { return driver; } }
The next class we need is BaseTest class which will be extended by every class with tests:
package com.testcraftsmanship.model.base; import com.testcraftsmanship.model.config.DriverFactory; import com.testcraftsmanship.model.pages.GooglePage; import org.openqa.selenium.WebDriver; public abstract class BaseTest implements PageInitializer { private final WebDriver driver; public BaseTest() { this.driver = DriverFactory.getDriver(); } public WebDriver getDriver() { return driver; } protected GooglePage openApplication() { driver.get("http://google.com"); return newInstance(GooglePage.class); } protected void closeApplication() { driver.quit(); } }
In this class getDriver() method is required by previously created interface. The
driver is created during instantiation of test class. The getDriver() method is used
by newInstance(GooglePage.class) so we don't need to pass the driver during every page
object creation in the argument as it is shown in the openApplication() method.
Lets now create two simple page object to illustrate how we can use created mechanism:
package com.testcraftsmanship.model.pages; import com.testcraftsmanship.model.base.BasePage; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; public class GooglePage extends BasePage { @FindBy(xpath = "//a[contains(text(), 'Gmail')]") private WebElement gmailButton; public GooglePage(WebDriver driver) { super(driver); } public GMailPage clickGmailButton() { gmailButton.click(); return newInstance(GMailPage.class); } }
This class represents google search page and as you can see we don't need to remember
about passing the driver to new page object. It extends BasePage class so when we want
to navigate to other page e.g. Gmail page we just click the desired button and use
newInstance() method which returns new instance of the class passed as an argument.
We should extends BasePage in all pages so we easily can use all common methods like newInstance.
package com.testcraftsmanship.model.pages; import com.testcraftsmanship.model.base.BasePage; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; public class GMailPage extends BasePage { @FindBy(css = ".gmail-nav__nav-link__create-account") private WebElement createAccountButton; public GMailPage(WebDriver driver) { super(driver); } public String createAccountButtonText() { return createAccountButton.getText(); } }
When we have at least two pages created we can show how our tests can looks like in this approach:
package com.testcraftsmanship; import com.testcraftsmanship.model.base.BaseTest; import org.testng.annotations.AfterTest; import org.testng.annotations.Test; import static org.assertj.core.api.Assertions.assertThat; public class GMailTest extends BaseTest { @Test public void correctTextShouldBeDisplayedOnGmailCreateButton() { String actualCreateAccountText = openApplication() .clickGmailButton() .createAccountButtonText(); assertThat(actualCreateAccountText).isEqualTo("CREATE ACCOUNT"); } @AfterTest public void cleanUp() { closeApplication(); } }
As we can see on the code above we don't need to remember about passing the driver
in the real test code. Test and instantiating new pages are more clear and easy because
actions connected to passing driver are performed under the hood.