Three simple chords of Alternative “PageObjects” and Hardcore of LoadableComponents Iakiv Kramarenko
May 06, 2015
Three simple chords of Alternative “PageObjects” and Hardcore of
LoadableComponents
Iakiv Kramarenko
Conventions :)
● Sympathy colors***:
Green =
Orange =
Red =
*** often are subjective and applied to specific context ;)
● At project with no Web UI automation, no unit testing
● With 4(5) Manual QA
– Using checklists + long detailed End to End test cases
● With 1 QA Automation found
Testing single page web app● Ajax
● only <div>, <a>, <input>
– Inconsistent implementation
● No confident urls
● One current Frame/Page with content per user step
– Deep structure: ● Authorization > Menus > SubMenus > Tabs > Extra > Extra
– Modal dialogs
Met Requirements● Automate high level scenarios
– use AC from stories
– existed Manual Scenarios
● Use 3rd party solutions
– Java Desired
● Involve Manual QA
– provide easy to use solution
– BDD Desired
● In Tough Deadlines :)
Dreaming of framework...● Fast in development
– Using instruments quite agile to adapt to project specifics
● Extremely easy to use and learn
● With DRY and handy
page loading
● Simple DSL for tests
● BDD – light and DRY as much as possible
Choosing Programming ParadigmFor WebUI Automation
Based on https://bitbucket.org/yashaka/oopbucket/src
OOP or not to OOPThat is the question
OOP can impose you to● Learn much
– Concept itself
– Design Patterns
● Have bulky structured implementation
– Coupled via inheritance
– Having too many layers of abstractions
● Work harder to implement DSL
Do you need this?
OOP can give you● Batch common operations on pages/steps ***
E.g.– Reporting per steps
– abstract open() per page
– abstract getExpectedElements() per page
● Obligations over conventions
● Certainty in future refactoring
Do you need this?
Sometimes...● Batch/common operations may be redundant for pages/steps
– Sufficient reporting can be implemented in low-level libraries
– “batch” open() may be called on LoadableComponent separately
– IHaveExpectedElements may give no advantages for smoke testing in your project
● And still can be implemented separately in e.g. LoadableComponent
● Or via Reflection
– Some “common” implementation can be moved from pages to “widgets” and still be implemented with OOP
Sometimes...● Conventions can be very easy
● No severe refactoring is coming
– Test Automation Project is not a NASA Space Shuttle ;).
So Think Always!
Procedural
Functional
OOP
And Balance!
Classic PageObject Pattern
public class LoginClassicPageObject extends BasePage {
@FindBy(css = "#login-form") private WebElement container;
@FindBy(name = "username") private WebElement usernameField;
@FindBy(name = "password") private WebElement passwordField;
@FindBy(css = ".ui-button[value='Log in']") private WebElement loginButton;
public void WebElement getContainer(){ return container; } @Override public void open(String baseurl) { driver.get(baseurl); }
public void doLogin(String login, String pass){
usernameField.sendKeys(login);
passwordField.sendKeys(pass);
loginButton.click();
}
public LoginClassicPageObject(
WebDriver driver, String baseurl) {
PageFactory.initElements(driver, this);
this.driver = driver;
this.baseurl = baseurl;
}
private String baseurl;
private WebDriver driver;
}
Involving
public class LoginSelenidePageObject extends BasePage {
private final String container = "#login-form";
private final By username = By.name("username");
private final By password = By.name("password");
private final String loginButton = ".ui-button[value='Log in']";
public void SelenideElement getContainer(){
return $(container);
}
@Override
public void open(String baseurl) {
open(baseurl);
}
public void doLogin(String login, String password){
$(username).setValue(login);
$(password).sendKeys(password);
$(loginButton).click();
}
public LoginSelenidePageObject(String baseurl) {
this.baseurl = baseurl;
}
private String baseurl;
}
public class LoginSelenidePageObject2 extends BasePage {
public void SelenideElement сontainer(){ return $("#login-form");}
public void SelenideElement usernameField(){ return $(By.name("username"));}
public void SelenideElement passwordField(){ return $(By.name("password"));}
public void SelenideElement loginButton(){ return $(".ui-button[value='Log in']");}
@Override
public void open(String baseurl) {
open(baseurl);
}
public void doLogin(String login, String password){
usernameField().setValue(login);
passwordField().setValue(password);
loginButton.click();
}
public LoginSelenidePageObject(String baseurl) {
this.baseurl = baseurl;
}
private String baseurl;
}
“Procedural” approach to implement “PageObjects”
PageUtilspublic class Login{
public static void open(String baseurl) { Selenide.open(baseurl); }
public static SelenideElement container() { return $("#login-form");}
public static SelenideElement usernameField(){ return $(By.name("username"));} public static SelenideElement passwordField(){ return $(By.name("password"));} public static SelenideElement loginButton(){ return $(".ui-button[value='Log in']");}
public static void doLogin(String login, String password){ usernameField().setValue(login); passwordField().setValue(password); loginButton().click(); }}
AlternatIvE
Or...public class Login{
public static void open(String baseurl) { Selenide.open(baseurl); }
public static final String container = "#login-form"; public static final By username = By.name("username"); public static final By password = By.name("password"); public static final String loginButton = ".ui-button[value='Log in']";
public static void doLogin(String login, String password){ $(username).setValue(login); $(password).sendKeys(password); $(loginButton).click(); }}
AlternatIvE
PageUtils usageLogin.open(baseurl);
Login.doLogin(username, password);
Home.addProduct("Product_1");UserPanel.doLogout();
Login.container().shouldBe(visible);
PageObjects usageloginPage = new LoginPage(baseurl);loginPage.open();loginPage.doLogin(username, password);
homePage = new HomePage();homePage.addProduct("Product_1");homePage.doLogout();loginPage.getContainer().shouldBe(visible);
Compare
Three Chords of “Procedural” PageUtils :)
1. Abstraction
Factor out implementation details into helper methods
doLogin(username, password);
public static void doLogin(String login, String password){ usernameField().setValue(login); passwordField().setValue(password); loginButton().click();}
public static SelenideElement usernameField(){ return $(By.name("username"));}
2. Modularity
Collect your helpers in classes of correspondent context
3. Try to Be “Functional”
– write functions returning result only based on passed parameters
– write smaller functions and use them in a 'chain':
select(dropdownIn(userPanel()), “En”)
– Instead of
selectLanguageDropdownInUserPanel(“En”)
– Use Composition over Inheritance
P.S. Be smart ;)
– You can't use inheritance.● If you have any conventions you need to remember to
follow them
When Use?
● Need to involve and teach Manual/Junior Automation QA
● Need a fast solution
● Language support Functional Paradigm
– At least first-class functions
● You know what you do:)
When maybe not use?
● All committers to test framework are either Senior QA Automation or Developers
● No need to teach Manual QA/Juniors
● No tough deadlines
● Java (only)
When not use?
● Your are Junior/Middle
– And/Or Manager/Lead/Dev says: OOP or DIE! :)
● You can't predict what features your framework may need in future
This is how your test
model may look
“What are those classes
in pagegetters package?”
:)
Here comeLoadable Components...
public abstract class SimpleLoadableComponent { public void get() { try { isLoaded(); } catch (Error e) { load(); isLoaded(); } }
protected abstract void load();
protected abstract void isLoaded() throws Error;}
What's the point?O_o
From :(
Login.open(baseurl);
Home.open(username, password);
Home.ensureHasProduct("Product_1");
Product.open("Product_1");
ProductTestTables.open();
ProductTestTables.addCategoryButton().shouldBe(visible);
Technically To
(new ProductTestTablesPage(
new ProductPage(
new HomePage(
new LoginPage(baseurl), username, password),
"Product_1"))).get();
Actually To :)
ProductTestTables.page("Product_1").get();
Selenium LoadableComponentpublic abstract class LoadableComponent<T extends LoadableComponent<T>> {
@SuppressWarnings("unchecked")
public T get() {
try {
isLoaded();
return (T) this;
} catch (Error e) {
load();
}
isLoaded();
return (T) this;
}
protected abstract void load();
protected abstract void isLoaded() throws Error;
}
Ajax? > Selenium SlowLoadableComponent
Calm down, no code, just link:)
● (c) A LoadableComponent which might not have finished loading when load() returns. After a call to load(), the isLoaded() method should continue to fail until the component has fully loaded.
Once you need some abstract classes to DRY your code...
public abstract class
AbstractPage<T extends SlowLoadableComponent<T>> extends SlowLoadableComponent<T>{
O_O
Typical isLoaded() Implementations
'url-based' isLoaded implementation
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
assertTrue(url.contains(pageUrl));
}
If you want to identify pages by actual content
protected void isLoaded() throws Error {
try {
WebElement div = driver.findElement(By.id("login-select"));
} catch (NoSuchElementException e) {
fail("Cannot locate user name link");
}
}
Once you use @FindBy
public void isLoaded() throws Error {
if (loginButton != null) {
assertElementIsDisplayed(loginButton);
} else {
fail("Login button is not loaded");
}
}
Typical Selenide isLoaded() implementation
public void isLoaded(){
Login.container().shouldBe(visible);
}
Selenide LoadableComponent
public abstract class SelenideLoadableComponent {
public void get() {
long originalTimeout = Configuration.timeout; try {
Configuration.timeout = 0; isLoaded(); Configuration.timeout = originalTimeout; } catch (Error e) {
Configuration.timeout = originalTimeout; load(); isLoaded(); } } protected abstract void load(); protected abstract void isLoaded();}
“slow”,ajax-friendly by default
“no” <T extends Madness>
If you wish...
public abstract class AbstractPage extends
SelenideLoadableComponent {
public abstract void isLoaded();
}
Home.page().get();
doCrazyStuff();
Home.page().isLoaded() // still?
Implementation Example
Initialize
public class ProductPage extends SelenideLoadablePage{
private HomePage parent;
protected String productName;
public ProductPage(HomePage parent, String productName){
this.parent = parent;
this.productName = productName;
}
….
Load
protected void load() {
parent.get();
Home.ensureHasProduct(productName);
Product.open(productName);
}
isLoaded()
public void isLoaded() {
Breadcrumb.productLink(productName).shouldBe(visible);
Product.testTableItem().shouldBe(visible);
}
Factory
public class Product {
public static ProductPage page(String productName){
return new ProductPage(Home.page(), productName);
}
…}
If it would be so “simple”...
But it would not :ppublic class AuthorizedPage extends AbstractPage {
protected AbstractPage parent;
private String username; private String password;
public AuthorizedPage( LoginPage parent, String username, String password) {
this.parent = parent; this.username = username; this.password = password; }
public AuthorizedPage(AuthorizedPage parent){
this.parent = parent; } ...
Initializepublic class ProductPage extends AuthorizedPage{
protected String productName;
public ProductPage(HomePage parent, String productName){
super(parent);
this.productName = productName;
}
public ProductPage(ProductPage parent){ //It's possible to “load” page from itself
super(parent);
this.productName = parent.getProductName();
}
….
Load protected void load() {
parent.get();
if (parent instanceof ProductPage) {
Breadcrumb.productLink(((ProductPage) parent).getProductName()).click();
} else { //parent instanceof HomePage
Home.ensureHasProduct(productName);
Product.open(productName);
}
}
Though...The beast is not so scary after you write up to 10 first Lions Components :)
AndYou still can live only with PageUtils and keep LoadableComponents as options to be implemented by crazy devs:)
QA Dev
scenario "Surf Pages", { where "Page is #page", { page = [ Login.page(), Home.page(), Settings.page(), Login.page(Authorized.page()), Product.page(TEST_PRODUCT), Login.page(Authorized.page()), Settings.page(), Product.page(Home.page(Settings.page()), "Product_1"), Product.page(Product.page(TEST_PRODUCT)), ProductTestTables.page(TEST_PRODUCT), Login.page(Authorized.page())] } then "go to #page", { page.get() }}
Bonus :)
When Maybe Use?
● No confident urls
● Complex “deep” page hierarchy
● Authorization > Menus > SubMenus > Tabs > Extra > Extra...
When Use?
● Desired dependent End to End scenarios with “walking through pages” feature
– emulating real user experience
– big amount of such test cases
When maybe not use?
● Too many ways to load the same page
● Though you still can implement LC for 1 way, if you need to use it often.
● Too many pages, especially “visible” at the same time
When not use?
● URL-based loading is enough
– Or work around via custom JS load helpers is enough● what is true for most cases...
● Have no “deep” pages
All Together
● PageUtils:
class Login
– Page smart loader:
Login.page().get()
– Page root html element:
Login.container()
– Method to open Page once preconditions satisfied:
Login.open()
– Page elements:
Login.signInButton()
– Page business steps:
Login.doLogin(“guest”, “1234”)
● LoadableComponent:
class LoginPage
isLoaded()
load()
Conventions
Ideas to think about
LoadableComponent
● Is not PageObject
– Though you can integrate it into PageObject, violating Single Responsibility principle
● It's an object incapsulating page loading logic.
– Initializing the “loading way” through LC constructor● It's possible also to move logic into separate loadable
components fro each “way”, though this may lead to overcomplicated hierarchy
– choosing the “way” in load() implementation
– And then just get() your page
LoadableComponent Integration
● PageUtils + LoadableComponent
– Two classes instead of one
● PageObject + LoadableComponent
– May be harder to achieve friendly tests code
● PageObject extends LoadableComponent
– Bulky
– Harder to explain to juniors/interns
– Violates Single Responsibility Principle
LoadableComponent Factory
Too more calls to page() ?
Use Singleton Pattern
PageUtils
Page elements as functions public static SelenideElement usernameField(){
return $(By.name("username"));
}
…
Login.usernameField().setVal("guest");
Page elements as locators public static final By usernameField = By.name("username");
…
$(Login.usernameField).setVal("guest");
Compare
Functional “Scales”
● Main cons of Procedural approach is that it may be not DRY
● In most cases you can fix this with high-order functions in much more concise way than with OOP
– Though less obvious for non-FP user
Ideas for improvements● Use Groovy as main language
– in order to simplify implementation.
– Finally Java is the OOP language● and not adapted for both procedural and functional styles.
– In Groovy OOP may be not “bulky”
● and with some functional & metaprogramming features you can achieve the same level of simplicity still powerful
– and easy to explain to juniors “how to use” (though not “how to understand details”)
Did it work for Manual QA?
Demo
Q&A
Resources, Links
● Src of example test framework:
https://github.com/yashaka/gribletest
● Programming Paradigms Comparison: https://bitbucket.com/yashaka/oopbucket/src
● Functional Thinking articles: http://www.ibm.com/developerworks/views/java/libraryview.jsp?search_by=functional+thinking
● Application under test used in easyb examples: http://grible.org/download.php
● Instruments
– http://selenide.org/
– http://code.google.com/p/selenium/wiki/LoadableComponent
● To Artem Chernysh for implementation of main base part of the test framework for this presentation
– https://github.com/elaides/gribletest● To Maksym Barvinskyi for application under test
– http://grible.org/
Contacts
● skype: yashaolin
● twitter: @yashaka
● http://www.linkedin.com/in/iakivkramarenko