This talk was given at "The KISS Method and PageObjects" meetup (https://goo.gl/2DBuxj).
The talk is about KISS (Keep It Simple Stupid) principle applied to PageObjects design with a comparison to other more classic approaches.
The slides contain both Selenium Webdriver and Selenide examples.
src code for the talk: https://github.com/yashaka/talks/tree/master/kiss-pageobjects
9. –Martin Fowler
Page objects are a classic example of encapsulation - they hide
the details of the UI structure and widgetry from other
components (the tests)
12. 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"));
}
13. 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);
}
…
14. 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);
}
…
15. 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();
}
}
17. Classic
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
18. Classic
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
19. 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 ???
LoadableComponent
20. “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;
}
}
21. “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();
}
…
22. “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();
}
public GoogleSearchResultsPage search(String text) {
this.search.query(text);
return new GoogleSearchResultsPage(this.driver).get();
}
}
“super” waiting for loading trick
23. “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;
}
}
24. “Super-duper” WaitingLoadableComponents
@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"));
}
all “super” waiting starts here
now here we can use common assert without waiting
29. 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");
}
30. PageObjects Implementation
public class Google {
public Google open() {
Selenide.open("/ncr");
return this;
}
public void search(String text) {
$(By.name("q")).setValue(text).pressEnter();
}
}
31. 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;
}
}
34. So why do we need
PageObjects?
Despite of DRYing the code…
35. So why do we need PageObjects?
Despite of DRYing the code…
36. –Martin Fowler
Page objects are a classic example of encapsulation - they hide
the details of the UI structure and widgetry from other
components (the tests)
37. Why may we need Encapsulation?
• Encapsulate for cohesion?
• that’s ok… pretty reasonable to keep related things together
(locators and actions on their elements)
38. 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? ;)
39. 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…
40. Why may we need Encapsulation?
• Encapsulate to narrow User Oriented behavioural API to be
readable enough and convenient in usage?
• :)
41. A point to think about…
“Encapsulate to narrow User Oriented behavioural API to be readable
enough and convenient in usage”
• developers?
• Sr. Automation engineers?
• Jr. Automation engineers?
• Manual Testers?
Readable and convenient for whom?
42. @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”?
43. @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”?
API is already readable and simple!
44. @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”?
developers know locators that are already readable for them too:)
45. @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"));
}
For “developers”?
actually they do make them readable when they write tests ;)
46. @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”?
copy&paste rules! ;)
47. @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”?
With Find&Replace over DRY ;)
48. 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"));
}
49. 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"));
}
hiding awful locators for “beauty” :D
50. 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!
51. 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"));
}
52. Remember classic version?
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);
}
…
…
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();
}
}
53. Remember classic version?
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);
}
…
…
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();
}
}
locators and actions are bloated with tech details
of browser management and waits
54. 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();
}
}
55. 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
56. 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"));
}
57. 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"));
}
58. 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"));
}
smaller page objects per context
59. 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"));
}
built in asserts
60. 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"));
}
61. 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"));
}
one entry point to Test Model
62. 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"));
}
simpler one parameter methods
63. 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"));
}
chainable methods of Fluent PageObjects (.result(0) returns Result object)
67. –Martin Fowler
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.
https://martinfowler.com/bliki/PageObject.html
Assertions-free PageObjects?
68. 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
public void followFirstLink() {
Google google = new Google();
google.open().search("Selenide");
google.followResultLink(0);
Selenide.Wait().until(titleIs("Selenide: concise UI tests in Java"));
}
assertions from “assertion library” provided by Selenide
69. 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();
}
}
70. 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:))
71. 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
73. 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");
}
74. 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();
}
}
75. 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();
}
}
77. 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
79. 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:
• 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
Their responsibility is to provide access
to the state of the underlying page
(c) Martin Fowler
80. 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
81. 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
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.
(c) Martin Fowler
82. (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;
}
}
83. 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
84. • 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
Despite the term "page" object, these objects shouldn't usually be built for each page,
but rather for the significant elements on a page
(c) Martin Fowler
85. (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;
}
}
86. (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;
}
}
87. (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"));
}
88. 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)
90. public class Google {
private SelenideElement searchInput = $(By.name("q"));
public Google open() {
Selenide.open("/ncr");
return this;
}
public void search(String text) {
this.searchInput.setValue(text).pressEnter();
}
}
public class Google {
public Google open() {
Selenide.open("/ncr");
return this;
}
public void search(String text) {
$(By.name("q")).setValue(text).pressEnter();
}
}
vs YAGNI version
you ain’t gonna need it ;)
91. public class Google {
public ElementsCollection results(){
return $$(".srg>.g");
}
public class Google {
public ElementsCollection results = $$(".srg>.g");
vs YAGNI version
you ain’t gonna need this form of encapsulation ;)
the only probable thing with a real risk of change is the locator
92. public class Google {
public final ElementsCollection results = $$(".srg>.g");
public class Google {
public ElementsCollection results = $$(".srg>.g");
vs YAGNI version
you ain’t gonna need this form of protection ;)
93. 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 ;)
95. 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> ;)
96. 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> ;)
97. 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 ;)
98. 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();
}
}
99. 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();
}
}
100. 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!");
}
101. 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…
102. 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
103. 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!");
}
104. 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
105. 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!");
}
106. 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!");
}
107. Still enough open points to take into account…
• But sometimes… Life is complicated:) And for some mystic reason
we “can’t” use API calls…
108. 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!");
}
109. 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();
}
}
Only then we need “explicit driver management”
111. Consider…
public class Widget {
private final SelenideElement container;
public Widget(SelenideElement container) {
this.container = container;
}
public SelenideElement self() {
return this.container;
}
}
112. Easy
public class Post extends Widget{
public Post(SelenideElement container) {
super(container);
}
public void shouldBe(String withText) {
self().shouldHave(text(withText));
}
}
113. 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”…
114. 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
118. @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
Magic
public void writeAndShare(String text) {
this.start().write(text).share();
new Stream().post(0).shouldBe(text);
}
119. @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
KISS
121. Recall “Super-duper” WaitingLoadableComponents
@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"));
}
all “super” waiting for all results starts here
now here we can use common assert without waiting
122. But what if do not want to wait all results? We are
interested only in the first one!
@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"));
}
all “super” waiting for all 10 results starts here
now here we can use common assert without waiting
123. WaitingLoadable may break the real user behaviour
in some contexts…
@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"));
}
all “super” waiting for all 10 results starts here
now here we can use common assert without waiting
124. KISS Answer
• User oriented automation based on waiting loadable elements over
loadable pages waiting their components
125. Diaspora Demo
How often do you open Facebook, and proceed to profile or messages,
while the stream is not loaded yet? ;)
126. 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
127. PageObjects Summary
• Readable and user oriented
• not HTML oriented
• Assert-steps included
• if “newcomers-oriented”
• Top Down design
• YAGNI
• no driver management
• no over-optimisation
• no redundant fields
• Explicit Test Logic
• No test-logic asserts in steps
• no hidden steps logic
• User Oriented waiting
• No explicit waits to finalise step
• No explicit waits at all! (like wait for all
page is loaded)
• Implicit tech details
• like waiting
• Fluent where adds conciseness and
readability
• no extra variables