Creating Maintainable Tests Using Page Objects
The Page Object Pattern is an abstraction commonly used when performing actions on a web page. This abstraction hides the implementation details from the test methods and allows the test methods to focus on the logic to test. Implementation details can include which elements or components are used, how they may be found on the page, and so forth.
The page object depends on how the page or view is implemented. It offers 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 that refactoring a view and moving components only requires any needed updates to the page object and not the individual tests.
Note
| The objects are traditionally called Page objects, even though they shouldn’t represent the whole page but rather a smaller part of it. |
Creating a Page Object
Regardless of the name, a page object actually encapsulates a DOM element and is also sometimes called a TestBench Element class.
An element class must do the following:
-
Extend
TestBenchElement
-
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 doesn’t have to be unique, as the element query user always defines what type of element for which they’re searching.
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 div
, whereas for templates it’s a custom element, such as main-view
.
Page Objects for Templates
A page object for a template-based login view can look like this:
@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 a unique custom tag name.
Page Objects for Java Classes
For views created using Java, the tag name isn’t unique enough. The page object will find plenty of unrelated elements unless you define more restrictions using the @Attribute
annotation.
The @Attribute
annotation allows you to define additional restrictions using attribute values. You would do this in the same way as attribute(name,value)
on an ElementQuery
. @Attribute
can be used for any attribute on the element. However, there are two attributes that are commonly used: id and CSS class name.
Suppose you have a Java login view like so:
public class LoginView extends Div {
...
}
Suppose further you have a login view element like this:
@Element("div")
public class LoginViewElement extends TestBenchElement {
...
}
Given these two factors, 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 do one of two things. You could define an id
:
public class LoginView extends Div {
public LoginView() {
setId("login-view");
}
...
}
Or you could add a class name
like so:
public class LoginView extends Div {
public LoginView() {
addClassName("login-view");
}
...
}
The page object can then use the id
like this:
@Element("div")
@Attribute(name = "id", value = "login-view")
public class LoginViewElement extends TestBenchElement {
...
}
Or it can use the class name
as this:
@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 (i.e., Template and Java class). The only difference is the way you find the element with which you want to interact.
Note
|
You should use contains when you’re 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. Creating elements for web components has no conceptual difference to creating 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 would use the standard ElementQuery
methods to retrieve an instance of your page object.
For example, to handle login in a test you can do the following:
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’s a 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
, you can accomplish this by updating the login()
method like so:
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 isn't 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. Otherwise, the main view isn’t found. |
A test method can now do the following:
@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. Here’s an example:
public class MyButtonElement extends ButtonElement {
public void pressUsingSpace() {
....
}
}
You can then use your new element by replacing this:
ButtonElement button = $(ButtonElement.class).id("ok");
...
You would use something this instead:
MyButtonElement button = $(MyButtonElement.class).id("ok");
button.pressUsingSpace();
391A7942-50E3-4630-BF65-C512C11B64A3