A story of one organisation taking an idea, formed by Antony Marcano and evolved by many, to solve some of the challenges that encountered with the growth of Selenium/WebDriver PageObjects in their Automated Tests. Talk co-presented with two of the people who have been a part of the journey – including Kostas Mamalis & Jan Molak.
4. IN THE BEGINNING
•
Long established financial institution
•
Used Scrum with 2 week Sprints
•
Outsourced development to large consultancy
•
No automated acceptance tests (few unit tests)
5. ALONG THE JOURNEY
•
Realised that automated tests were essential
•
Wrote lots of Cucumber tests
•
Backed by Selenium/WebDriver
•
Used the PageObject Pattern
9. WEBDRIVER EXAMPLE
DesiredCapabilities capabilities = new DesiredCapabilities();
WebDriver driver = new PhantomJSDriver(desiredCapabilities());
driver.get(baseUrl+"owners/find.html");
driver.findElement(By.cssSelector("#search-owner-form button")).click();
assertThat(
driver.findElements(By.cssSelector("owners tbody tr")).size(),
is(10)
);
10. FINDING ALL OWNERS - WEBDRIVER EXAMPLE
DesiredCapabilities capabilities = new DesiredCapabilities();
WebDriver driver = new PhantomJSDriver(desiredCapabilities());
driver.get(baseUrl+"owners/find.html");
driver.findElement(By.cssSelector("#search-owner-form button")).click();
assertThat(
driver.findElements(By.cssSelector("owners tbody tr")).size(),
is(10)
);
11. FINDING ALL OWNERS - PAGEOBJECT EXAMPLE
DesiredCapabilities capabilities = new DesiredCapabilities();
WebDriver driver = new PhantomJSDriver(desiredCapabilities());
driver.get(baseUrl+"owners/find.html");
FindOwnersPage findOwners =
PageFactory.initElements(driver, FindOwnersPage.class);
OwnersPage owners = findOwners.findWith(EMPTY_SEARCH_TERMS);
assertThat(owners.numberOfOwners(), is(10));
12. PROBLEMS AROSE
•
Large PageObject classes
•
Brittle test-code (less than raw Selenium)
•
Duplication across PageObjects for each of the
‘portals’
13. THEY TRIED THE FOLLOWING
•
Separate behaviour into Navigation Classes
•
Reduce duplication with inheritance
Causing ...
•
Large Navigation classes
•
Deep inheritance hierarchy
14. EFFECTS ON THE TEAM
•
Took longer and longer to add new tests
•
Got harder to diagnose problems
•
Low trust in the ‘test framework’ and Cucumber
•
Reduced faith
in automated testing
•
Impacted morale
17. THE INSIGHT
Roles
← Who
➥ Goals
← Why
➥ Tasks
← What
➥ Actions ← How
Inspired by Kevin Lawrence’s talk at the first AAFTT in 2007
More of his thinking here: http://www.developertesting.com/archives/month200710/20071013-In%20Praise%20of
%20Abstraction.html
18. 2008 - JNARRATE
@Test
public void should_be_able_to_edit_a_page() {
Given.thatThe(wiki).wasAbleTo(beAtThe(PointWhereItHasBeen.JUST_INSTALLED));
And.thatThe(user).wasAbleTo(navigateToTheHomePage());
And.thatThe(user).wasAbleTo(navigateToTheHomePage());
When.the(user).attemptsTo(
tas
k
changeTheContent().to("Welcome to Acceptance Test Driven Development")
tas
k
);
Then.the(textOnTheScreen().ofThe(user)).
tas
k
shouldBe("Welcome to Acceptance Test Driven Development");
}
Playing with fluent APIs and started to explore the model of Tasks & Actions
(although back then the labels I used more like Kevin’s labels).
20. 2012 - THE JOURNEY PATTERN
Actor
performs
Tasks
has
Abilities
composed of
enable
Actions
interact with
Screen
contains
Elements
21. JOURNEY PATTERN APPLIED
JUNIT
Roles
➥ Goals
➥ Tasks
← Who
← Why
← What
➥ Actions ← How
Actor theReceptionist =
new Actor().with(WebBrowsing.ability());
@Test public void
should_find_all_owners_by_default
theReceptionist.attemptsTo(
Go.to(findOwnersScreen.url()),
Search.forOwnersWith(EMPTY_SEARCH_TERMS),
Count.theNumberOfOwners()
);
Enter.the(searchTerms).
into(findOwnersScreen.searchTerms),
Click.onThe(findOwnersScreen.searchButton)
22. JOURNEY PATTERN APPLIED
CUCUMBER
Roles
← Who
As a Pet Clinic Receptionist
➥ Goals
← Why
Scenario: Find all owners by default
← What
When I search for owners with BLANK search terms
@When
(“^I search for owners with BLANK search terms$”)
➥ Tasks
theReceptionist.attemptsTo(
Search.forOwnersWith(EMPTY_SEARCH_TERMS)
);
➥ Actions ← How
Enter.the(searchTerms).
into(findOwnersScreen.searchTerms),
23. PUTTING IT ALL TOGETHER
Actor theReceptionist = new Actor().with(WebBrowsing.ability());
theReceptionist.attemptsTo(
Go.to(findOwnersScreen.url()),
Search.forOwnersWith(EMPTY_SEARCH_TERMS),
Count.theNumberOfOwners()
);
assertThat(
theReceptionist.sawThatThe(numberOfOwners()),
was(theExpectedNumberOfOwners)
);
24. A TASK
…
private static String searchTerms;
@Override
public void performAs(Actor asAReceptionist) {
asAReceptionist.attemptTo(
Enter.the(searchTerms).into(findOwnersScreen.searchTerms),
Click.onThe(findOwnersScreen.searchButton)
);
}
public SearchForOwnersWith(String searchTerms) {
this.searchTerms = searchTerms;
}
…
25. A SCREEN
@Url("owners/find.html")
public class FindOwnersScreen extends WebScreen {
@LocateBy(css="#search-owner-form input")
public ScreenElement searchTerms;
@LocateBy(css="#search-owner-form button")
public ScreenElement searchButton;
}
26. AN ACTION
public class Enter extends WebDriverInteraction implements Perform {
private String text;
private ScreenElement field;
public void performAs(Actor actor) {
web(actor).findElement(field.locator()).sendKeys(text);
}
public Enter(String text) { this.text = text; }
public static Enter the(String text) {return new Enter(text);}
public Perform into(ScreenElement field) {
this.field = field;
return this;
}
}
27. PROBLEMS SOLVED
•
Smaller “Screen” classes
•
Small, focused “Task” classes
•
Readable code
•
Consistent metaphor
•
Minimal inheritance
•
Removed need for duplication across behaviours previously in
PageObjects or “Navigation” classes
28. DESIGN PRINCIPLES
•
DRY - navigational steps in one place
•
Separation of Concerns - Page Structure and Actions separate
•
Small Classes - easy to comprehend
•
Single Responsibility - classes focused on one thing
and one thing only
•
Minimise conditional logic - navigational if-thens replaced
with composable sequences
New requirement for automated testing -> tests focused on UI -> inverted test pyramid
1. Let’s refresh our memory what the Page Object pattern is all about
2. AUDIENCE: Can we have a show of hands; how many of you work with Selenium? Webdriver? Selenium IDE?
3. OK, so let's see how functional testing of the UI can be addressed using an example of a serious, financial application ...
Because of confidentiality reasons we can't present our client's applications, but I believe that even a much simpler project will be enough to demonstrate the limitations of two common approaches to UI testing.
I'll leave re-scaling the problem to the size of your typical projects to your imagination.
Let's talk about testing the PetClinic, a sample Spring application that demonstrates the basic of using the Spring framework.
We have some people with WebDriver experience here. So guys, what is this test trying to prove?
So the scenario we have here: a PetClinic Receptionist wants to use the default behaviour of the Search form, which allows them to see all the data stored in the database if no search terms are specified.
Now, how clear is this scenario based on this code?It’s difficult to tell what the test is trying to prove with all the css selector noise
PageObjects introduce a basic abstraction that encapsulates page elements and actions that can be performed on a given page in a PageObject;
Actions return other PageObjects -> maintenance overhead should the flow change
They might encourage code duplication -> what if you can log in on different pages?