The document discusses page object design patterns for test automation. It describes four main steps: 1) Expose the service or page being tested rather than using WebDriver APIs directly in tests. 2) Eliminate random sleeps by adding wait methods. 3) Support different users by overloading methods or using parameters. 4) Initialize page objects using a page factory to avoid duplicating lookup code. It also discusses using coordinators to synchronize tests by waiting for events from the application under test.
4. Talk Topics (we won't discuss)
● Locators - best practice
● Retrys
○ Locator retry (SPA)
○ Entire Test (stability)
5. Page Objects?
A Design Pattern.
Provides a programmatic API to drive and
interrogate a UI
6. Naming things is hard
● Originally "Window Drivers" - Martin Fowler, 2004
● Only pages? what about:
○ Header/footer
○ Components/widgets
○ Simple HTML elements (e.g., Tables)
7. Page Object Pattern
Expose the service you're interacting with, not the
implementation. -- Selenium Wiki
If you have a WebDriver APIs in your test methods...
You're doing it wrong. -- Simon Stewart
9. Step 1 - Expose The Service
<form id="gaia_loginform">
<input id="email">
<input id="passwd">
<input id="signIn" type="submit">
</form>
10. Step 1 - Expose The Service
void testMyApp() {
// login
driver.findId("email").setText("u@app.com");
driver.findId("passwd").setText("12345");
driver.findId("signIn").click();;
sleep(3000); // wait till page loads
assert(...) // my assertions
}
11. Step 1 - Expose The Service
void testMyApp() {
// login
driver.findId("email").setText("u@app.com");
driver.findId("passwd").setText("12345");
driver.findId("signIn").click();;
sleep(3000); // wait till page loads
assert(...) // my assertions
}
Simon says no!
12. Test Automation
the implementation of the automatic execution of some Business Logic
Business Logic testMyApp() {
testMyApp() {
account.login();
gallery.showImage()
}
Implementation
Class LoginPage() {
login() {
account.login();
// selenium code
gallery.showImage()
}
}
}
Class GalleryPage() {
showImage() {...}
}
13. Step 1 - Expose The Service
loginPage = new LoginPage();
loginPage.login();
public class LoginPage {
public login();
}
14. Step 1 - Expose The Service
loginPage = new LoginPage();
loginPage.login();
// compilation error: missing return type
public class LoginPage {
public ? login();
}
15. Step 1 - Expose The Service
Option 1: void
public class LoginPage {
public void login();
}
16. Step 1 - Expose The Service
void testMyApp() {
// TODO move to @setup
loginPage = new LoginPage();
loginPage.login();
sleep(3000); // wait till page loads
assert(…)
}
17. Step 1 - Expose The Service
Pro - Only deals with login page
● Don't interleave code relevant to other pages in
this class.
Con - Only deals with login page
● Was the login successful?
● On which page should we be?
● Is the new page ready?
18. Step 1 - Expose The Service
Option 2 (improved): Return a page object
public class LoginPage {
public GalleryPage login() {{
…
return new GalleryPage();
}
}
19. Step 1 - Expose The Service
void testMyApp() {
// TODO consider moving to @before
loginPage = new LoginPage();
galleryPage = loginPage.login();
sleep(3000);
galleryPage.showImageFullscreen();
assert(…)
}
20. Q: What's the source of all evil?
"No more war - no more blood shed"
21. A: Random waits
random sleep
"No more war - no more blood shed"
Abie Nathan
The voice of peace
22. Step 2 - Eliminate random sleep
void testMyApp() {
loginPage = new LoginPage();
galleryPage = loginPage.login();
sleep(3000); // should we move it?
galleryPage.showImageFullscreen();
assert(…)
}
23. Step 2 - option 2
public class LoginPage {
public GalleryPage login() {
…
sleep(3000);
return new GalleryPage();
}
}
25. Step 2 - back to option 1
public class LoginPage {
void login() {…}
}
public class GalleryPage {
void showImageFullscreen() {…}
static GalleryPage waitForPage() {…}
}
26. Step 2 - back to option 1
void testMyApp() {
loginPage = new LoginPage()
loginPage.login(); // login() is void
galleryPage = GalleryPage.waitForPage();
galleryPage.showImageFullscreen();
assert(…)
}
27. Step 2 - Combining options 1+2
public class LoginPage {
public GalleryPage login() {
…
// return new GalleryPage();
return GalleryPage.waitForPage();
}
}
28. Step 2 - Force API comformance
public class LoginPage {
static LoginPage waitForPage() {…}
GalleryPage login() {…}
}
public class GalleryPage {
static GalleryPage waitForPage() {…}
void showImageFullscreen() {…}
}
29. Step 2 - Basic code reuse
abstract class BasicPage {
// force derived classes
public static BasicPage waitForPage();
}
public class GalleryPage {
public static BasicPage waitForPage() {…}
}
30. Step 2 - Basic code reuse
abstract class BasicPage {
// force derived classes
public static BasicPage waitForPage();
}
public class GalleryPage {
public static BasicPage waitForPage() {…}
} Computer says no!
Cannot override static methods!
31. Step 2 - Basic code reuse
abstract class BasicPage {
// force derived classes to implement
public BasicPage waitForPage();
}
public class GalleryPage {
public BasicPage waitForPage() {…}
} Computer says ok!
But could be improved!
32. Step 2 - Basic code reuse
abstract class BasicPage {
// force derived classes to implement
public BasicPage waitForPage();
}
public class GalleryPage {
public GalleryPage waitForPage() {…}
}
33. Step 2 - Basic code reuse
Another option is to use c'tor as wait
public class GalleryPage {
GalleryPage() {
sleep(3000);
}
}
34. Step 2 - Basic code reuse
Tip!
Add all common utilities to base class
abstract class BasicPage {
public BasicPage waitForPage();
public void waitForSpinnerToFade();
}
36. Step 3 - Params and overload
Sounds simple!
public class LoginPage {
public GalleryPage login(user, password);
}
37. Step 3 - Params and overload
void testMyApp() {
// bad password
loginPage = new LoginPage();
loginPage = loginPage.login('a', 'wrong');
// good password
galleryPage = loginPage.login('a', 'correct');
galleryPage.showImageFullscreen();
}
38. Step 3 - Params and overload
What about different roles? what about failures
public class LoginPage {
public GalleryPage login(user, password);
public OtherPage login(user, password);
public LoginPage login(user, password);
}
// compilation error:
can't distinguish overload by return type
39. Step 3 - Params and overload
Compiles successfully
public class LoginPage {
public GalleryPage loginAsRegular(…);
public OtherPage loginAsAdmin(…);
public LoginPage loginAsBadCredintial(…);
}
40. Step 3 - Overloading Philosophy
The LoginPage is used for:
1. Setup
Drive the app to a specific state
No one cares about the implementation
2. Test the Login page itself
Test the specific implementation
41. Step 3 - Overloading Philosophy
1. Setup
// look ma! no params!
loginPage.login();
Implementation might be using
● Username/password
● Cookies
● Google Account
Might be hardcoded, or using config files
42. Step 3 - Overloading Philosophy
2. Test the Login page itself
○ Abstract everything
login(username, password)
○ Act on element wrappers (get/set kind)
Definition: InputDriver getPasswordField()
Usage: loginPage.getPasswordField.set('12345')
* less recommended
43. Step 3 - Overloading Philosophy
Should we put everything together?
class LoginPage {
login() {}
login(username, password){}
}
44. Step 3 - Overloading Philosophy
Do we want more abstraction
interface LoginPage {
login();
LoginPageDriver getDriver();
}
interface LoginPageDriver {
login(username, password);
}
52. Step 4 - Page Factory
But this won't pass any code review
public class GoogleLoginPage {
private WebElement q;
// Linting error! name too short
and non descriptive
}
53. Step 4 - Page Factory
Annotations to the rescue!
public class GoogleLoginPage {
@FindBy(how = How.name, using = "q")
private WebElement searchBox;
}
54. Step 4 - Page Factory
Shorthand FTW!
public class GoogleLoginPage {
@FindBy(name = "q")
private WebElement searchBox;
}
Supports id, tagName and custom annotations!
55. Step 4 - Assertions
Two options
● Separate from Page Objects
Community Recommends
● Inside Page Object
Maybe inside the BasicPage class
56. Off topic - No more sleep
public class GalleryPage {
public void waitForPage() {
sleep(3000);
}
}
57. Off topic - No more sleep
Some solutions
1. The "No smoke without fire" -
Wait for another element we know that loads last
2. The "Take the time"
Wait till state is what you expect (element exists,
row count,..). Selenium's implicit wait helps.
3. The "Coordinator" - Recommended!
Wait for a sign from AUT
59. The Coordinator
Option 1 - Javascript
The API
driver.executeAsyncScript("some js.. callback()");
Translation - browser runs
function executeAsync(codeToEval, callback) {
// evaluated code has access to callback
eval(codeToEval);
}
60. The Coordinator
Option 1 - Javascript
The API
driver.executeAsyncScript("some js.. callback()");
Translation - browser runs
function executeAsync(codeToEval, callback) {
// name 'callback' might change. last param guaranteed though
eval(codeToEval);
}
61. The Coordinator
driver.executeAsyncScript("
some js..
// callback()
var lastIndex = arguments.length;
var workingCallback = arguments[lastIndex]
workingCallback(); // now
setTimeout(workingCallback, 5000); // later
");
62. The Coordinator
The API
driver.executeAsyncScript("some js.. callback()");
Better implementation
function executeAsync(codeToEval, callback) {
// evaluated code has access to callback
var lastArgument = "arguments[arguments.legnth - 1]"
eval(" ( function(callback){" +codeToEval+" } )(lastArgument)");
}
63. The Coordinator - option 2
login()
waitForTestEvent('logged')
gallery.showImage()
// load things
…
// ready
sendTestEvent('logged')
64. The Coordinator - option 2
Testers wait for a known event
public class LoginPage {
public GalleryPage login() {
…
waitForTestEvent('gallery-ready')
return new GalleryPage();
}
}
65. The Coordinator - option 2
Dev add html element in test mode
<body>
<div id="app"> app goes here </div>
<div id="test"> test events go here </div>
</body>
66. The Coordinator - option 2
Imlement waitForTestEvent in base class
abstract class BasicPage {
void waitForTestEvent(eventName) {
By selector = By.CSS("#test ." + eventName)
driver.waitForElement(selector);
WebElement element = driver.find(selector);
driver.removeElement(element);
}
}
67. The Coordinator - option 2
Dev add html element in test mode
function loadGalleryPage() {
callServer(function() {
// page is loaded
testing.sendTestEvent('gallery-ready')
})
}
68. The Coordinator - option 2
Dev add html element in test mode
class Testing {
sendTestEvent: function(ev) {
if (!app.isInTest){
return;
$('#test').append('<div class="+ev+">')
})
}
Illustration only, don't use jQuery. Use Angular.js / Ember.js