Page Objects in Selenium 2 (WebDriver).
Press Spacebar or Tab To Get
StartedTo view this presentation please use
the latest Firefox, Chrome, or Safari browser.
Inspired byThis project is inspired by:
Ben Burton's WebDriver
Best Practices
Dave Justin’s
Dave Justin’s
Dave Justin’s page
object post
James Cox's
James Cox's
James Cox's Using
Page Objects
Why
Page Objects pattern?Reduces the amount of duplicated
code. Encapsulate the internal state of a page into a single page
object.
UI changes only affect to a single Page Object, not to the actual test codes.
Code re-use: Able to use the same page object in a variety of tests cases.
Step
1: Page Object base class.
PageBase class provides the base
structure and properties of a page object to extend.
Step 2: Extends PagaBase
Step 3: Navigation from Page to
Page
1. When you're simulating having the
user enter a new URL into the URL bar of the browser, then it's the
responsibility of the test class to create the page object it needs.
2. some operation on the page that
would cause the browser to point to another page -- for example, clicking a
link or submitting a form -- then it's the responsibility of that page object
to return the next page object.
Step 4: Tests with Page Objects
Page Object PatterndiggPin ItEmailRSSSPage Object Pattern, the term that
selenium users keep buzzing. Page object is a design pattern that can be
implemented as a selenium best practices. The functionality classes
(PageObjects) in this design represent a logical relationship between the pages
of the application.·
The Page Object pattern represents the
screens of your web app as a series of objects and encapsulates the features
represented by a page.
·
It allows us to model the UI in our
tests.
·
A page object is an object-oriented
class that serves as an interface to a page of your AUT.
More on Page Object Pattern at Selenium
wiki. Some of the
advantages of page object pattern as listed below,
·
Reduces the duplication of code
·
Makes tests more readable and
robust
·
Improves
the maintainability of tests, particularly when there is frequent
change in the AUT. (Useful in Agile methodology based projects)
Enough of theory, lets get into
practical implementation.Page Object, which models the Google
search page:[sourcecode language="java"
wraplines="false" collapse="false"]
public class GoogleSearchPage {protected WebDriver driver;
private WebElement q;
private WebElement btn;public GoogleSearchPage(WebDriver
driver) {
this.driver = driver;
}
public void open(String url) {
driver.get(url);
}
public void close() {
driver.quit();
}
public String getTitle() {
return driver.getTitle();
}
public void searchFor(String searchTerm) {
q.sendKeys(searchTerm);
btn.click();
}
public void typeSearchTerm(String searchTerm) {
q.sendKeys(searchTerm);
}
public void clickOnSearch() {
btn.click();
}
}
[/sourcecode]WebDriver provides a way to map it to a
real web page. The PageFactory class provides a convenient way
of initializing and mapping the Page Object fields.[sourcecode language="java"
wraplines="false" collapse="false"]
public class WhenAUserSearchesOnGoogle {private GoogleSearchPage page;@Before
public void openTheBrowser() {
page = PageFactory.initElements(new FirefoxDriver(), GoogleSearchPage.class);
page.open("http://google.co.in/");
}@After
public void closeTheBrowser() {
page.close();
}@Test
public void whenTheUserSearchesForSeleniumTheResultPageTitleShouldContainCats()
{
page.searchFor("selenium");
assertThat(page.getTitle(), containsString("selenium") );
}
}
[/sourcecode]By default, it will map Page Object
properties to fields with matching ids or names, so the example given here will
work fine out of the box. But sometimes we need more control over identifying
elements in the HTML page and mapping them to our Page Object fields. One way
to do this is to use the @FindBy annotation, as shown in the
following code:[sourcecode language="java"
wraplines="false" collapse="false"]
public class GoogleSearchPage {protected WebDriver driver;@FindBy(name="q")
private WebElement searchField;@FindBy(name="btnG")
private WebElement searchButton;public
AnnotatedGoogleSearchPage(WebDriver driver) {
this.driver = driver;
}public void open(String url) {
driver.get(url);
}public void close() {
driver.quit();
}public String getTitle() {
return driver.getTitle();
}public void searchFor(String searchTerm)
{
searchField.sendKeys(searchTerm);
searchButton.click();
}public void typeSearchTerm(String searchTerm)
{
searchField.sendKeys(searchTerm);
}public void clickOnSearch() {
searchButton.click();
}
}
[/sourcecode]
Page ObjectsThe Page Objects design pattern is outlined in the Selenium wiki, but to summarize, Page Objects are meant to encapsulate the messy internal state of a page. Changes in the presentation code should only require changes only to the Page Objects, not to the actual test code. Using a Page Object promotes consistency; there may be five different ways to legitimately determine that you are on the login page, but adhering to the one definition in the Page Object will prevent you from having to maintain the other variants.
Page Objects should be ignorant of an application’s business logic, they should only be aware of page state and how to interact with it. By clearly delineating the test code from the page objects, you will be able to use the same page objects in a variety of tests cases and achieve code re-use.
The Sample Code
The application that we will be using is the Spring Security Contact Example that I deployed locally in a Tomcat 6 container. No modifications of the web archive are necessary.
The first two classes are Page Objects: the Login Page and the Home Page. Both pages validate that the page is currently loaded by checking the current page’s title. A Page Object’s validation code may execute before the page has been loaded in the browser, so we will leverage the WebDriver asynchronous validation mechanism. Both Pages will wait up to thirty seconds, polling every two seconds to see if the expected title is present before giving up. WebDriver used to have a somewhat klunky way of doing this, but the new FluentWait and ExpectedConditions makes this code much more concise and readable.
LoginPage.java
- public class LoginPage {
- private final WebDriver driver;
- public LoginPage(WebDriver driver) {
- super();
- this.driver = driver;
- Wait wait = new FluentWait(driver)
- .withTimeout(30, SECONDS)
- .pollingEvery(2, SECONDS);
- wait.until(ExpectedConditions.titleIs("Login"));
- }
- public HomePage loginAs(String username, String password) {
- executeLogin(username, password);
- return new HomePage(driver);
- }
- public void failLoginAs(String username, String password) {
- executeLogin(username, password);
- }
- private void executeLogin(String username, String password) {
- driver.findElement(By.name("j_username")).sendKeys(username);
- driver.findElement(By.name("j_password")).sendKeys(password);
- driver.findElement(By.name("submit")).submit();
- }
- public String getErrorMessage() {
- return driver.findElement(By.xpath("/html/body/p/font[@color='red']")).getText();
- }
- }
HomePage.java
- public class HomePage {
- private final WebDriver driver;
- private final static Logger logger = LoggerFactory
- .getLogger(HomePage.class);
- public HomePage(WebDriver driver) {
- super();
- this.driver = driver;
- Wait wait = new FluentWait(driver)
- .withTimeout(30, SECONDS)
- .pollingEvery(2, SECONDS);
- wait.until(ExpectedConditions.titleIs("Your Contacts"));
- }
- public String getHomePageWelcomeMessage() throws Exception{
- return driver.findElement(By.xpath("/html/body/h1")).getText();
- }
- }
Each Page Object also contains an additional method that checks for the presence or content of an element. The Page Object only locates and retrieves these values. We will let the test case interpret the correctness of these values.
I’ve created a single junit test that will test a successful and unsuccessful login attempt. The test instantiates a Firefox WebDriver to pass to the Page Objects. In a more sophisticated test suite you would inject the WebDriver implementation class to leverage the same test against different browsers. To start each test, we navigate to a protected URI. This should trigger the authentication process and bring the user to the login page. After each test we make an attempt to clean up by accessing the logout URI and cleaning up any session cookies that remain in the browser. Finally, after all of the tests are completed, we gracefully shutdown the WebDriver.
LoginTest.java
- public class LoginTest {
- private final static Logger logger = LoggerFactory.getLogger(LoginTest.class);
- private final static WebDriver driver = new FirefoxDriver();
- private final static String hostAndPortAndContext = "http://localhost:8081/spring-security-samples-contacts-3.0.8.CI-20110909.121734-1";
- private final static String securedURI = "/secure/index.htm";
- private final static String logoutURI = "/j_spring_security_logout";
- @AfterClass public static void afterAllIsSaidAndDone() {
- driver.quit();
- }
- @After
- public void after() {
- driver.get(hostAndPortAndContext+logoutURI);
- driver.manage().deleteAllCookies();
- }
- @Before
- public void before() {
- driver.get(hostAndPortAndContext+securedURI);
- }
- @Test
- public void testLogin() throws Exception {
- LoginPage loginPage = new LoginPage(driver);
- HomePage homePage = loginPage.loginAs("rod", "koala");
- assertEquals("rod's Contacts",homePage.getHomePageWelcomeMessage());
- }
- @Test
- public void testFailedLogin() throws Exception {
- LoginPage loginPage = new LoginPage(driver);
- loginPage.failLoginAs("nobody", "WRONG");
- assertTrue(loginPage.getErrorMessage().contains("Reason: Bad credentials."));
- }
- }
Running the JUnit test will spawn an instance of Firefox to run the two test cases. The browser will use a unique profile, so your tests will not be affected by your current browser cache or configuration.
Shortcomings of Page Objects and Web Driver
If you change the password of the valid user and re-run the test, you will need to wait the entire duration of the timeout period (currently thirty seconds) before moving onto the next test. This is because the Home Page is only checking to see if the Home Page has loaded, not if the login page is being re-displayed with an error. The problem with this is twofold
- it is unclear as to why a test failed. You are only given a stack trace that indicates the expected Home Page element could not be loaded within the timeout period. But what happened? Was the account locked, was the server not up, or did the application blow up? If you are watching the tests run, the underlying cause of failure may be obvious, but if you are running the tests on a CI server, or executing them remotely with Selenium Grid. it may be difficult to diagnose.
- it takes a long time for the test to fail. If the failure is on a main navigation path (e.g. Login) it will take a long time for all of the tests to fail. Automated test suites that used to take 10 minutes to run on the CI server may now take several hours to complete before you are notified that something is wrong.
Another concern is that we are now coupling page state/interaction with page flow. The Login page executes the same login logic twice, once for failed logins, the other for successful logins. Both methods do the same thing, but have different return types as the expected navigation outcome will be different. So despite our best intentions, the validation of the business logic has leaked into the page objects.
We could code the page flow into a page builder class that can detect unexpected navigation flow. This class could detect a failed user login due to a bad password. But this solution quickly increases the complexity of the code. The tidy one liner check now becomes a complicated check for the presence of a login error message in a try catch block. Using a builder for main navigation flows (e.g. login) may have merit, but it becomes overkill to use as the default pattern.
Such a page builder class would not catch all unexpected navigation errors. Unless you’ve defined custom error pages, the container can also render error pages. These error pages are container specific; Tomcat will generate a different 404 page than Websphere. Browsers can also render error pages. For example, if the browser is unable to connect to a host the browser will render a page indicating the failure. The list of possible unexpected pages is quickly growing.
It would be nice if the actual Driver implementation could identify browser generated error pages. It would also be possible to expose the HTTP Response code to identify server side errors. Unfortunately, it does not appear that this functionality will be appearing in Web Driver any time soon.
It might be difficult to detect errors, but when they happen you can at least get the current content of the page. This can be done by logging the contents of the frame with driver.getPageSource() or grabbing a screenshot with getScreenshotAs(). The thought of associating screen shots to a test sound appealing and all of the major drivers (IE, Chrome and Firefox) support the getScreenShotAs method. But it’s difficult to a harvest screenshots for inspection after a remote test run. For the most part, taking a current page source dump should be sufficient to indicate current page state.
Conclusions
Page Objects do a good job of encapsulating the underlying page source, but not as well at modeling navigation flow. Web Driver 2.0 and Selenium are continually improving. Hopefully they will continue to do so by adding some of the missing features.
If you are not yet using PageObjects, consider doing so. If you are not yet using Selenium, what are you waiting for?
No comments:
Post a Comment