page objects done right - selenium conference 2014
DESCRIPTION
Presentation by Oren Rubin on Page Object presented at the seleniumconf2014TRANSCRIPT
Page ObjectsOren Rubin Testim.io
About Me
Talk Topics
● Page Objects
○ Why?
○ How?
○ When?
● Synchronously
○ Integrate in Page Objects
○ Remove magic sleep
Talk Topics (we won't discuss)
● Locators - best practice
● Retrys
○ Locator retry (SPA)
○ Entire Test (stability)
Page Objects?
A Design Pattern.
Provides a programmatic API to drive and interrogate a UI
Naming things is hard
● Originally "Window Drivers" - Martin Fowler, 2004
● Only pages? what about:
○ Header/footer
○ Components/widgets
○ Simple HTML elements (e.g., Tables)
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
Step 1 - Expose The Service
Step 1 - Expose The Service
<form id="gaia_loginform">
<input id="email">
<input id="passwd">
<input id="signIn" type="submit">
</form>
Step 1 - Expose The Service
void testMyApp() {
// login
driver.findId("email").setText("[email protected]");
driver.findId("passwd").setText("12345");
driver.findId("signIn").click();;
sleep(3000); // wait till page loads
assert(...) // my assertions}
Step 1 - Expose The Service
void testMyApp() {
// login
driver.findId("email").setText("[email protected]");
driver.findId("passwd").setText("12345");
driver.findId("signIn").click();;
sleep(3000); // wait till page loads
assert(...) // my assertions}
Simon says no!
Test Automation
testMyApp() { account.login();
gallery.showImage()
}
Business LogictestMyApp() { account.login();
gallery.showImage()
}
the implementation of the automatic execution of some Business Logic
ImplementationClass LoginPage() { login() { // selenium code }}
Class GalleryPage() { showImage() {...}}
Step 1 - Expose The Service
loginPage = new LoginPage();
loginPage.login();
public class LoginPage {
public login();
}
Step 1 - Expose The Service
loginPage = new LoginPage();
loginPage.login();
// compilation error: missing return type
public class LoginPage {
public ? login();
}
Step 1 - Expose The Service
Option 1: void
public class LoginPage {
public void login();
}
Step 1 - Expose The Service
void testMyApp() {
// TODO move to @setup
loginPage = new LoginPage();
loginPage.login();
sleep(3000); // wait till page loads
assert(…)
}
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?
Step 1 - Expose The Service
Option 2 (improved): Return a page object
public class LoginPage {
public GalleryPage login() {{…
return new GalleryPage();}
}
Step 1 - Expose The Service
void testMyApp() {// TODO consider moving to @before
loginPage = new LoginPage();
galleryPage = loginPage.login();
sleep(3000);
galleryPage.showImageFullscreen();assert(…)
}
Q: What's the source of all evil?
"No more war - no more blood shed"
A: Random waits
"No more war - no more blood shed"
random sleep
Abie NathanThe voice of peace
Step 2 - Eliminate random sleep
void testMyApp() {
loginPage = new LoginPage();
galleryPage = loginPage.login();
sleep(3000); // should we move it?galleryPage.showImageFullscreen();
assert(…)}
Step 2 - option 2
public class LoginPage {
public GalleryPage login() {…
sleep(3000);
return new GalleryPage();}
}
Step 2 - option 2
void testMyApp() {
loginPage = new LoginPage();
// synchronous for testers
galleryPage = loginPage.login();
galleryPage.showImageFullscreen();
assert(…)}
Step 2 - back to option 1
public class LoginPage {void login() {…}
}
public class GalleryPage { void showImageFullscreen() {…}
static GalleryPage waitForPage() {…}}
Step 2 - back to option 1
void testMyApp() {
loginPage = new LoginPage()
loginPage.login(); // login() is void
galleryPage = GalleryPage.waitForPage();
galleryPage.showImageFullscreen();
assert(…)}
Step 2 - Combining options 1+2
public class LoginPage {
public GalleryPage login() {…
// return new GalleryPage();
return GalleryPage.waitForPage();}
}
Step 2 - Force API comformance
public class LoginPage {static LoginPage waitForPage() {…}GalleryPage login() {…}
}
public class GalleryPage {static GalleryPage waitForPage() {…}void showImageFullscreen() {…}
}
Step 2 - Basic code reuse
abstract class BasicPage {// force derived classes
public static BasicPage waitForPage();}
public class GalleryPage {public static BasicPage waitForPage() {…}
}
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!
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!
Step 2 - Basic code reuse
abstract class BasicPage {// force derived classes to implement
public BasicPage waitForPage();}
public class GalleryPage {public GalleryPage waitForPage() {…}
}
Step 2 - Basic code reuse
Another option is to use c'tor as wait
public class GalleryPage {GalleryPage() {
sleep(3000);
}
}
Step 2 - Basic code reuse
Tip!Add all common utilities to base class
abstract class BasicPage {public BasicPage waitForPage();
public void waitForSpinnerToFade();}
What's next?
Support Different Users
Step 3 - Params and overload
Sounds simple!
public class LoginPage {
public GalleryPage login(user, password);
}
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();
}
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
Step 3 - Params and overload
Compiles successfully
public class LoginPage {
public GalleryPage loginAsRegular(…); public OtherPage loginAsAdmin(…); public LoginPage loginAsBadCredintial(…);}
Step 3 - Overloading Philosophy
The LoginPage is used for:1. Setup
Drive the app to a specific stateNo one cares about the implementation
2. Test the Login page itselfTest the specific implementation
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
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
Step 3 - Overloading Philosophy
Should we put everything together?
class LoginPage {
login() {}
login(username, password){}
}
Step 3 - Overloading Philosophy
Do we want more abstraction
interface LoginPage { login();
LoginPageDriver getDriver();}
interface LoginPageDriver { login(username, password);
}
Step 3 - Overloading Philosophy
class LoginPageImpl implements LoginPage, LoginPageDriver { login() {}
login(username, password);
LoginPageDriver getDriver() {return this;
}
}
Step 4 - Page Factory
public class LoginPage {
private WebDriver driver;
public LoginPage(driver) {
this.driver = driver;
}
public GalleryPage login(username, password) {
driver.findId("email").setText(username);
…
}
Step 4 - Page Factory
public class LoginPage {
private WebElement email;
public LoginPage(driver) {
email = driver.findById("email");
}
public GalleryPage login(username, password) {
email.setText(username);
…
}
Step 4 - Page Factory
public class LoginPage {
private WebElement email;
private WebElement password;
public LoginPage(driver) {
email = driver.findById("email");
password = driver.findById("password");
// Linting error! too much glue code
}
}
Step 4 - Page Factory
public class LoginPage {
private WebElement email;
private WebElement password;
/* look ma.. no ctor! */
}
// loginPage = new LoginPage();
loginPage =
PageFactory.initElement(driver, LoginPage.class)
Step 4 - Page Factory
Recommended way - in c'tor
public class LoginPage {
LoginPage() {
// wait till ready
sleep(3000);
// init
PageFactory.initElement(driver, this)
}
}
Step 4 - Page Factory Pseudo Code
class PageFactory {
public static initElements(driver, class){
obj = class.new();
obj.properties.forEach(function(property) {
if (property.type === WebElement.class) {
byId = driver.findById(property.name);
byName = driver.findByName(property.name);
obj[property.name] = byId || byName
}
});
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
}
Step 4 - Page Factory
Annotations to the rescue!
public class GoogleLoginPage {
@FindBy(how = How.name, using = "q")private WebElement searchBox;
}
Step 4 - Page Factory
Shorthand FTW!
public class GoogleLoginPage {
@FindBy(name = "q")private WebElement searchBox;
}
Supports id, tagName and custom annotations!
Step 4 - Assertions
Two options● Separate from Page Objects
Community Recommends
● Inside Page ObjectMaybe inside the BasicPage class
Off topic - No more sleep
public class GalleryPage {
public void waitForPage() {sleep(3000);
}
}
Off topic - No more sleep
Some solutions1. 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
The Coordinator
login()waitForTestEvent('logged')
gallery.showImage()
// injected codesetInterval( function({ if (works) { // we're done callback(); }), 500ms)
The Coordinator
Option 1 - JavascriptThe API driver.executeAsyncScript("some js.. callback()");
Translation - browser runsfunction executeAsync(codeToEval, callback) {
// evaluated code has access to callbackeval(codeToEval);
}
The Coordinator
Option 1 - JavascriptThe API driver.executeAsyncScript("some js.. callback()");
Translation - browser runsfunction executeAsync(codeToEval, callback) {
// name 'callback' might change. last param guaranteed though
eval(codeToEval);}
The Coordinator
driver.executeAsyncScript("some js.. // callback()var lastIndex = arguments.length;var workingCallback = arguments[lastIndex]
workingCallback(); // nowsetTimeout(workingCallback, 5000); // later
");
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)");
}
The Coordinator - option 2
login()waitForTestEvent('logged')
gallery.showImage()
// load things…// readysendTestEvent('logged')
The Coordinator - option 2
Testers wait for a known event
public class LoginPage {public GalleryPage login() {
…
waitForTestEvent('gallery-ready')
return new GalleryPage();}
}
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>
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);
}
}
The Coordinator - option 2
Dev add html element in test mode
function loadGalleryPage() {
callServer(function() {
// page is loaded
testing.sendTestEvent('gallery-ready')
})
}
The Coordinator - option 2
Dev add html element in test modeclass Testing {
sendTestEvent: function(ev) {
if (!app.isInTest){
return;
$('#test').append('<div class="+ev+">')
})
}Illustration only, don't use jQuery. Use Angular.js / Ember.js
अंत!
Thank You!
Oren Rubin
Testim.io | shexman@gmail | @shexman | linkedin