Creating Maintainable Tests using Page Objects

The Page Object Pattern is an abstraction commonly used when performing actions on a web page. The abstraction hides the implementation details (which elements/components are used, how can they be found on the page etc) from the test methods and allows the test methods to focus on the logic to test. The page object depends on how the page/view is implemented and offer high level methods representing actions that a real user could perform on the page. The separation enables the test method to be independent of the implementation details, so refactoring a view and moving components around only require updates to the page object (if any change is required at all) and not the individual tests.

Note
The objects are tradititonally called Page objects even though they should not represent the whole page but rather a smaller part of it.

Creating a Page Object

Regardless of the name, a page object in reality encapsulates a DOM element and is also sometimes called a TestBench Element class.

An element class must:

  1. Extend TestBenchElement

  2. Define an @Element("tag-name") annotation

The @Element annotation defines the tag name of the element which can be located by the element class. The tag name does not have to be unique as the element query user always defines what type of element she is looking for.

When creating a page object for your view, you should use the root tag of the view in the @Element annotation. For views created using Java, this might be e.g. div while for templates it is a custom element, e.g. main-view.

Page Objects for Templates

A page object for a template based login view can look like:

@Element("login-view") // map to <login-view>
public class LoginViewElement extends TestBenchElement {

    protected TextFieldElement getUsernameField() {
        return $(TextFieldElement.class).id("username");
    }

    protected PasswordFieldElement getPasswordField() {
        return $(PasswordFieldElement.class).id("password");
    }

    protected ButtonElement getLoginButton() {
        return $(ButtonElement.class).id("login");
    }

    public void login(String username, String password) {
        getUsernameField().setValue(username);
        getPasswordField().setValue(password);
        getLoginButton().click();
    }
}

When mapping to a template, it’s typically enough to define the tag name as each template has its unique custom tag name.

Page Objects for Java Classes

For views created using Java, the tag name is not unique enough and the page object will find lots of unrelated elements unless you define more restrictions using the @Attribute annotation. The @Attribute annotation allows you to define additional restrictions using attribute values, in the same way as attribute(name,value) on an ElementQuery. @Attribute can be used for any attribute on the element but there are two attributes it is commonly used for: id and CSS class name.

Given a Java login view:

public class LoginView extends Div {

and a login view element:

@Element("div")
public class LoginViewElement extends TestBenchElement {

A query such as $(LoginViewElement.class).first() would find the first <div> on the page. To make the page object find only the LoginView, you can either define an id:

public class LoginView extends Div {
    public LoginView() {
        setId("login-view");
    }

or add a class name:

public class LoginView extends Div {
    public LoginView() {
        addClassName("login-view");
    }

The page object can then use the id as:

@Element("div")
@Attribute(name = "id", value = "login-view")
public class LoginViewElement extends TestBenchElement {

or the class name as:

@Element("div")
@Attribute(name = "class", contains = "login-view")
public class LoginViewElement extends TestBenchElement {

The rest of the page object would be the same in both cases (Template and Java class), the only difference is how you find the element you want to interact with.

Note
You should use contains when you are matching class or similar multi value attributes, so that login-view matches even when there are multiple class names, e.g. class="dark login-view active".
Tip
An @Attribute value or contains property can be set to Attribute.SIMPLE_CLASS_NAME to make it match the simple class name of the page object class with any Element or PageObject suffix removed. As @Attribute annotations are inherited, you can add this on a base class for your elements.
Note
All Vaadin component integrations for TestBench can also be considered page objects even though they only provide a high level API for a single component. There is no conceptual difference between creating elements for web components and elements for templates or classes representing a whole view.

Using a Page Object

To be able to use the helper methods from a page object, you need to get an instance of the page object. You use the standard ElementQuery methods to retrieve an instance of your page object, e.g. to handle login in a test you can do:

public class LoginIT extends TestBenchTestCase {

   // Driver setup and teardown omitted

    @Test
    public void loginAsAdmin() {
        getDriver().open("http://localhost:8080");
        LoginViewElement loginView = $(LoginViewElement.class).first();
        loginView.login("admin@vaadin.com", "admin");
        // TODO Assert that login actually happened
    }
}

Chaining Page Objects

Whenever an action on a page object results in the user being directed to another view, it is good practice to find an instance of the page object for the new view and return that. This allows test methods to chain page object calls and continue to perform actions on the new view.

For the LoginViewElement we could accomplish this by updating the login method:

public MainViewElement login(String username, String password) {
    getUsernameField().setValue(username);
    getPasswordField().setValue(password);
    getLoginButton().click();
    // Find the page object for the main view the user ends up on
    // onPage() is needed as MainViewElement is not a child of LoginViewElement.
    return $(MainViewElement.class).onPage().first();
}
Tip
When the login view finds the main view element, it automatically validates that the login succeeded or the main view will not be found.

A test method can now do:

@Test
public void mainViewSaysHello() {
    getDriver().open("http://localhost:8080");
    LoginViewElement loginView = $(LoginViewElement.class).first();
    MainViewElement mainView = loginView.login("admin@vaadin.com", "admin");
    Assert.assertEquals("Hello", mainView.getBanner());
}

You can find a fully functional page object based test example in the demo project at https://github.com/vaadin/testbench-demo/tree/master/src/test/java/com/vaadin/testbenchexample/pageobjectexample.

Extending a Page Object

If you want to add functionality to an existing element, you can extend the original element class and add more helper methods, e.g.

public class MyButtonElement extends ButtonElement {

   public void pressUsingSpace() {
     ....
   }
}

You can then use your new element by replacing

ButtonElement button = $(ButtonElement.class).id("ok");
...

by

MyButtonElement button = $(MyButtonElement.class).id("ok");
button.pressUsingSpace();