Kisspageobjects 01 2017 170113192904

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 132

KISS PageObjects

@yashaka 01.2017
Plan
Intro

Classic examples

KISS PageObjects Demo

Retrospective

Q&A
Afterwords
There are good practices in context,
but there are no best practices.
(c) Cem Kaner, James Bach
Afterwords Preface
There are good practices in context,
but there are no best practices.
(c) Cem Kaner, James Bach
Intro
KISS?
Keep It Simple Stupid!
PageObjects?
Page objects are a classic example of encapsulation - they hide
the details of the UI structure and widgetry from other
components (the tests)

–Martin Fowler
(c) https://martinfowler.com/bliki/PageObject.html
The boring part :p
Classic “Horror” Examples
Classic Usage of PageObjects
@Test

public void search() {

Google google = new Google(driver);

google.open().search("Selenide");

wait.until(numberOf(google.getResults(), 10));

assertThat(google.getResults().get(0).getText(), 

containsString("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

Google google = new Google(driver);

google.open().search("Selenide");

google.followResultLink(0);

wait.until(titleIs("Selenide: concise UI tests in Java"));

}
Classic PageObjects (PageFactory)
public class Google {

private WebDriver driver;

private WebDriverWait wait;


@FindBy(name = "q")

private WebElement searchInput;


@FindBy(css = ".srg>.g")

private List<WebElement> results;


public List<WebElement> getResults() {

return this.results;

}


public Google(WebDriver driver) {

this.driver = driver;

this.wait = new WebDriverWait(driver, 4);

PageFactory.initElements(driver, this);

}

Classic PageObjects (PageFactory)
public class Google {

private WebDriver driver;

private WebDriverWait wait;


@FindBy(name = "q")

private WebElement searchInput;


@FindBy(css = ".srg>.g")

private List<WebElement> results;


public List<WebElement> getResults() {

return this.results;

}


public Google(WebDriver driver) {

this.driver = driver;

this.wait = new WebDriverWait(driver, 4);

PageFactory.initElements(driver, this);

}

Classic PageObjects (PageFactory)

public Google open() {

this.driver.get("http: //google.com/ncr");

return this;

}


public Google search(String text) {

this.searchInput.sendKeys(text + Keys.ENTER);

return this;

}


public void followResultLink(int index) {

wait.until(numberIsAtLeast(results, index + 1))

.get(index)

.findElement(By.cssSelector(".r>a"))

.click();

}

}
PageObjects as LoadableComponents

“The LoadableComponent is a base class that aims to make


writing PageObjects less painful.”

– https://github.com/SeleniumHQ/selenium/wiki/LoadableComponent
Classic LoadableComponent
public class Google extends LoadableComponent<Google>{

…


protected void load() {

this.driver.get("http: //google.com/ncr");

}


protected void isLoaded() throws Error {

String url = driver.getCurrentUrl();

assertTrue("Not on the google page: " + url,
url.contains(" www.google.com"));

}
public class Google {

…


public Google open() {

this.driver.get("http: //google.com/ncr");

return this;

}
Classic LoadableComponent
public class Google extends LoadableComponent<Google>{

…


protected void load() {

this.driver.get("http: //google.com/ncr");

}


protected void isLoaded() throws Error {

String url = driver.getCurrentUrl();

assertTrue("Not on the google page: " + url,
url.contains(" www.google.com"));

}
public class Google {

…


public Google open() {

this.driver.get("http: //google.com/ncr");

return this;

}
LoadableComponent
public class Google extends LoadableComponent<Google>{

…


protected void load() {

this.driver.get("http: //google.com/ncr");

}


protected void isLoaded() throws Error {

String url = driver.getCurrentUrl();

assertTrue("Not on the google page: " + url,
url.contains(" www.google.com"));

}

“The LoadableComponent is a base class that aims to make


writing PageObjects less painful.” o_O ???
“Super-duper” WaitingLoadableComponents
public class Search extends LoadingComponent<Search> {


@FindBy(name = "q")

private WebElement element;


public Search(WebDriver driver) {

super(driver);

}


public boolean isLoaded() {

return element.isDisplayed();

}


public Search query(String text) {

this.element.clear();

this.element.sendKeys(text + Keys.ENTER);

return this;

}

}
“Super-duper” WaitingLoadableComponents
public class Results extends LoadingComponent<Results> {


@FindBy(css = ".srg>.g")

private List<WebElement> elements;


public Results(WebDriver driver) {

super(driver);

}


public boolean isLoaded() {

return this.elements.size() == 10;

}


public void followResultLink(int index){

this.elements.get(index)
.findElement(By.cssSelector(".r>a")).click();

}

“Super-duper” WaitingLoadableComponents
public class GooglePage extends WaitingLoadableComponent<GooglePage> {

private Search search;


public GooglePage(WebDriver driver) {

super(driver);

this.search = new Search(driver);

}


protected void load() {

this.driver.get("http: //google.com/ncr");

}


public boolean isLoaded() {

return this.search.isLoaded();

}


“super” waiting for loading trick
public GoogleSearchResultsPage search(String text) {

this.search.query(text);

return new GoogleSearchResultsPage(this.driver).get();

}

}
“Super-duper” WaitingLoadableComponents
public class GoogleSearchResultsPage 

extends LoadingComponent<GoogleSearchResultsPage> {

private Search search;

private Results results;


public GoogleSearchResultsPage(WebDriver driver) {

super(driver);

this.search = new Search(driver);

this.results = new Results(driver);

}


public boolean isLoaded() {

return this.results.isLoaded();

}


public Results getResults() {

return this.results;

}

}

“Super-duper” WaitingLoadableComponents

all “super” waiting starts here


@Test

public void search() {

GooglePage google = new GooglePage(driver);

GoogleSearchResultsPage resultsPage = google.get().search("Selenide");

assertThat(resultsPage.getResults().size(), equalTo(10));

assertThat(resultsPage.getResults().get(0).getText(),

containsString("Selenide: concise UI tests in Java"));

}

now here we can use common assert without waiting


“The LoadableComponent is a base class that aims to make
writing PageObjects less painful.” o_O ???
WTF?
PageObjects Demo
Demo src log
PageObjects Usage

@Test

public void search() {

new Google().open().search("Selenide");

new SearchResults()

.shouldHaveSize(10)

.shouldHaveResultText(0, "Selenide: concise UI tests in Java");

}


@Test

public void followFirstLink() {

new Google().open().search("Selenide");

new SearchResults().followResultLink(0);

new NewPage().shouldHaveTitle"Selenide: concise UI tests in Java");

}
PageObjects Implementation

public class Google {



public Google open() {

Selenide.open("/ncr");

return this;

}


public void search(String text) {

$(By.name("q")).setValue(text).pressEnter();

}

}
PageObjects Implementation
public class SearchResults {

private ElementsCollection elements(){

return $$(".srg>.g");

}


public void followResultLink(int index) {

this.elements().get(index).find(".r>a").click();

}


public SearchResults shouldHaveSize(int number) {

this.elements().shouldHave(size(number));

return this;

}


public SearchResults shouldHaveResultText(int index, String text) {

this.elements().get(index).shouldHave(text(text));

return this;

}

}
PageObjects Implementation

public class NewPage {



public void shouldHaveTitle(String text) {

Selenide.Wait().until(titleIs(text));

}

}
Retrospective
So why do we need
PageObjects?
Despite of DRYing the code…
So why do we need PageObjects?

Despite of DRYing the code…


Page objects are a classic example of encapsulation - they hide
the details of the UI structure and widgetry from other
components (the tests)

–Martin Fowler
Why may we need Encapsulation?

• Encapsulate for cohesion?

• that’s ok… pretty reasonable to keep related things together


(locators and actions on their elements)
Why may we need Encapsulation?

• Encapsulate to hide something internal to be not broken by client


(tests code)?

• but what may be broken? o_O Have you ever had any thought on
resetting some page field from outside? ;)
Why may we need Encapsulation?
• Encapsulate to hide something internal so on change it will not
broke the client (tests code)? i.e. to provide stable API

• ok… but then next question to think on:

• where is that line which frame the “stable API”?

• maybe it’s less abstract than you got used to…


Why may we need Encapsulation?

• Encapsulate to narrow User Oriented behavioural API to be


readable enough and convenient in usage?

• :)
A point to think about…
“Encapsulate to narrow User Oriented behavioural API to be readable
enough and convenient in usage”

Readable and convenient for whom?


• developers?

• Sr. Automation engineers?

• Jr. Automation engineers?

• Manual Testers?
For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").shouldHave(size(10));

$$(".srg>.g").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").get(0).find(".r>a").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").shouldHave(size(10));

$$(".srg>.g").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test
 API is already readable and simple!
public void followFirstLink() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").get(0).find(".r>a").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").shouldHave(size(10));

$$(".srg>.g").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").get(0).find(".r>a").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}

developers know locators that are already readable for them too:)
For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("query")).setValue("Selenide").pressEnter();

$$(".result").shouldHave(size(10));

$$(".result").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

open("/ncr");

$(By.name("query")).setValue("Selenide").pressEnter();

$$(“.result").get(0).find(".result-link").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}

actually they do make them readable when they write tests ;)


For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").shouldHave(size(10));

$$(".srg>.g").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").get(0).find(".r>a").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}

copy&paste rules! ;)
For “developers”?
@Test

public void search() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").shouldHave(size(10));

$$(".srg>.g").get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

open("/ncr");

$(By.name("q")).setValue("Selenide").pressEnter();

$$(".srg>.g").get(0).find(".r>a").click();

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}

