Upload
yasha-kramarenko
View
1.510
Download
1
Embed Size (px)
DESCRIPTION
***VIDEO***: view in SD at http://youtu.be/HPHKeBakulQ or download in HD at http://bit.ly/1nyvA67 Often we have lack of automation resources. If we just would involve less experienced juniors to implement test model and even Manual QA to write DSL like tests… In this talk I want to present the simplified approach to write PageObjects for your test model as it would be like playing “three chords” song on a guitar. And also share the experience of pacifying the LoadableComponent pattern, rather hard in implementation but making your tests much more DRY and easy to use in context of loading pages.
Citation preview
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