|
PageObjects
The Page Object pattern represents the screens of your web app as a series of objects
Page ObjectsWithin your web app's UI there are areas that your tests interact with. A Page Object simply models these as objects within the test code. This reduces the amount of duplicated code and means that if the UI changes, the fix need only be applied in one place. Implementation NotesPageObjects can be thought of as facing in two directions simultaneously. Facing towards the developer of a test, they represent the services offered by a particular page. Facing away from the developer, they should be the only thing that has a deep knowledge of the structure of the HTML of a page (or part of a page) It's simplest to think of the methods on a Page Object as offering the "services" that a page offers rather than exposing the details and mechanics of the page. As an example, think of the inbox of any web-based email system. Amongst the services that it offers are typically the ability to compose a new email, to choose to read a single email, and to list the subject lines of the emails in the inbox. How these are implemented shouldn't matter to the test. Because we're encouraging the developer of a test to try and think about the services that they're interacting with rather than the implementation, PageObjects should seldom expose the underlying WebDriver instance. To facilitate this, methods on the PageObject should return other PageObjects. This means that we can effectively model the user's journey through our application. It also means that should the way that pages relate to one another change (like when the login page asks the user to change their password the first time they log into a service, when it previously didn't do that) simply changing the appropriate method's signature will cause the tests to fail to compile. Put another way, we can tell which tests would fail without needing to run them when we change the relationship between pages and reflect this in the PageObjects. One consequence of this approach is that it may be necessary to model (for example) both a successful and unsuccessful login, or a click could have a different result depending on the state of the app. When this happens, it is common to have multiple methods on the PageObject: public class LoginPage {
public HomePage loginAs(String username, String password) {
// ... clever magic happens here
}
public LoginPage loginAsExpectingError(String username, String password) {
// ... failed login here, maybe because one or both of the username and password are wrong
}
public String getErrorMessage() {
// So we can verify that the correct error is shown
}
}The code presented above shows an important point: the tests, not the PageObjects, should be responsible for making assertions about the state of a page. For example: public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
inbox.assertMessageWithSubjectIsUnread("I like cheese");
inbox.assertMessageWithSubjectIsNotUndread("I'm not fond of tofu");
}could be re-written as: public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}Of course, as with every guideline there are exceptions, and one that is commonly seen with PageObjects is to check that the WebDriver is on the correct page when we instantiate the PageObject. This is done in the example below. Finally, a PageObject need not represent an entire page. It may represent a section that appears many times within a site or page, such as site navigation. The essential principle is that there is only one place in your test suite with knowledge of the structure of the HTML of a particular (part of a) page. Summary
Examplepublic class LoginPage {
private final WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
// Check that we're on the right page.
if (!"Login".equals(driver.getTitle())) {
// Alternatively, we could navigate to the login page, perhaps logging out first
throw new IllegalStateException("This is not the login page");
}
}
// Conceptually, the login page offers the user the service of being able to "log into"
// the application using a user name and password.
public HomePage loginAs(String username, String password) {
// This is the only place in the test code that "knows" how to enter these details
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("passwd")).sendKeys(password);
driver.findElement(By.id("login")).submit();
// Return a new page object representing the destination. Should the login page ever
// go somewhere else (for example, a legal disclaimer) then changing the method signature
// for this method will mean that all tests that rely on this behaviour won't compile.
return new HomePage(driver);
}
}
Support in WebDriverThere is a PageFactory in the support package that provides support for this pattern, and helps to remove some boiler-plate code from your Page Objects at the same time. |
Sign in to add a comment
This is (or matches) imho the concept of DSL's when doing acceptance TDD with e.g. FitNesse?. Furthermore it also provides a kind of GUI mapping, when the element locators are extracted into static final fields. Thus in case of a GUI change (not every web developer uses unique attributes) the element locator would only have to be changed at one place in the page class.
I implemented my first test case using the Page Objects pattern. There are three pages in my test: Home Page, Login Page, and My Account Page. I feel good about the results. Thanks for your hard and especially smart work put in WebDriver?.
By the way, the Summary on this page is excellent. One question on my side, though. How do you name the public methods of a page that offer "navigation" services? - something like "goToLoginPage"?
Example: In my situation I have a home page with lots of links. Home page acts like a main service dispatcher. In order to enjoy the services offered by a particular "service provider page" one has to navigate to that page first and then ask that page for the service.
My test looks like this: driver.get("http://myhomepage.com/"); HomePage? homePage = PageFactory.initElements(driver, HomePage?.class); LoginPage? loginPage = homePage.goToLoginPage(); // navigation service loginPage.loginAs("validusername", "correctpassword"); // customer value service
Could you replace my "navigation services" with something more abstract? Or is it good enough as is?
Thanks, Mircea
It's another day and I can see things a bit differently, not necessary better. Yesterday I named my home page as being main service dispatcher. Now, I would name it service directory. I'd rename navigation services as selection services. Consequently, my goToLoginPage would be named as selectLoginService. So, when I click the Login link on the home page I'll say in my test selectLoginService(). Once I am on the Login page - which is the service provider for me as a user of the application - I would say loginAs(username, password). Please let me know your thoughts.
@mircea: This isn't really the appropriate place to have these discussions. Please post a message to the WebDriver? group for this kind of help. The reason is twofold:
1) Not many people are following these comments, so not many people can help you solve your problems.
2) The group is indexed by search engines, whereas these comments are scrubbed occasionally. This means that any help we do give is available to other people if we have the discussion on the mailing list.
The group can be found at: http://groups.google.com/group/webdriver