With Find&Replace over DRY ;)


For “Sr. Automation Engineers”?
@Test

public void search() {

Google google = new Google();

google.open().search("Selenide");

google.results().shouldHave(size(10));

google.results().get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

Google google = new Google();

google.open().search("Selenide");

google.followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Sr. Automation Engineers”?
@Test

public void search() {

Google google = new Google();

google.open().search("Selenide");

google.results().shouldHave(size(10));

google.results().get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


 hiding awful locators for “beauty” :D
@Test

public void followFirstLink() {

Google google = new Google();

google.open().search("Selenide");

google.followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Sr. Automation Engineers”?
@Test

public void search() {

Google google = new Google();

google.open().search("Selenide");

google.results().shouldHave(size(10));

google.results().get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

Google google = new Google();

google.open().search("Selenide");

google.followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}

too lazy for locators’ copy&paste:) - autocomplete rules!


Remember classic version?
@Test

public void search() {

Google google = new Google(driver);

google.open().search("Selenide");

wait.until(numberOf(google.getResults(), 10));

assertThat(google.getResults().get(0).getText(), 

containsString("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

Google google = new Google(driver);

google.open().search("Selenide");

google.followResultLink(0);

wait.until(titleIs("Selenide: concise UI tests in Java"));

}
Remember classic version?
public class Google {

private WebDriver driver;

private WebDriverWait wait;


@FindBy(name = "q")
 …
private WebElement searchInput;
 public Google open() {


 this.driver.get("http: //google.com/ncr");

@FindBy(css = ".srg>.g")
 return this;

private List<WebElement> results;
 }


 

public List<WebElement> getResults() {
 public Google search(String text) {

return this.results;
 this.searchInput.sendKeys(text + Keys.ENTER);

}
 return this;


 }

public Google(WebDriver driver) {
 

this.driver = driver;
 public void followResultLink(int index) {

this.wait = new WebDriverWait(driver, 4);
 wait.until(numberIsAtLeast(results, index + 1))

PageFactory.initElements(driver, this);
 .get(index)

}
.findElement(By.cssSelector(".r>a"))


.click();

}

}
Remember classic version?
public class Google {
 locators and actions are bloated with tech details
private WebDriver driver;

private WebDriverWait wait;
 of browser management and waits

@FindBy(name = "q")
 …
private WebElement searchInput;
 public Google open() {


 this.driver.get("http: //google.com/ncr");

@FindBy(css = ".srg>.g")
 return this;

private List<WebElement> results;
 }


 

public List<WebElement> getResults() {
 public Google search(String text) {

return this.results;
 this.searchInput.sendKeys(text + Keys.ENTER);

}
 return this;


 }

public Google(WebDriver driver) {
 

this.driver = driver;
 public void followResultLink(int index) {

this.wait = new WebDriverWait(driver, 4);
 wait.until(numberIsAtLeast(results, index + 1))

PageFactory.initElements(driver, this);
 .get(index)

}
.findElement(By.cssSelector(".r>a"))


.click();

}

}
for “Sr. Automation version”
public class Google {

public Google open() {

Selenide.open("/ncr");

return this;

}


public void search(String text) {

$(By.name("q")).setValue(text).pressEnter();

}


public ElementsCollection results(){

return $$(".srg>.g");

}


public void followResultLink(int index) {

this.results().get(index).find(".r>a").click();

}

}
for “Sr. Automation version”
public class Google {

public Google open() {

Selenide.open("/ncr");

return this;

}


public void search(String text) {

$(By.name("q")).setValue(text).pressEnter();

}


public ElementsCollection results(){

return $$(".srg>.g");

}


public void followResultLink(int index) {

this.results().get(index).find(".r>a").click();

}

}
clean abstraction over locators and actions
For “Sr. Automation Engineers”?
@Test

public void search() {

Google google = new Google();

google.open().search("Selenide");

google.results().shouldHave(size(10));

google.results().get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test

public void followFirstLink() {

Google google = new Google();

google.open().search("Selenide");

google.followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Jr. Automation Engineers”?
@Test

public void search() {

new Google().open().search("Selenide");

new SearchResults()

.shouldHaveSize(10)

.shouldHaveResultText(0, "Selenide: concise UI tests in Java");

}


@Test

public void followFirstLink() {

new Google().open().search("Selenide");

new SearchResults().followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Jr. Automation Engineers”?
@Test

public void search() {

new Google().open().search("Selenide");

new SearchResults()

.shouldHaveSize(10)

.shouldHaveResultText(0, "Selenide: concise UI tests in Java");

}


smaller page objects per context
@Test

public void followFirstLink() {

new Google().open().search("Selenide");

new SearchResults().followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Jr. Automation Engineers”?
@Test

public void search() {

new Google().open().search("Selenide");

new SearchResults()

.shouldHaveSize(10)

.shouldHaveResultText(0, "Selenide: concise UI tests in Java");

}


 built in asserts
@Test

public void followFirstLink() {

new Google().open().search("Selenide");

new SearchResults().followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
For “Manual Testers”?
@Test

public void search() {

google.home.open().search("Selenide");

google.results

.shouldHaveSize(10)

.result(0).shouldContain("Selenide: concise UI tests in Java");

}


@Test

public void followFirstLink() {

google.home.open().search("Selenide");

google.results.result(0).followLink();

assertThat(titleIs("Selenide: concise UI tests in Java"));

}
For “Manual Testers”?
@Test

public void search() {

google.home.open().search("Selenide");

google.results

.shouldHaveSize(10)

.result(0).shouldContain("Selenide: concise UI tests in Java");

}


 one entry point to Test Model
@Test

public void followFirstLink() {

google.home.open().search("Selenide");

google.results.result(0).followLink();

assertThat(titleIs("Selenide: concise UI tests in Java"));

}
For “Manual Testers”?
@Test

public void search() {

google.home.open().search("Selenide");

google.results

.shouldHaveSize(10)

.result(0).shouldContain("Selenide: concise UI tests in Java");

}


@Test
 simpler one parameter methods
public void followFirstLink() {

google.home.open().search("Selenide");

google.results.result(0).followLink();

assertThat(titleIs("Selenide: concise UI tests in Java"));

}
For “Manual Testers”?
@Test

public void search() {

google.home.open().search("Selenide");

google.results

.shouldHaveSize(10)

.result(0).shouldContain("Selenide: concise UI tests in Java");

}


 chainable methods of Fluent PageObjects (.result(0) returns Result object)
@Test

public void followFirstLink() {

google.home.open().search("Selenide");

google.results.result(0).followLink();

assertThat(titleIs("Selenide: concise UI tests in Java"));

}
Why should we care?

To be efficient!
Ok, Where is KISS here?
It is exactly about leveraging
convenient techniques
in concrete context
to make it simpler and efficient!
Reviewing some techniques of
PageObjects design
from KISS point of view
Assertions-free PageObjects?
I favor having no assertions in page objects. I think you can avoid
duplication by providing assertion libraries for common assertions
- which can also make it easier to provide good diagnostics.

Page objects are commonly used for testing, but should not make
assertions themselves. Their responsibility is to provide access to
the state of the underlying page. It's up to test clients to carry out
the assertion logic.

–Martin Fowler

https://martinfowler.com/bliki/PageObject.html
Recall “For Sr. Automation Engineers” version…
@Test

public void search() {

Google google = new Google();

google.open().search("Selenide");

google.results().shouldHave(size(10));

google.results().get(0).shouldHave(text("Selenide: concise UI tests in Java"));

}


@Test
 assertions from “assertion library” provided by Selenide
public void followFirstLink() {

Google google = new Google();

google.open().search("Selenide");

google.followResultLink(0);

Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));

}
Assertion-free PageObject
public class Google {

public Google open() {

Selenide.open("/ncr");

return this;

}


public void search(String text) {

$(By.name("q")).setValue(text).pressEnter();

}


public ElementsCollection results(){

return $$(".srg>.g");

}


public void followResultLink(int index) {

this.results().get(index).find(".r>a").click();

}

}
But… what if…
• The end user is not “Sr. Automation” ?

• Finally he may be even a Manual Tester…

• The UI is so complex that assertions become also more


composite?

• Crazy Managers demand higher level of abstraction in reports

• (they are “crazy”, so you can’t teach them what is good or bad:))
Assertions-free PageObjects?
• break some forms of encapsulation

• reveal too much of technical details (in context of end user level of
understanding) from PageObject to outside

• over-engineered

• may lead to create separate StepsObject

• or additional layers in implementation (like BDD layer)

• loose high cohesion


StepsObject?

GIVEN PageObject as an HTML Page API


THEN StepsObject stands for User Behaviour API
StepsObjects?
@Test

public void search() {

new GoogleUser().opensGoogle()

.searches("Selenide")

.expectsNumberOfResults(10)

.expectsResultWithText(0, "Selenide: concise UI tests in Java");

}


@Test

public void followFirstLink() {

new GoogleUser().opensGoogle()

.searches("Selenide")

.followsResultLink(0)

.expectsTitle("Selenide: concise UI tests in Java");

}
StepsObjects?
public class GoogleUser {


private final Google google = new Google();


public GoogleUser opensGoogle() {

open("/ncr");

return this;

}


public UserOnResultsPage searches(String text) {

this.google.search().setValue(text).pressEnter();

return new UserOnResultsPage();

}

}
StepsObjects?
public class UserOnResultsPage extends GoogleUser {


private final Google google = new Google();


public UserOnResultsPage expectsNumberOfResults(int number) {

this.google.results().shouldHave(size(number));

return this;

}


public UserOnResultsPage expectsResultWithText(int index, String text) {

this.google.results().get(index).shouldHave(text(text));

return this;

}


public UserOnNewPage followsResultLink(int index) {

this.google.resultLink(index).click();

return new UserOnNewPage();

}

}
StepsObjects?

public class UserOnNewPage {




public UserOnNewPage expectsTitle(String text) {

Selenide.Wait().until(titleIs(text));

return this;

}

}
HTML-only PageObject?
public class Google {


public SelenideElement search() {

return $(By.name("q"));

}


public ElementsCollection results(){

return $$(".srg>.g");

}


public SelenideElement resultLink(int index) {

return this.results().get(index).find(".r>a");

}

}
now the code that describes the same page components are spread over several
classes
StepsObjects?

• over-engineered => at least two classes instead of one

• lower cohesion

• related things are not kept in one place


Assertions-included PageObjects (HTML + Steps)!
• full encapsulation

• Tell Don’t Ask

• high cohesion

• End User oriented. He needs “behavioural object”, He actually does not care
about:

• Their responsibility is to provide access


Bloated with code?
to the state of the underlying page
• KISS answer: break down(c) Martin
into Fowler
smaller PageObjects

• you will have still two or even more classes but for exactly one thing from
the User point of view
Assertions-included PageObjects!
• full encapsulation

• Tell Don’t Ask

• high cohesion

• KISS: one class instead of two ones for the same thing from the User point of
view

• Bloated with code?

• KISS answer: break down into smaller PageObjects

• you will have still two or even more classes but for exactly one thing from
the User point of view
Assertions-included PageObjects!
• full encapsulation

• Tell Don’t Ask

• high cohesion

• KISS: one class instead of two ones for the same thing from the User point of
view

• Bloated with code?

KISS
• answer: break down into smaller PageObjects
Advocates of assertion-free page objects say that including assertions mixes the responsibilities
of providing access to page data with assertion logic, and leads to a bloated page object.
• you will have still two or even more classes but for exactly one thing from
(c) Martin Fowler
the User point of view
(For “Jr. Automation Engineers” versions)
public class SearchResults {

private ElementsCollection elements(){

return $$(".srg>.g");

}


public void followResultLink(int index) {

this.elements().get(index).find(".r>a").click();

}


public SearchResults shouldHaveSize(int number) {

this.elements().shouldHave(size(number));

return this;

}


public SearchResults shouldHaveResultText(int index, String text) {

this.elements().get(index).shouldHave(text(text));

return this;

}

}
Assertions-included PageObjects!
• full encapsulation

• Tell Don’t Ask

• high cohesion

• KISS: one class instead of two ones for the same thing from the User point of
view

• Bloated with code?

• KISS answer: break down into smaller PageObjects aka Widgets

• you will have still two or even more classes but for exactly one thing from
the User point of view
• full encapsulation

• Tell Don’t Ask

Despite the cohesion


• high term "page" object, these objects shouldn't usually be built for each page,
but rather for the significant elements on a page
• KISS: one class instead of two ones for the
(c) Martin same thing from the User point of
Fowler
view

• Bloated with code?

• KISS answer: break down into smaller PageObjects aka Widgets

• you will have still two or even more classes but for exactly one thing from
the User point of view
(For “Manual Testers” version)
public class SearchResults {


private ElementsCollection elements(){

return $(".srg>.g");

}


public Result result(int index) {

return new Result(this.elements().get(index));

}


public SearchResults shouldHaveSize(int number) {

this.elements().shouldHave(size(number));

return this;

}

}
(For “Manual Testers” version)
public class Result{

private final SelenideElement container;


public Result(SelenideElement container) {

this.container = container;

}


public void followLink() {

this.container.find(".r>a").click();

}


public Result shouldContain(String text) {

this.container.shouldHave(text(text));

return this;

}

}
(For “Manual Testers” version)
@Test

public void search() {

google.home.open().search("Selenide");

google.results

.shouldHaveSize(10)

.result(0).shouldContain("Selenide: concise UI tests in Java");

}


@Test

public void followFirstLink() {

google.home.open().search("Selenide");

google.results.result(0).followLink();

assertThat(titleIs("Selenide: concise UI tests in Java"));

}
KISS Pageobjects for Jr. and Manual Testers, where
extra reporting is needed
=
2 in 1:

HTML PageObjects
+
StepsObjects with Asserts

(being broken down to “Widgets” if needed)


KISS
*=
simplicity instead of over-engineering
=
YAGNI = You Ain’t Gonna Need It
public class Google {

private SelenideElement searchInput = $(By.name("q"));


public Google open() {

Selenide.open("/ncr");

return this;

}

you ain’t gonna need it ;)

public void search(String text) {

this.searchInput.setValue(text).pressEnter();

}

}

public class Google {




public Google open() {

vs YAGNI version Selenide.open("/ncr");

return this;

}


public void search(String text) {

$(By.name("q")).setValue(text).pressEnter();

}

}
public class Google {

public ElementsCollection results(){

return $$(".srg>.g");

}

you ain’t gonna need this form of encapsulation ;)

vs YAGNI version

public class Google {



public ElementsCollection results = $$(".srg>.g");


the only probable thing with a real risk of change is the locator
public class Google {

public final ElementsCollection results = $$(".srg>.g");


you ain’t gonna need this form of protection ;)

vs YAGNI version

public class Google {



public ElementsCollection results = $$(".srg>.g");

public class Google {

public final ElementsCollection results = $$(".srg>.g");


though this teaches you kind of the “true safe” programming


and this can be kind of “good habit” to be trained ;)
Driver management?
Driver management?

• Why to bother with this bulky management if YAGNI?

• the only relevant case is when you have to work with two opened
browsers in one test. But how many such tests did you wrote? ;)

• remember that for “parallel testing” case you have


ThreadLocal<WebDriver> ;)
But sometimes that’s the case…

• Why to bother with this bulky management if YAGNI?

• the only relevant case is when you have to work with two opened
browsers in one test. But how many such tests did you wrote? ;)

• remember that for “parallel testing” case you have


ThreadLocal<WebDriver> ;)
Winning a bit of speed…
@Test

public void shareMessageToFollowers() {

SelenideDriver selenideBrowser = new SelenideDriver(new FirefoxDriver());

SelenideDriver yashakaBrowser = new SelenideDriver(new FirefoxDriver());


new Diaspora(selenideBrowser).open().signIn(

SecretData.Selenide.username, SecretData.Selenide.password);

/* Yashaka follows Selenide ;) */

new Diaspora(yashakaBrowser).open().signIn(

SecretData.Yashaka.username, SecretData.Yashaka.password);


new NewPost(selenideBrowser).start().write("Selenide 4.2 released!").share();

new Stream(yashakaBrowser).post(0).shouldBe("Selenide 4.2 released!");

}

by missing the “logout” step for Selenide user


and verifying ER in already opened 2nd browser ;)
By making PageObjects more “bulky” :|
public class Diaspora {

private final SelenideDriver driver;


public Diaspora(SelenideDriver driver) {

this.driver = driver;

}


public Diaspora open() {

Selenide.open("/");

return this;

}


public void signIn(String username, String password) {

new NavBar(this.driver).select("Sign in");

new Form(this.driver.element("#new_user"))

.set("USERNAME", username)

.set("PASSWORD", password)

.submit();

}

}
Instead of much cleaner
public class Diaspora {


public Diaspora open() {

Selenide.open("/");

return this;

}


public void signIn(String username, String password) {

new NavBar().select("Sign in");

new Form($("#new_user"))

.set("USERNAME", username)

.set("PASSWORD", password)

.submit();

}

}
That can be used to achieve the same goal by
using corresponding API helpers:

@Test

public void shareMessageToFollowers() {

new Diaspora().open().signIn(
SecretData.Selenide.username, SecretData.Selenide.password);

new NewPost().start().write("Selenide 4.2 released!").share();

new API().ensureLoggedIn(
SecretData.Yashaka.username, SecretData.Yashaka.password);

new Stream().post(0).shouldBe("Selenide 4.2 released!");

}
Still enough open points to take into account…

• Will API calls simulate the same real behaviour?

• Like Logged in user does nothing but his stream is updated…


Still enough open points to take into account…
• Like Logged in user does nothing but his stream is updated…

• Do we actually need the simulation to be “like real”?

• We still can use API calls to check that “new post” from an author
gets the storage

• And in separate test “the follower can do nothing” and after


populating the storage by API call with a “new author’s post” -
we will verify (via Selenium) that follower sees the post
Smarter simulation with API calls
@Test

public void authorSharesMessage() {

new ApiCall().ensureSignedIn(

SecretData.Selenide.username, SecretData.Selenide.password);


new NewPost().start().write("Selenide 4.2 released!").share();

new Stream().post(0).shouldBe("Selenide 4.2 released!");

new ApiCall().assertMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

}


@Test

public void followerSeesNewMessageInTheStream() {

new Diaspora().ensureSignedIn(

SecretData.Yashaka.username, SecretData.Yashaka.password);


new ApiCall().createMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

new Stream().post(0).shouldBe("Selenide 4.2 released!");

}
Still enough open points to take into account…
• Like Logged in user does nothing but his stream is updated…

• Do we actually need the simulation to be “like real”?

• We still can use API calls to check that “new post” from an author
gets the storage

• And in separate test “the follower can do nothing” and after


populating the storage by API call with a “new author’s post” -
we will verify (via Selenium) that follower sees the post
Smarter simulation with API calls
@Test

public void authorSharesMessage() {

new ApiCall().ensureSignedIn(

SecretData.Selenide.username, SecretData.Selenide.password);


new NewPost().start().write("Selenide 4.2 released!").share();

new Stream().post(0).shouldBe("Selenide 4.2 released!");

new ApiCall().assertMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

}


@Test

public void followerSeesNewMessageInTheStream() {

new Diaspora().ensureSignedIn(

SecretData.Yashaka.username, SecretData.Yashaka.password);


new ApiCall().createMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

new Stream().post(0).shouldBe("Selenide 4.2 released!");

}
Atomic, faster, and so more efficient than “real simulation”
@Test

public void authorSharesMessage() {

new ApiCall().ensureSignedIn(

SecretData.Selenide.username, SecretData.Selenide.password);


new NewPost().start().write("Selenide 4.2 released!").share();

new Stream().post(0).shouldBe("Selenide 4.2 released!");

new ApiCall().assertMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

}


@Test

public void followerSeesNewMessageInTheStream() {

new Diaspora().ensureSignedIn(

SecretData.Yashaka.username, SecretData.Yashaka.password);


new ApiCall().createMessageInStorage(
SecretData.Selenide.username, "Selenide 4.2 released!");

new Stream().post(0).shouldBe("Selenide 4.2 released!");

}
Still enough open points to take into account…

• But sometimes… Life is complicated:) And for some mystic reason


we “can’t” use API calls…
Only then we need “explicit driver management”
SelenideDriver selenideBrowser = new SelenideDriver(new FirefoxDriver());

SelenideDriver yashakaBrowser = new SelenideDriver(new FirefoxDriver());

...

@Test

public void shareMessageToFollowers() {

new Diaspora(selenideBrowser).open().signIn(

SecretData.Selenide.username, SecretData.Selenide.password);

/* Yashaka follows Selenide ;) */

new Diaspora(yashakaBrowser).open().signIn(

SecretData.Yashaka.username, SecretData.Yashaka.password);


new NewPost(selenideBrowser).start().write("Selenide 4.2 released!").share();

new Stream(yashakaBrowser).refresh().post(0).shouldBe(“Selenide 4.2 released!");

}
Only then we need “explicit driver management”
public class Diaspora {

private final SelenideDriver driver;


public Diaspora(SelenideDriver driver) {

this.driver = driver;

}


public Diaspora open() {

Selenide.open("/");

return this;

}


public void signIn(String username, String password) {

new NavBar(this.driver).select("Sign in");

new Form(this.driver.element("#new_user"))

.set("USERNAME", username)

.set("PASSWORD", password)

.submit();

}

}
KISS
=
not necessarily Easiness
Consider…

public class Widget {



private final SelenideElement container;


public Widget(SelenideElement container) {

this.container = container;

}


public SelenideElement self() {

return this.container;

}

}
Easy

public class Post extends Widget{



public Post(SelenideElement container) {

super(container);

}


public void shouldBe(String withText) {

self().shouldHave(text(withText));

}

}
Easy

public class Post extends Widget{



public Post(SelenideElement container) {

super(container);

}


public void shouldBe(String withText) {

self().shouldHave(text(withText));

}

}

too much of “implicit magic”…


Simple

public class Post {



private final SelenideElement container;


public Post(SelenideElement container) {

this.container = container;

}


public void shouldBe(String withText) {

this.container.shouldHave(text(withText));

}

}

“Explicit is better than implicit” (c) The Zen of Python, PEP-20


Simple is not Easy
“Simple Made Easy”

–Rich Hickey
assert KISS != magic
assert KISS == explicit
Magic
public void writeAndShare(String text) {

this.start().write(text).share();

new Stream().post(0).shouldBe(text);

}

@Test

public void shareMessage() {

new Diaspora().open().signIn(

SecretData.Selenide.username, SecretData.Selenide.password);

new NewPost().shareNewPost("Selenide 4.2 released!”)

}

hidden “start new post” test-logic, and hidden assertion


KISS

@Test

public void shareMessage() {

new Diaspora().open().signIn(

SecretData.Selenide.username, SecretData.Selenide.password);

new NewPost().start().write("Selenide 4.2 released!").share();

new Stream().post(0).shouldBe("Selenide 4.2 released!");

}

explicit test-logic-step
explicit assert-step
WaitingLoadable?
Recall “Super-duper” WaitingLoadableComponents

all “super” waiting for all results starts here


@Test

public void search() {

GooglePage google = new GooglePage(driver);

GoogleSearchResultsPage resultsPage = google.get().search("Selenide");

assertThat(resultsPage.getResults().size(), equalTo(10));

assertThat(resultsPage.getResults().get(0).getText(),

containsString("Selenide: concise UI tests in Java"));

}

now here we can use common assert without waiting


But what if do not want to wait all results? We are
interested only in the first one!

all “super” waiting for all 10 results starts here


@Test

public void search() {

GooglePage google = new GooglePage(driver);

GoogleSearchResultsPage resultsPage = google.get().search("Selenide");

assertThat(resultsPage.getResults().size(), equalTo(10));

assertThat(resultsPage.getResults().get(0).getText(),

containsString("Selenide: concise UI tests in Java"));

}

now here we can use common assert without waiting


WaitingLoadable may break the real user behaviour
in some contexts…

all “super” waiting for all 10 results starts here


@Test

public void search() {

GooglePage google = new GooglePage(driver);

GoogleSearchResultsPage resultsPage = google.get().search("Selenide");

assertThat(resultsPage.getResults().size(), equalTo(10));

assertThat(resultsPage.getResults().get(0).getText(),

containsString("Selenide: concise UI tests in Java"));

}

now here we can use common assert without waiting


KISS Answer

• User oriented automation based on waiting loadable elements over


loadable pages waiting their components
How often do you open Facebook, and proceed to profile or messages,
while the stream is not loaded yet? ;)

Diaspora Demo
So KISS leads to
• User Oriented waiting

• No explicit waits to finalise step


• => Explicit Test Logic (No test-logic asserts in steps)

• No explicit waits at all! (like wait for all page is loaded)

• Implicit tech details


• like waiting
PageObjects Summary
• Readable and user oriented • Explicit Test Logic
• not HTML oriented • No test-logic asserts in steps
• no hidden steps logic
• Assert-steps included
• User Oriented waiting
• if “newcomers-oriented”
• No explicit waits to finalise step
• Top Down design • No explicit waits at all! (like wait for all
• YAGNI page is loaded)
• no driver management • Implicit tech details
• like waiting
• no over-optimisation
• Fluent where adds conciseness and
• no redundant fields readability
• no extra variables
PageObjects Secret ;)

NO Selenium Webdriver,
use more concise wrappers!
PageObjects Secret ;)

Prefer
test automation tools
over
browser automation tools
Afterwords
There are good practices in context,
but there are no best practices.
(c) Cem Kaner, James Bach
Q&A
Thank you!
yashaka @
github.com/automician
automician.com
seleniumcourses.com

@yashaka 01.2017

You might also like