Loading...

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.