footer: @EvilTester | Copyright © 2019, Compendium Developments Ltd slidenumbers: true Autoscale: true
- Alan Richardson
- @eviltester
After gaining some experience of web automation tools, you start to realise that “yes, you have to learn the API”, but the real challenge is modeling the application and building an abstraction layer which supports different approaches to automation. And when we build an abstraction layer, we have lots of options to choose between.
- Do you use the Page Factory?
- What counts as a page object?
- Should a page object offer logical functions like “loginAs” or does it only model the physical world?
- How do we deal with navigation? Return page objects, or with a different set of objects?
- Do we need abstractions for Dom elements like input fields or is WebElement enough?
- When do we synchronise with WebDriverWait and when do we use SlowLoadableComponent?
- Should we build our own abstractions on top of SlowLoadableComponent?
By using a simple web application, we will use these, and other questions, to discuss and experiment with, the most challenging parts of Web Automation – the modeling and construction of robust and re-usable abstraction layers.
Existing practitioners, be prepared to discuss the choices you have made in the past, what worked, and what didn’t, so that we can learn and possibly build on some of those decisions.
This is open to novices and experienced practitioners, but if you want to take part then make sure you have a functioning Selenium WebDriver installation. All examples will be presented using Java, but that doesn’t stop you writing experiments in Python, .Net, or whatever other language you favour.
Bring your laptop, and you’ll have the opportunity to explore different ways of building Page Objects and abstraction layers.
* Intro
* General Abstractions Overview
* Abstraction Approaches Overview & Discussions
* Modelling
* Example Implementations
* Additional Approaches
* Hands on Exercises
* Debriefs & Wrap Ups
- github.com/eviltester/automationAbstractions
- link to slides
The code uses Maven, Java 1.8 (or above), everything is brought down as a dependency using Maven - you do not need to install WebDriver or Drivers. You do need to have Chrome (recommended), or Firefox, or Safari.
- pom.xml
- dependencies
- web driver manager
- examples
- uk.co.compendiumdev.examples
- simple worked examples with exercises
- casestudybasic
- a basic test with no abstractions to refactor to page objects or other components
- casestudyexample
- an existing test with existing abstractions, fix, refactor, explore
- casestudyrefactor
- a test with a single page object, good basis to refactor from
- Used WebDriver?
- What Languages?
- Abstraction Layers?
- Frameworks?
- Current setup
- Laptop With You? Ready to code?
- Discuss
- Modelling
- Separation of concerns
- Logical vs Physical
- Functional vs Structural
- Interfaces vs Implementations
- Data / Entities / Persistence
- Functionality / Task Flow
- Goals / Strategies
- Layers – GUI, DB, HTTP
- Etc.
NoAbstractionTest.java
shown on next slide
@Test
public void canCreateAToDo(){
WebDriver driver = new ChromeDriver();
String siteURL = "http://todomvc.com/examples/backbone/";
driver.get(siteURL);
int originalNumberOfTodos = driver.findElements(
By.cssSelector("ul.todo-list li")).size();
WebElement createTodo = driver.findElement(
By.className("new-todo"));
createTodo.click();
createTodo.sendKeys("new task");
createTodo.sendKeys(Keys.ENTER);
Assertions.assertTrue(driver.findElement(
By.className("filters")).isDisplayed());
int newToDoCount = driver.findElements(
By.cssSelector("ul.todo-list li")).size();
Assertions.assertTrue(newToDoCount >
originalNumberOfTodos);
driver.close();
driver.quit();
}
- ChromeDriver
- WebDriver
- Variable Names, Method Names
- Library Method calls
- Assertion Libraries
- JUnit Test Runner and Annotations
WithAbstractionTest.java
shown on next slide
@Test
public void canCreateAToDo(){
WebDriver driver = new ExecutionDriver().get();
TodoMVCUser user = new TodoMVCUser(driver,
new TodoMVCSite());
user.
opensApplication().
and().
createNewToDo("new task");
TodoMVCPojoPage page =
new TodoMVCPojoPage(driver,
new TodoMVCSite().getURL());
assertThat(page.getCountOfTodoDoItems(), is(1));
assertThat(page.isFooterVisible(), is(true));
ExecutionDriver.closeDriver(driver);
}
@Before, @After
- abstract setup and tear down- driver creation using ExecutionDriver
- environment url management using TodoMVCSite
- User Workflow abstraction TodoMVCUser
- Application page abstractions ApplicationPageFunctional
- test method and variable naming
- assertions from HamCrest
- Discussion
- Change implementations easily
- Single Responsibility – only changes when necessary
- Makes automation readable and maintainable
- When app changes, fewer changes to test code
- Code Completion when writing code
“...The name of the song is called ‘Haddocks' Eyes.’”
“Oh, that's the name of the song, is it?" Alice said, trying to feel interested.
“No, you don't understand,” the Knight said, looking a little vexed. “That's what the name is called. The name really is ‘The Aged Aged Man.’”
- Lewis Carroll, Through The Looking Glass
“Then I ought to have said ‘That's what the song is called’?” Alice corrected herself.
“No, you oughtn't: that's quite another thing! The song is called ‘Ways And Means’: but that's only what it's called, you know!”
“Well, what is the song, then?” said Alice, who was by this time completely bewildered. “I was coming to that,” the Knight said. “The song really is ‘A-sitting On A Gate’: and the tune's my own invention.””
- Lewis Carroll, Through The Looking Glass
“The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.”
www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340.html
www.cs.utexas.edu/~EWD/ewd03xx/EWD340.PDF
“I must create a system. or be enslav'd by another Mans; I will not reason & compare: my business is to create”
http://www.blakearchive.org/search/?search=%22must%20create%20a%20system%22
http://www.blakearchive.org/copy/jerusalem.a?descId=jerusalem.a.illbk.10
- names are easy to change
- multiple nested abstractions
- precision
- create new layers when required
- don't shove everything in one class
- flexibility, not control
- make it easy to get the thing abstracted
- abstractions for 'common' items
- decision making (and re-making)
- Data
- Physical
- Domain
- Logical
- etc.
- Data
- Generic Data Abstractions e.g. email, postcode
- Physical
- Physical layout of your application e.g. pages, components
- Navigation across pages
- Domain
- Your application Entities domain e.g. user, account
- Logical
- User actions, workflows
- etc.
- Page Objects
- Element Abstractions: select, textbox, etc.
- Domain Objects, Infrastructure
- Gherkin (Given/When/And/Then)
- Domain Specific Languages
- Any More?
- Abstraction != Tool / Framework / Implementation
- Gherkin != Cucumber
- Page Object != Page Factory
- Domain Specific Language != Keyword Driven
If we want to get good at abstraction then we need to model, split apart, and make the relationships clear
- Dom Abstraction
- WebElement
- Browser Abstraction
- WebDriver
- FireFox, Chrome, Remote etc.
- WebDriver
- HTML Abstractions
- Cookies, Frames, etc.
- 'Support' classes augment WebDriver
- e.g. com.seleniumsimplified.selenium.support
- Frameworks
- Control
- Can only do what Framework allows
- Calls your code
What else?
- Libraries
- Freedom
- Can be abstracted
- Used for multiple purposes e.g. functional, performance, exploratory
- You call it
What else?
- JUnit
- WebDriver
- AssertJ, HamCrest
- A Base Test Class that we extend
Discuss. Other Examples? Pros & Cons?
We write @Test methods that use abstractions to do stuff.
- Do we write Unit Tests for our @Test code?
- Do we write Unit Tests for our Abstraction Layers?
- Do we run our @Test with code coverage?
see coverage package
The key to effective abstractions is modelling.
- Single Page App
- Shows Counts
- Data held in HTML5 local storage
- Create/Edit/Complete a 'todo'
- Clear Completed
- Filter active/completed/all
- Delete a todo
- What Abstractions might we build?
- What thoughts do we have?
- What underpins our analysis?
- What might be hard?
- What risks are there?
- that impact our automating and modelling
Our code is a model of the application
@Test
- flows- Abstraction Layers
- models with a point of view
- we can model the same page in different ways
- Method, Class, Variable Naming
- our Ubiquitous Language
- Models reveal coverage
- review Page Objects might reveal gaps in testing
- if it isn't modelled, it isn't covered.
- Infrastructure - downloading dependencies and setting up tools
- Driver Abstractions - don't instantiate drivers directly use an abstraction
- Environment Abstractions - config abstraction, e.g. don 't code URLs into test
- Element Abstractions - alternative to WebElement e.g. CheckBox, Select etc.
- Page Object - model the page as common actions or physical elements
- Fluent Page Objects - allow method chaining for readable tests avoiding BDD DSL tools
- Navigation Abstractions - separate navigation from actions, so pages don't have to know about other pages
- Component Abstractions - common chunks of the page as separate classes
- Domain Abstractions - model the domain and work with data, or ubiquitous language
- Gherkin as DSL Abstraction - using BDD style tools for DSL creation
- Assertion Abstractions - different assertion approaches separate from test execution
- Base Test - framework style implementation of abstraction layers
- Browser Abstractions - access cookies, local storage etc.
- Workarounds - abstractions to help work around bugs, limitations, in tooling
- Maven and other dependency management tools
- WebDriverManager - downloads drivers
e.g. see InfrastructureTest
Downloads dependencies.
e.g. https://github.com/bonigarcia/webdrivermanager
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>3.6.2</version>
<scope>test</scope>
</dependency>
@Test
public void ensureChromeDriverIsAvailable(){
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
final TodoMVCSite todoMVCSite = new TodoMVCSite();
driver.get(todoMVCSite.getURL());
driver.close();
driver.quit();
}
Abstraction is not just in your source code.
- downloading, setup, install
- chocolatey, homebrew
- custom scripts
- configuration
- support multiple approaches (ability to override at different levels)
- global machine, command line, local in IDE, default
- support multiple approaches (ability to override at different levels)
- Pros?
- Cons?
- Pros
- Makes WebDriver installation simpler
- Less to manage in version control
- Cons
- May reduce technical understanding of WebDriver
- Which might impact adoption of cloud execution
- Dependent on version manager updates
A common abstraction for Web GUI automating
WebDriver driver = Driver.get();
Driver
allows us to abstract away the configuration of browsers- To run our tests on multiple browsers without changing the test
We have to be careful not to have this limit us.
see DriverManagerTest
- Cached browsers to re-use browser
- Creating uncached 'unique' browsers
- Static fields for all tests
- Unique browser per @Test to support parallelism
- Configuration via Environment Variables or System Properties
- Configuration via methods
- etc.
This can become complicated e.g.
- Pros?
- Cons?
- Pros
- Easy to run tests on different browsers without changing test code
- Easy to handle changes in how browsers startup or close
- Central place to control extensions etc.
- Cons
- Can become complicated
- People don't learn how to initiate or configure browsers
- When baked into a framework can be harder to configure and use on an adhoc basis
driver.get("https://test.myenv.com");
Instead delegate the handling of information to other classes.
final AppEnv myenv = new AppEnv();
driver.get(myenv.getURL());
see EnvironmentAbstractionTest
- helps us avoid hard coding in our test:
- urls,
- usernames,
- passwords,
- etc.
A 'Config' abstraction.
Because these classes often contain sensitive data they are likely to:
- configure themselves from System Properties e.g. JVM -D parameters
- configure themselves from Environment Variables
- throw runtime exceptions if data not set
- possibly have non-sensitive defaults
- Pros?
- Cons?
- Pros
- switching between environments becomes easier
- Cons
- can become complicated
- often not enough flexibility e.g. config by file, harder to amend at runtime
public interface Checkbox {
public boolean isChecked();
public Checkbox check();
public Checkbox uncheck();
public Checkbox toggle();
}
- Would you include 'toggle'?
- Does it need to be an interface?
public class CheckboxHTMLElement implements WrapsElement {
private final WebElement webelement;
public CheckboxHTMLElement(WebElement element){
webelement = element;
}
public boolean isChecked() {
return webelement.isSelected();
}
public void check() {
if(!isChecked())
toggle();
}
//...
- Existing support: Select,
- Possible: TextBox, Checkbox, TextBox, File etc.
WrapsElement
enforces a 'standard' for abstractions- Can enforce Semantics
- Checkbox: isChecked, check(), uncheck(), toggle()
- TextBox: clear(), enterText()
- etc.
- Pass back from Page Objects into test?
- e.g. Checkbox in elementabstraction
- new CheckBox(element)
- Pros?
- Cons?
- Pros
- Can help 'restrict' code i.e. check or uncheck, rather than click
- enforces 'semantics'
- Cons
- May have to create a custom page factory if using annotations
- not returning the WebElement may restrict people using your library
-
The most obvious automation abstraction
-
What is it?
- A page? A Component?
-
Experiences?
-
Do web applications still have pages?
public class TodoMVCPojoPage {
private static final By TODO_ITEMS = By.cssSelector(
"ul.todo-list li:not(.hidden)");
private final WebDriver driver;
private final String url;
public TodoMVCPojoPage(WebDriver driver, String url) {
this.driver = driver;
this.url = url;
}
public List<WebElement> getTodoItems() {
return driver.findElements(TODO_ITEMS);
}
//... see TodoMVCPojoPage.java and TodoMVCPojoTest.java
//... package uk.co.compendiumdev.examples.pojo;
- What methods does it have?
- Functional
- login(username, password),
- loginAs(user)
- Structural
- enterName(username), enterPassword(password), clickLogin(), submitLoginForm(username, password)
- Functional
- Does it expose elements or not?
- public WebElement loginButton;
- public WebElement getLoginButton();
- public clickLoginButton();
- Does a Page Object return other pages?
public IssueListPage submit(){
driver.findElement(By.id(“submit”)).click();
return new IssueListPage(driver);
}
- Pros?
- Cons?
- Experiences
- What rules / guidelines / biases do you use for page objects?
- No constructors, static factory methods
- Do not expose WebElements
- Never Return other Page Objects
- Physical Actions, not logical
- Only logical actions, not physical
- POJO
- Page Factory
- LoadableComponent
- SlowLoadableComponent
- Functional vs Structural
- Fluent Interfaces
- Other approaches?
- POJO
- Plain object, driver in constructor, methods use driver.
- Page Factory
- Annotated elements, lazy instantiation via reflection
- LoadableComponent
- Common interface (load, isLoaded), isLoaded throws Error
- SlowLoadableComponent
- Common interface, waits for on isLoaded
- Functional vs Structural
- type of methods represent user actions or physical actions
- Fluent Interfaces
- to support method chaining for readable tests
- Other approaches?
see packages uk.co.compendiumdev.examples
- pojo,
- pagefactory,
- navigation,
- structuralvsfunctional,
- synchronisedcomponent,
- component
public class TodoMVCPojoPage {
private static final By TODO_ITEMS = By.cssSelector(
"ul.todo-list li:not(.hidden)");
private final WebDriver driver;
private final String url;
public TodoMVCPojoPage(WebDriver driver, String url) {
this.driver = driver;
this.url = url;
}
public List<WebElement> getTodoItems() {
return driver.findElements(TODO_ITEMS);
}
//... see TodoMVCPojoPage.java and TodoMVCPojoTest.java
//... package uk.co.compendiumdev.examples.pojo;
- Simple Class
- WebDriver passed to constructor
- Refactor to Composition of any components
- Single Responsibility?
- Model Physical Page?
- Model Common Functions use on the page?
see pojo \ TodoMVCPojoPage , other examples in the code base
- Pros
- Cons
- Pros
- easy to maintain
- simple to understand and extend, requires basic java knowledge
- easy to add synchronisation
- Cons
- might not have a common interface e.g. 'get', 'load', 'waitForReady'
- easy to add too many methods and try to do too much
@FindBy(how = How.CSS, using="#todo-count strong")
private WebElement countElementStrong;
@FindBy(how = How.CSS, using="#todo-count")
private WebElement countElement;
@FindBy(how = How.CSS, using="#filters li a")
List<WebElement> filters;
public ApplicationPageStructuralFactory(WebDriver driver,
TodoMVCSite todoMVCSite) {
PageFactory.initElements(driver, this);
this.driver = driver;
//...
- Annotate fields with @FindBy
- Instantiate in constructor using PageFactory.initElements
- Can create your own page factory initialiser
compare pagefactory package with pojo
- Pros
- Cons
- Pros
- fast to get started
- annotated elements can be used in WebDriverWait Expected Conditions that take WebElement parameters
- Cons
- people often forget about synchronisation
- easy to fall back on implicit wait to fix synchronisation problems
- too easy to make WebElements public
- doesn't handle relative element finding easily
- people can find it hard to mix method and annotation approach
Model the functions the page exposes, or the physical structure of the page.
- Functional
- loginAs(username, password)
- loginAs(user) // understands domain
- Structural
- enterUsername(username)
- enterPassword(password)
- clickLoginButton()
- submitLoginForm(username, password)
- getLoginButton()
private StructuralPage structure;
public FunctionalPage(WebDriver driver,
TodoMVCSite todoMVCSite) {
structure = new StructuralPage(driver, todoMVCSite);
}
public int getCountInFooter() {
// if there is an exception because count is not visible
// then there are 0 items
try{
return structure.getCountInFooter();
}catch(Exception e){
return 0;
}
}
public void filterOnAll() {
structure.clickOnFilter(0);
}
- Pros?
- Cons?
- Pros
- simple modelling approach
- Cons
- might be too simple a model
- might be confusing - what is logical, what is physical?
- delegating from functional to structural seen as unnecessary maintenance
- Many page objects are a mix of both - Pros? Cons?
- How far could structural go?
- a data object that manages By locators?
- methods to do stuff, and locators?
- should locators be public?
see package structuralvsfunctional
- One way of answering “what methods to put on a page object”
- Is it functional / behavioural?
- Is it structural?
- 'logical' vs 'physical'?
- Functional 'uses' Structural implementation
- Allow writing tests in DSL type manner
e.g.
page.
open().
and().
createTodo("My First Todo").
createTodo("M
then().
toggleTodoStatus(1);
see fluent package
public FluentTodoMVCPage createTodo(String name){
return typeIntoNewToDo(name + Keys.ENTER);
}
public FluentTodoMVCPage open() {
driver.get(url);
return this;
}
public FluentTodoMVCPage and(){
return this;
}
public FluentTodoMVCPage then(){
return this;
}
- Methods return the page object or other objects to allow method chaining
- Instead of
- void clickDeleteButton();
- PageObject clickDeleteButton();
- Syntactic sugar methods:
- and(), then(), also()
- Work well at high levels of abstraction
- Pros?
- Cons?
- Pros
- code completion makes writing test code easier
- static Factory methods can be used to avoid
new Page
- code is easy to read
- might avoid needing a BDD framework
- Cons
- can encourage long 'sentences' in the test code
- chains can get deep if not careful about objects returned
- Extending LoadableComponent provides a common interface for 'get'ing pages.
- With an inbuilt erroring mechanism to check if on correct page
see SynchronisedComponentPojoPage
public class SynchronisedComponentPojoPage extends
LoadableComponent {
//...
@Override
protected void load() {
driver.get(url);
}
@Override
protected void isLoaded() throws Error {
boolean loaded = false;
try{
if(driver.findElement(
By.className("new-todo"))!=null){
loaded = true;
}
}catch (Exception e){
}
if(!loaded){
throw new Error("page not loaded");
}
}
//...
- Extends LoadableComponent
- Get
- If isLoaded, return this
- Else load()
- Check isLoaded()
- Get
- Implement load
- Add any synchronisation in load to wait for the loading. Exit only when 'loaded'.
- Implement isLoaded
- Check, and throw Error if not loaded
- Pros?
- Cons?
see SynchronisedComponentPojoPage
- Pros
- provides a common interface to 'pages' and 'components' i.e.
get
- can make isLoaded public if necessary
- provides a common interface to 'pages' and 'components' i.e.
- Cons
- Uses Error instead of Exception
- Because of the use of 'error', I tend not to use this
- But I do use the concept of a single interface
- and I like the notion of 'isLoaded' but I prefer it to return a boolean and create a custom 'ExpectedCondition' for detecting if 'isLoaded'
- The most important concept for reliable execution
- basically
- wait for something to be ready,
- then interact with it
- Can be done at any level - page, page load, element methods, functions
- Implicit Wait
- configure once in WebDriver and all findElement calls will 'wait' for the element
- Explicit Wait
- explicitly use
new WebDriverWait(driver, 5).until(...)
in our code
- explicitly use
driver.manage().timeouts().
implicitlyWait(15L, TimeUnit.SECONDS);
Assertions.assertTrue(
driver.findElement(
By.id("filters")).
isDisplayed());
The findElement 'waits' until element is in DOM before isDisplayed
driver.manage().timeouts().
implicitlyWait(0L, TimeUnit.SECONDS);
WebDriverWait wait = new WebDriverWait(driver,15);
wait.until(ExpectedConditions.
elementToBeClickable(By.id("filters")));
Example: firsttests / WebDriverWaitTest.java
Instead of:
WebElement createTodo =
driver.findElement(By.className("new-todo"));
WebElement createTodo =
new WebDriverWait(driver,10).
until(ExpectedConditions.
visibilityOfElementLocated(
By.className("new-todo")));
Thoughts? Discuss?
- Implicit Wait or Explicit Wait?
- Synchronise in Tests or Abstraction Layers?
Navigation is often not thought about.
Many people default to methods that navigate returning new Page objects for the navigation path.
Options:
- Direct in Test e.g.
driver.get
- Instantiate new pages based on test flow
- Page Object methods might return other Pages
- We might use navigation objects
see navigation package
- Instantiate new pages based on test flow
- Navigation as side-effect, may have to bypass 'get'
- Loadable pages, non-loadable, support classes
allPage.clickOnActiveFilter(); // causes page change
activePage = new ActiveToDosPage(driver, todoMVCSite);
activePage.waitUntilLoaded();
Assertions.assertEquals(4, countVisibleTodos());
- e.g. a method on the login page might be
- MyAccountPage clickLogin();
- Returns a new page
- void clickLogin();
- MyAccountPage clickLogin();
ActiveToDosPage activePage = allPage.clickOnActiveFilter();
activePage.waitUntilLoaded();
- direct, or Path based (current page → desired page)
- Navigate.to(MyAccountPage.class)
- Jump.to(MyAccountPage.class)
new TodoMVCNav(driver, todoMVCSite).
open(ACTIVE_TODOS_PAGE);
- Pros?
- Cons?
see navigation package
- Pros
- separate navigation means reduced maintenance
- make test responsible for knowing navigation path
- navigation object means reducing test maintenance
- Cons
- pages returning other pages increases responsibility and maintenance
- i.e. page also has to know about app flow
- what if an action has multiple options e.g.
clickLogin()
clickLoginBad()
- inconsistency in page object methods, some return pages, some don't
- encourages method chaining without fluency
- pages returning other pages increases responsibility and maintenance
- Components on the screen
- e.g.
component
package
- e.g.
- e.g.
VisibleToDoEntry
,Filters
,Footer
,Header
,VisibleToDoList
, etc. - Could have 'functional' representation for repeated items e.g. login forms
- Could have 'structural' representation
- Likely use : page object composition
VisibleToDoEntry todo;
todo = VisibleToDoEntry.getToDoAt(driver, 1);
todo.edit("visible todo");
todo.markActive();
see component
package
- Pros?
- Cons?
- Pros
- can synchronise at component level
- more semantic interaction
- avoiding overloading page with methods
- reduced maintenance of page objects
- Cons
- might lead to page methods duplicating component functionality
- pages returning components might lead to too much method chaining
- same interface as LoadableComponent but 'get' is a synch mechanism
- Extends SlowLoadableComponent
- Constructor has to additionally call
- super(new SystemClock(), 10);
- Where 10 is a timeout # of seconds
- get()
- If isLoaded then return this Else load
- While not loaded{ wait for 200 millseconds}
- Implement load and isLoaded
- But can remove sync loops from load
public class TodoEditField extends SlowLoadableComponent {
private final WebDriver driver;
public TodoEditField(WebDriver driver) {
super(Clock.systemDefaultZone(), 20);
this.driver = driver;
}
@Override
protected void load() {}
@Override
protected void isLoaded() throws Error {
try {
WebElement element = driver.findElement(
By.cssSelector("li.editing input.edit"));
if (element.isEnabled() && element.isDisplayed()) {
return;
}
}catch(Exception e){}
throw new Error("Component not loaded");
}
//...
synchronisedcomponent \ TodoEditField
- Pros?
- Cons?
- Pros
- consistent interface
- encourages creation of components
- Cons
- throws error not exception
- synchronisation requires a
get
but semantically that might wrong i.e. component exists, whyget
it?
- Logical Objects
- ToDo
- ToDoList
- Physical Objects
- LocallyStoredToDo
- Actors
- User
- Environment
- Site
- Implementation
- Actions
- TodoActions (high level activities) e.g.
createMultipleTodos
- TodoActions (high level activities) e.g.
user.
createNewToDo("My First Todo").
and().
createNewToDo("My Second Todo");
see DomainTest
- Domain objects make random data easier to manage
RandomTodoGenerator rnd =
new RandomTodoGenerator();
myTodos.addNewToDoItem(
rnd.getRandomTodoName());
Discus
- Instead of
todoMVC.enterNewToDo(“New Item”)
- We could have have
ToDoItem newItem = new ToDoItem(“New Item”);
todoMVC.enterNewToDo(newItem);
- Discuss
See code in DomainBasedTest
e.g. User
-
user.createNewToDo(“new item”)
-
user.createNewToDo(newItem)
-
Discuss
See use in NoAbstractionTest
- Pros?
- Cons?
- Pros
- more semantic tests
- random data can expand coverage
- can make tests smaller
- delegate to other objects to reduce maintenance
- Cons
- coupling might mean more maintenance if domain changes
- random data needs management - logging and seeding
- how far should domains be mixed?
- should a page know about a TodoItem object?
- or just the text of the todo?
Feature: We can create and edit To Do lists in ToDoMvc
We want to amend todos in ToDoMVC because that is
the set of exercises on the abstraction tutorial
Scenario: Create a ToDo Item
Given a user opens a blank ToDoMVC page
When the user creates a todo "new task"
Then they see 1 todo item on the page
- Implement steps using highest appropriate abstraction layer
- CucumberJVM as 'DSL implementor'
- 'Expressibility' vs 'Step Re-use'
- See
todomvc.feature
anddomaindslcucumberjvm
package
- Pros?
- Cons?
- Pros
- easy to use DSL creation
- easy data driven functionality
- Cons
- people think they are 'doing BDD'
- people try to use for everything
- code embedded in step rather than abstraction forces use of Cucumber
- more skills required on team
- should we create a feature or a test? splits focus
- Assertions are important for keeping
@Test
code readable - Self-documenting assertions can make it easier to understand the test fast
- Assertion abstractions can usually be mixed across Cucumber, and Test Execution Frameworks
Assertions.assertEquals(0,
todoMVC.getTodoItems().size());
org.opentest4j.AssertionFailedError:
Expected :0
Actual :1
- Junit 4
Assert.
- Junit 5
Assertions.
- Hamcrest built in
assertThat(todoMVC.getTodoItems().size(), is(0));
java.lang.AssertionError:
Expected: is <0>
but: was <1>
Expected :is <0>
Actual :<1>
- english language syntax
- static inclusion of 'is' etc.
- More readable error messages than JUnit
- http://hamcrest.org
assertThat(todoMVC.getTodoItems().size()).isEqualTo(0);
org.opentest4j.AssertionFailedError:
Expecting:
<1>
to be equal to:
<0>
but was not.
Expected :0
Actual :1
- method chaining can make it easier to build assertions with less knowledge of the library
- has SoftAssertions to allow multiple assertions 'fail' in test
- has BDD style assertion
then
todoMVC.editItem(0, "Edited Todo");
then(todoMVC.getTodoItems().size()).isEqualTo(1);
- Pros?
- Cons?
- Pros
- flexibility of how you want assertions to read
- can improve on error understanding by using a different framework
- different libraries can make assertions easier to write
- Cons
- might be harder to switch if existing tests
- need to learn an Assertion library as well as an execution framework
- soft asserts might encourage too many assertions in same test
public class BaseClassExampleTest
extends SeleniumBaseTestCase {
@Test
public void canUseADefaultBrowser(){
final TodoMVCSite todoMVCSite = new TodoMVCSite();
driver().get(todoMVCSite.getURL());
Assertions.assertTrue(
driver().getTitle().contains("TodoMVC"));
}
}
- People often create a Base Test Case
- And all tests extend the Base Test Case
- This can create the browsers and supporting components/data
see basetest package
- Pros?
- Cons?
- Pros
- possibly less code to write in
@Test
- avoids boiler plate
- possibly less code to write in
- Cons
- might be hard to know what Best Test does
- might not be possible to 'create a browser' or other setup without extending base test which might limit my testing.
- base test case might setup more than it needs
- what if we mix REST API and Web Usage in test, do we extend a RestAndWebTest?
Personal bias alert: I prefer not to use this, I prefer composition rather than inheritance.
- WebDriver itself is a browser abstraction
- But it may not give us access to all the functionality e.g driver.manage gives access to cookies but not local storage
- Using (JavaScriptExecutor) allows us access to anything that JS has access to, and that can include other features of the browser
- We may also have to 'workaround' browser changes or nuances
public class Storage implements LocalStorage{
private final JavascriptExecutor js;
public Storage(JavascriptExecutor js){
this.js = js;
}
@Override
public String getItem(String key) {
return (String) js.executeScript( String.format(
"return window.localStorage.getItem('%s');", key));
}
//...
- WebDriver has a LocalStorage interface that isn't universally implemented
- Implementing the Interface may make it easier to maintain longer term
- Future versions of WebDriver and Browser Drivers might reduce need for this
- Pros?
- Cons?
- Pros
- gain access to more than WebDriver provides
- Cons
- more code to maintain
- risk of cross browser issues in our abstraction code
- may require synchronisation
- may be vulnerable to future security browser changes
public class EnsureWebElementIs {
public static void inViewOnThePage(
WebDriver driver,
WebElement todoListItem) {
((JavascriptExecutor) driver).
executeScript(
"window.scrollTo(0," +
todoListItem.getLocation().getY() + ")");
}
}
- often use JavaScript Executor
- are often made unnecessary by WebDriver changes
- e.g. now WebDriver scrolls elements in to view
- can help mitigate driver changes e.g.
public static void closeDriver(WebDriver adriver){
try {
adriver.close();
adriver.quit();
}catch(Exception e){}
}
- Pros?
- Cons?
- Pros
- adding workarounds into abstraction layers keeps test running without impacting test code
- Cons
- people may not realise that we are using workaround and don't learn to debug flaky tests
- people assume this is web driver functionality and can't work in other projects or companies
- what if the test should fail, and workaround patches the system too much
- Base Page
- JUnit 5 Selenium
- Synchronisation Abstractions
- Data Abstractions
- Screenplay Pattern
- Internal DSLs
- Model Based Testing
- Base Page - page all page objects inherit from
- JUnit 5 Selenium - additional annotations to control and configure selenium
- Synchronisation Abstractions - waiting classes, chaining waits, polling
- Data Abstractions - random data, payload objects, property based, data driven
- Screenplay Pattern - small actions vs pages
- Internal DSLs - fluent and GWT like DSLs
- Model Based Testing - a model traversed to automate
https://github.com/bonigarcia/selenium-jupiter
@ExtendWith(SeleniumExtension.class)
public class ASeleniumTest {
@Test
public void useChrome(ChromeDriver driver) {}
@Test
void useRemoteChrome(@DriverUrl("http://localhost:4444/wd/hub")
@DriverCapabilities("browserName=chrome")
RemoteWebDriver driver) {}
-
Actors
-
Roles
-
Tasks
- You control your focus.
- what do you want to experiment with?
- Model the application
- Build abstraction layers to implement the model
- Experiment with additional approaches mentioned that you haven't used
- Use existing code and amend and experiment
github.com/eviltester/automationAbstractions
- look at the 'examples' package, run tests, experiment
- use the 'todos' view in IntelliJ, pick and choose some exercises
- use the Case Studies as a basis to start from - refactor and expand
- Start from scratch (Automate Scenarios):
- Create a ToDo (check: count, text)
- Can Delete a ToDo (check: footers, count)
- Can Mark a ToDo as completed
- Can create a bunch of ToDos and delete, mark as complete etc.
- Add additional checks as required
- Create Abstraction layers as appropriate
github.com/eviltester/automationAbstractions
- Did anyone do anything different?
- Any other abstraction approaches you used?
- Anything else to add?
- Right / Wrong?
- Decisions?
- Project/Team/Organisation Standards?
- Not about "Right" and "Wrong"
- Those are 'fixed' viewpoints
Instead:
- agreed approach because...
- we decided on X, because...
- abstractions and models change and flex
- Driver
- Build around that so instantiate any page or component as required at any time
- Synchronisation
- To make sure that the desired object is available and ready for use (as defined by synchronisation)
- Navigation
- Implicit (via actions)
- Explicit
- Open/jump (via driver.get)
- To (state model from current, to desired)
- Examine some common abstraction biases
- Discuss pros/cons based on experience
e.g.
- loginAs(user)
- List getUsersList()
- The 'limits' and overlap of Abstraction Layers
- Build it now, or 'refactor to' later
- How much change is anticipated?
- To which layer? GUI, Business domain, Workflow?
- Who is working with the automation code?
- Skill levels? Support needed?
- How/When with the automation execute?
“To the creative mind there is no right or wrong. Every action is an experiment, and every experiment yields its fruit in knowledge.”
- www.eviltester.com
- @eviltester
- www.youtube.com/user/EviltesterVideos
- www.compendiumdev.co.uk
- uk.linkedin.com/in/eviltester
- Linkedin - @eviltester
- Twitter - @eviltester
- Youtube - EvilTesterVideos
- Instagram - @eviltester
- Github - @eviltester
- Slideshare - @eviltester
- Facebook - @eviltester
- Pinterest - @eviltester
Alan is a test consultant who enjoys testing at a technical level using techniques from psychotherapy and computer science. In his spare time Alan is currently programming a multi-user text adventure game and some buggy JavaScript games in the style of the Cascade Cassette 50. Alan is the author of the books "Dear Evil Tester", "Java For Testers" and "Automating and Testing a REST API".
Alan's main website is www.eviltester.com and he blogs at blog.eviltester.com