Angelov Anton Design Patterns For Highquality Automated Test
Angelov Anton Design Patterns For Highquality Automated Test
Automated Tests
Java Edition
Clean Code for Bulletproof Tests
Anton Angelov
Automate The Planet
Table of Contents
Credits
About the Author
About the Reviewers
Acknowledgements
Foreword
Preface
What this book covers
Chapter 1. Defining High-Quality Test Attributes
Chapter 2. Optimizing and Refactoring Legacy Flaky Tests
Chapter 3. Strategies for Speeding-up the Tests
Chapter 4. Test Readability
Chapter 5. Enhancing the Test Maintainability and Reusability
Chapter 6. API Usability
Chapter 7. Building Extensibility in Your Test Library
Chapter 8. Assessment System for Tests’ Architecture Design
Chapter 9. Benchmarking for Assessing Automated Test Components Performance
Chapter 10. Test Data Preparation and Configuring Test Environments
Appendix 1. Defining the Primary Problems that Test Automation Frameworks Solve
Appendix 2. Most Exhaustive CSS Selectors Cheat Sheet
Appendix 3. Most Exhaustive XPath Selectors Cheat Sheet
Who Is This Book For?
Before You Get Started
Conventions
Reader feedback
Errata
Piracy
Questions
Chapter 1. Defining High-Quality Test Attributes
Different Types of Tests
Unit Tests
Integration Tests
System Tests
What Is a Test Automation Framework?
What Is a Software Library?
What Is a Framework?
Test Automation Framework
Is Selenium WebDriver a Test Framework?
SOLID Principles
OCP – Open/Closed Principle
LSP – Liskov Substitution Principle
ISP – Interface Segregation Principle
DIP – Dependency Inversion Principle
High-Quality Test Attributes
What Is a Design Pattern?
Test Maintainability and Reusability
Test Readability
API Usability
Extensibility
Learning Curve
Summary
Questions
Chapter 2. Optimizing and Refactoring Legacy Flaky Tests
Writing the First Real-World Automated Test
First Automated Test Case Explanation
Second Automated Test Case Explanation
Third Automated Test Case Explanation
First Automated Test Code
Second Automated Test Code
Third Automated Test Code
Reasons for Failures
Refactoring Tests
Implicit VS Explicit Waits
Implementing Explicit Waits in Tests
DRY- Do Not Repeat Yourself Principle
Decorator Design Pattern for Fixing WebDriver Unstable Actions
Decorator Design Pattern
Decorator Design Pattern Implementation for IWebElement
Decorator Design Pattern Implementation for WebDriver
Decorator Design Pattern in Tests
Test Independence- Isolation Principle
Refactoring Tests to Follow Test Independence- Isolation Principle
Summary
Questions
Chapter 3. Strategies for Speeding-up the Tests
Instrumenting the Test Code to Find Possible Points for Optimization
Optimize Authentication
How to Wait for Asynchronous Requests to Finish in Tests?
Waiting for All AJAX Requests Completion
Optimize Browser Initialization- Observer Design Pattern
Observer Design Pattern
Observer Design Pattern Implementation
Configure Browser Behavior via Attribute
Isolated Browser Initialization for Each Test
Running Tests in Parallel
Black Hole Proxy Approach
Implementing the Black Hole Proxy
Summary
Questions
Chapter 4. Test Readability
Page Object Model Design Pattern
Page Object Model Design Pattern with PageFactory
Page Object Model Design Pattern without PageFactory
Page Object Model Usage in Tests
Handling Common Page Elements and Actions
Defining Common Page Elements
Non-DRY Page Objects
First Version of Reusing Common Elements
Creating Common Page Section Page Objects
Page Sections Usage in Page Objects - Version One
Page Sections Usage in Page Objects - Version Two
Page Sections Usage in Tests
High-Quality Code - Meaningful Names
General Naming Guidelines
Naming Classes
Naming New Versions of Existing API
Naming the Methods - The Right Way
General Naming Guidelines
Using Meaningful Method Names
Best Practices for Method Parameters
Follow Coding Standards - Tools
Enforcing Coding Standards Using EditorConfig
Enforcing Coding Standards Using CheckStyle-IDEA Plug-in
Summary
Questions
Chapter 5. Enhancing the Test Maintainability and Reusability
Navigatable Page Objects- Template Method Design Pattern
Non-refactored Version Page Objects
Create Separate Base Classes for Navigatable and Non-navigatable Pages
Template Method Design Pattern
Template Method Design Pattern Implementation
Using Composition Principle
Non-refactored Version Page Objects without Composition
Elements and Assertions - Composition Implementation
Using Composition in Tests
Reuse Elements and Assertions via Base Pages
Base Elements and Assertions Classes
BaseElements and BaseAssertions Usage
Defining AssertableEShopPage and NavigatableAssertableEShopPage
AssertableEShopPage and NavigatableAssertableEShopPage Usage
Final Code Reuse with NewInstanceFactory
Reuse Test Workflows - Facade Design Pattern
Test Workflows Naive Implementation
Facade Design Pattern
Facade Design Pattern Implementation
Combining Facade with Template Method Design Pattern
Purchase Facade with Template Methods
Concrete Facade Implementation
Summary
Questions
Chapter 6. API Usability
Interface Segregation principle for WebDriver Decorator
WebDriver Decorator Current Implementation
Splitting Driver Interface into Smaller Interfaces
Smaller Interfaces Usage in Page Objects
Use Page Objects Through Singleton Design Pattern
Singleton Design Pattern
Singleton Design Pattern Implementation
Singleton Design Pattern Usage in Tests
App Design Pattern for Creating Page Objects
App Design Pattern Implementation
App Usage in Tests
Fluent API Page Objects
Fluent API Implementation
Using Fluent API in Tests
Page Objects Elements Access Styles
Exposing Elements Through Public Get Methods
Accessing Elements Through Elements Public Get Method
Hiding Elements in Tests
Hiding Element Unnecessary Details
Summary
Questions
Chapter 7. Building Extensibility in Your Test Library
Building Extensibility for Finding Elements through Strategy Design Pattern
Vanilla WebDriver Finding of Elements
Strategy Design Pattern
Reviewing Current Version for Finding Elements
Creating Elements Find Strategies
Refactoring ElementFindService
Improving Elements Find Strategies API Usability
Adding New Find Strategies via Extension Methods
Building Extensibility for Waiting for Elements through Strategy Design Pattern
Vanilla WebDriver Waiting for Elements
Reviewing Current Version of Elements Waiting
Creating Elements Wait Strategies
Creating ElementWaitService
Adding Extensibility Points through EventFiringWebDriver
Integrating EventFiringWebDriver
Summary
Questions
Chapter 8. Assessment System for Test Architecture Design
Assessment System Introduction
What Problem Are We Trying to Solve?
Criteria Definitions
1. Readability
2. Maintainability
3. Reusability
4. API Usability
5. Extensibility
6. Code Complexity Index
7. Learning Curve
8. KISS*
Steps to Apply
Assessment System Usage Examples
Tests without Page Objects Assessment
Architecture Design Overview
1. Readability
2. Maintainability
3. Reusability
4. API Usability
5. Extensibility
6. Code Complexity Index
7. Learning Curve
Test Design Index
Tests with Page Objects Assessment
Architecture Design Overview
1. Readability
2. Maintainability
3. Reusability
4. API Usability
5. Extensibility
6. Code Complexity Index
7. Learning Curve
Test Design Index
Tests with Facades Assessment
Architecture Design Overview
1. Readability
2. Maintainability
3. Reusability
4. API Usability
5. Extensibility
6. Code Complexity Index
7. Learning Curve
Test Design Index
Final Assessment
Summary
Questions
Chapter 9. Benchmarking for Assessing Automated Test Components
Performance
What Is Benchmarking?
Benchmarking Your Code with JMH - Java Microbenchmark Harness
Main Features
JMH Example
Benchmark Button Click Solutions
Button Benchmark Experiment
JMH Profiling
StackProfiler
GC Profiler
WinPerfAsmProfiler Profiler
Optimized Browser Initialization Benchmark Integration
Updates in Observer Classes
Summary
Questions
Chapter 10. Test Data Preparation and Test Environments
Stop Hard-coding Input Data
Problematic Test Data
Configuration Transformations
Creating URL Settings
Creating WebDriver Timeouts Settings
Creating Default Billing Info Settings
Refactoring Default Billing Info Settings
Creating Default Browser Settings
Environmental Variables
Introducing Test Fixtures
Using Faker for Generating Data
Using TestNG Data Driven Tests
Using an API as a Source of Fixture Data
Using Data Stubs
Using an API or DB for Verification
Summary
Questions
Appendix 1. Defining the Primary Problems that Test Automation
Frameworks Solve
Sub Problem 1 - Repetition and People Boredom, Less Reliable
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 2 - Engineers Are Not So Good with Numbers, Accuracy
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 3 - Time for Feedback- Release
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 4 - Regression Issues Prevention
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 5 - Skipping Part of the Scope
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 6 - Ping-pong
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 7 - Money, Time, Effort for Maintaining Test Artifacts
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 8 - Various App Configurations
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 9 - Low QA Team Morale
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 10 - Questioned Professionalism
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 11 - Provide Evidence What You Did
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 12 - Holidays and Sickness, 24 Hours
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 13 - Some Things Cannot be Tested Manually
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 14 – Consistency
Automation Solution Hypothesis
Subsequent Problems
Sub-Problem 15 - Faster Scale-up and Scale-down
Automation Solution Hypothesis
Subsequent Problems
Summary
Appendix 2. Most Exhaustive CSS Selectors Cheat Sheet
Element Selectors
Contextual Selectors
Attribute Selectors
Useful n Values
Pseudo-class Selectors that Apply to Siblings
Pseudo-class Selectors for Link and User States
Further Reading
Appendix 3. Most Exhaustive XPath Selectors Cheat Sheet
Contextual Selectors
Attribute Selectors
XPath Methods
Axis Navigation
Math Methods
String Methods
Further Reading
Bibliography
Credits
Copyright @ 2021 Automate The Planet
All rights reserved. No part of this book may be reproduced, stored in a
retrieval system, or transmitted in any form or by any means, without the
prior written permission of the author, except in the case of brief quotations
embedded in critical articles or reviews. Every effort has been made in the
preparation of this book to ensure the accuracy of the information presented.
However, the information contained in this book is sold without warranty,
either express or implied. Neither the author, nor Automate The Planet, and
its dealers and distributors will be held liable for any damages caused or
alleged to be caused directly or indirectly by this book.
The author has endeavored to provide trademark information about all the
companies and products mentioned in this book by the appropriate use of
capitals. However, the author cannot guarantee the accuracy of this
information.
First published: March 2021
Production reference: Published by Automate The Planet Ltd.
Address: https://www.automatetheplanet.com/
Book cover design and formatting by Martin Kolev
About the Author
Anton Angelov is CTO and Co-founder of Automate The Planet Ltd,
inventor of BELLATRIX Test Automation Framework. Nowadays, he directs
a team of passionate engineers helping companies succeed with their test
automation. Additionally, he consults companies, leads automated testing
training series in C# and Java, writes books, and gives conference talks.
– 280+ Published Articles Automate The Planet
– 120+ Published Articles Code Project
– 60+ Published Articles DZone as Most Valuable Blogger
– 6+ Articles Published in Quality Magazines
– 20+ Given International Conferences Talks
– 3 books published
– 5,000,000+ article views
– 1000 000+ amazing readers for 2020
– Read in 180+ countries
About the Reviewers
Nikolay Avramov is a Senior Developer in Test and Team Lead at Automate
The Planet. For the past 8 years, he worked as a Quality Assurance Engineer
in Telerik (later acquired by Progress software). He started his career in the
field from ground zero- from taking part of the Telerik Academy to becoming
a Principal QA Engineer/Ninja that is in the heart of any innovation or
automation initiative. Through his work he has developed, and integrated
multiple custom frameworks and tools used across the entire company. To
name a few- a custom Load and Performance Testing Framework; System
Testing Framework; Web Service Testing Framework; Visual Verification
Framework; and many other productivity tools that assured the highest
quality of all web and business assets of the company. He found his true
passion in Automation while looking for a ‘weapon’ to fight the boring, non-
productive daily tasks that a QA faces. By reducing the manual repeatable
jobs, he believes that we can make the world a better place, full of happy and
productive people that are ready to tackle the next challenging task, instead of
the next pile of test scenarios to cover by hand. He is a passionate technology
geek and automation specialist that strives to implement optimizations and
achieve high-quality standards in any aspect of life.
A few words for the book:
The book will pretty much give you the building blocks you need and will
take you through the natural process of building a fully fletched test
automation framework within a few hour’s read.
The beginning and the end of the book shows the difference between the
rookie QA enthusiast and the automation ninja. This knowledge will give you
the power to make educated decisions for common problems in test
automation and will give you the right arguments to defend them in front of
any manager or developer.
Acknowledgements
The content of the book is a result of the accumulated knowledge of many
great engineers and IT professionals. Many good programmers tend to be
very egocentric, believing that they are always right and know everything.
Often, they forget how they got their knowledge. It is a result not only of our
efforts but a mixture of personal effort/work, luck, living, and working
environment. I believe that in this phase of my life, I know enough "tips and
tricks" so that I can write a book. However, this wouldn't be possible without
the help of all the people that took some part of my life. The example and
hard work of my parents, high school, and university teachers. Later, the
many mentors and colleagues I had in the past companies I worked for. I am
a person who is a natural learner and regularly reads new books and watches
video courses/lectures. I wouldn't be as good as I am now without the
valuable information in these materials.
As some of you know, for the past several years, I am regularly writing
articles on my website- Automate The Planet. The book accumulates a big
part of the knowledge I tried to share during the years. But the site wouldn't
be what it is without your support! So, I would like to say, "Thank You!" to
all fans and readers. The same is valid for all the talks I gave to many
international DEV and QA conferences. Thank you for being there, hearing
and trying to apply the ideas I talk about.
Most importantly, I want to say BIG THANK YOU to my colleague Nikolay
Avramov for agreeing to help me with the technical and editorial tasks.
Without his help and support, the quality of the book wouldn't be the same!
My partner in crime at Automate The Planet Martin Kolev is responsible for
all visual enhancements, the marketing, and many other tedious tasks around
the project.
Thanks to all my friends that were there for me and hearing me talking geek
stuff about the book.
Foreword
Since I usually skip the Foreword chapters of other books, I will try to be
short. My core belief is that to achieve high-quality test automation that
brings value- you need to understand core programming concepts such as
SOLID and the usage of design patterns. After you master them, the usual
career transition is into more architecture roles, such as choosing the best
possible approaches for solving particular test automation challenges. This is
the essence of the book. No more “Hello world” examples but some
serious literature about test automation practices!
P.S. After the first book's success, Design Patterns for High-Quality
Automated Tests C# Edition, many people asked me when there will be a
version for Java. This is why I started refreshing my Java knowledge and
started writing. One year later, the book is here. More or less, the book
explains the same concepts, but all code examples and specifics target the
Java world. If you have read the C# edition, you can skip some of the more
theoretical chapters or recheck them for a refresher.
You may notice that I have changed the sub-title for those of you who have
purchased the C# version. I believe that the new sub-title communicates
much better the ideas of the book. I won't bother you with lengthy
introductions and discussions about what clean code means. There are whole
books about the subject. But if I had to summarize what clean code means in
one sentence, I would say: "Clean code is code that is easy to understand and
easy to change." Easy to understand means the code is easy to read, whether
that reader is the original author of the code or somebody else. Its meaning is
clear, so it minimizes the need for guesswork and the possibility of
misunderstandings. It is easy to understand on every level. Easy to change
means the code is easy to extend and refactor, and it's easy to fix bugs in the
codebase. This can be achieved if the person making the changes understands
the code and feels confident that the code changes do not break any existing
functionality. I will end the intro with two quotes by two famous authors
Robert C. Martin and Michael Feathers. [Wcc 14]
"If you want your code to be easy to write, make it easy to read."
"Clean code always looks like it was written by someone who cares. There is
nothing obvious you can do to make it better."
Preface
Design Patterns for High-Quality Automated Tests will help you write better
tests!
Conventions
In this book, you will find several styles of text that distinguish between
different kinds of information. Here are some examples of these styles, and
an explanation of their meaning.
Code words in text are shown as follows: CartPage
A block of code is set as follows:
public void clickProceedToCheckout() {
proceedToCheckout().click();
_driver.waitUntilPageLoadsCompletely();
}
Important words or code snippets are shown in bold.
Keep in mind that to make the example t and more comfortable to read, I
haven't included the 'throws exception' part of the methods' signature.
NOTE
Important notes appear in a box like this
Definition
Definition: Example
Definition description
Reader feedback
My team and I tested the Kindle content heavily and tried to make it as
pleasant as possible to read. However, please note that some titles or code
snippets can look slightly stretched due to the Kindle formatting limitations.
It is happening because there are many Kindle devices, and the content is
displayed responsive to be rendered relatively well on all of them. Moreover,
the applied format allows you to change the code and text font size. Some
publishers go with providing the code as images, but the snippets become
harder to read.
Feedback from the readers is always welcome. Let me know what you think
about the book, what you liked or disliked. To do so, write me at LinkedIn -
https://bit.ly/2NjWJ19
Errata
Although I have taken every care to ensure the accuracy of the content
mistakes do happen. If you find a mistake in the book, whatever in the text or
in the code- I would be grateful if you would report it. If you find any errata,
please send them to me via LinkedIn - https://bit.ly/2NjWJ19
Piracy
Piracy of copyright material on the Internet is an ongoing problem across all
media. If you come across any illegal copies of the work, in any form, on the
Internet, please provide me with the location address or website name
immediately so that I can pursue a remedy.
I appreciate your help in protecting the ability to bring you valuable content!
Questions
You can contact me at LinkedIn - https://bit.ly/2NjWJ19 if you are having
any problems with any aspect of the book, and I will do my best to address it.
Chapter 1. Defining High-Quality
Test Attributes
This book is not only about teaching you how to use some design patterns or
practices but furthermore to solve particular test automation problems. Some
of them are flaky tests that are hard to maintain, read, or learn to write new
tests, and the list goes on. The goal of the book is to help you write more
stable and faster tests. With the many practices and design patterns presented
in the book, you will be able to write automated tests that need less time to
figure out what went wrong, to be easier to support new requirements, ease
the new team members to learn to use your code and much more.
At the beginning of the chapter, we will go through a short list of the types of
tests. Then, I will define what exactly is a test automation framework and
some other related terms. To be able to write high-quality automated tests,
more knowledge is needed than just knowing how to program in a certain
language or use a specific framework. To solve these problems, our
automated tests should have some core high-quality test attributes. Also, we
will talk about related programming principles abbreviated SOLID.
The following topics will be covered in this chapter:
Different Types of Tests
What Is a Test Automation Framework? Definitions
SOLID Principles
High-Quality Test Attributes
Unit Tests
These are, by definition, the smallest test units. This type of test is written to
test an individual object and even individual methods of an object. Unit
testing is highly necessary because it prevents bugs from creeping in at the
lowest level. These tests rarely use any real test data and often solely rely on
generated data.
Integration Tests
They consist of several modules of code put together, allowing them to pass
data between each other. The purpose of these types of tests is to make sure
that all modules integrate and work with each other. They are also known as
component integration tests or integration tests in the small. Usually, in
this kind of testing, we still isolate third-party services such as network, file
system, and OS. Another related term is integration tests in the large, which
usually engineers use to mention integration tests that don't isolate 3rd party
dependencies.
System Tests
System Testing is the testing of a complete and fully integrated software
product. It is the highest level of testing. It executes against production
(PROD) or production-like environments, such as staging (STA). Like
integration tests, the system test also tries to verify that all the components,
including third-party services, can communicate well with each other.
These tests can verify our software's functional or non-functional
requirements. Some engineers refer to these tests as End-to-end tests, UI
tests, User Scenario tests. However, I believe there is a slight difference
between the terms.
Our WebDriver automated tests fall into the system testing category. If all
actions and assertions are performed against the user interface (UI), they
should be called UI tests. End-to-end (user scenario) test would be called a
test that is verifying a real User scenario, from the beginning to the end of the
User interaction with the system under test. No shortcuts or mocks are used -
the test mimics human interaction entirely. In the case of system tests, we are
speaking of highly optimized tests that are focusing on verifying a particular
functionality of the system. For example- instead of logging in each time
through the UI or registering new users, we can use internal APIs or DB
directly to do so. The same is valid for the performed assertions. If you use
these low levels, then it is inappropriate to call these types of tests- UI tests.
NOTE
Sometimes the term system of systems test is used. These tests verify the
integration between a couple of systems. For example, our website may use
a different internal system to register and authenticate users and another for
performing online purchases. The system of system tests would go through
all of them to verify that the functionality works as expected.
Definition: Application
An application is a software created to benefit its users by executing
specific functions, tasks or other activities. In information technologies, we
refer to application as a computer program developed to aid people to
complete specific tasks.
The first conclusion we can make after this definition is that our automated
tests are also applications. Over the years, there have been many discussions
over the internet and at conferences whether test engineers should be able to
program rather than using UI tools for automated testing. I firmly believe that
the new age test automation QA should be a good programmer, treating
his/her code as production code - following high-quality code standards.
Definition: Software Library
The software library contains pre-written code, classes, methods, interfaces,
constants, configuration data and such. Other programs can use its methods
without defining them
Definition: API
The group of methods that give us access to features of the OS, application
or other services is called application programming interface or API. If the
API is well designed, it should ease the creation of programs by giving us
suitable methods as building blocks.
Many engineers believe that in their companies, they have built custom test
frameworks. But in fact, they have libraries or groups of methods and classes
that can help them to write automated tests. The API of the library defines
how the users will access its methods, what arguments they need to pass and
so on. It is a convention of how the library is accessed and used, while the
actual implementation is handled in the library itself. The API is a
specification of what we can use whereas the software library is the actual
implementation of this specification. It should follow the prescribed rules.
We can have multiple implementations of the same API. For example, Java
and Scala implement the Java API. Both are later compiled to bytecode,
which enables Java developers to use the same methods. The same is valid
for the .NET world where C# and VB.NET are different languages, but both
implement the .NET API and compile to MSIL.
What Is a Framework?
Software frameworks have these distinctive features that separate them from
libraries:
Definition: Framework
A software framework is a software library by its core, but at the same
time, it is a bit different. We use the software libraries to build our
application without a way to change or modify them. On the contrary, the
software framework is usually developed in a more generic/abstract way
and gives us the possibility to override some of its functionalities and
modify or customize their behavior. Thus, it gives us a more reusable code
written more abstractly so that it can be reused in more users’ scenarios.
SOLID Principles
Before we can define the high-quality test attributes, we should mention
some of the well-known object-oriented programming principles. Throughout
the book I will mention some of them. SOLID is a mnemonic acronym for
five design principles. The goal of these principles is to help us in writing
more understandable, flexible, and maintainable software. SOLID stands for:
SRP – Single Responsibility Principle
OCP – Open/Closed Principle
LSP – Liskov Substitution Principle
ISP – Interface Segregation Principle
DIP – Dependency Inversion Principle
SRP – Single Responsibility Principle
NOTE
The principles are a subset of many principles promoted by Robert C.
Martin (colloquially known as "Uncle Bob"). He is best known for being
one of the authors of the Agile Manifesto. The theory of SOLID principles
was introduced by Martin in his 2000th paper "Design Principles and
Design Patterns", although the SOLID acronym itself was introduced later
by Michael Feathers.
The principles states- "Every software module should have only one reason to
change" which means that every class, method, etc. can do many things, but
they should serve a single purpose.
All the methods and variables in the class should support this purpose.
Everything else should be removed. It shouldn't be like a swiss knife
providing 20 different utility actions since if one of them is changed, all
others need to be updated too.
But if we can have each of those items separated it would be simple, easy to
maintain, and one change will not affect the others. The same principle also
applies to classes and objects in the software architecture- you can have them
as separate simpler classes.
Let me give you an example.
public void create() {
try {
// Database code goes here
}
catch (Exception ex) {
Path path = Paths.get("C:\\exception.txt");
BufferedWriter writer = Files.newBufferedWriter(path);
writer.write(ex.getLocalizedMessage());
}
}
The CustomerOrder class is doing something which it is not supposed to do. It
should create purchases and save them in the database, but if you look at the
catch block closely, you will see that it also does log activity. It has too many
responsibilities.
If we want to follow the Single Responsibility principle SRP, we should
divide the class into two separate simple classes. We can move the logging to
a separate class.
public class FileLogger {
public void createLogEntry(String error) {
Path path = Paths.get("C:\\errors.txt");
BufferedWriter writer = Files.newBufferedWriter(path);
writer.write(error);
}
}
After that the CustomerOrder class can delegate the logging to the FileLogger
class and be more focused on the creating purchases.
public class CustomerOrder {
private final FileLogger _fileLogger = new FileLogger();
@Override
public double calculateBonusPointsDiscount(double totalPrice, int points) {
return totalPrice - points * 0.5;
}
}
public class GoldDiscountCalculator extends DiscountCalculator {
@Override
public double calculateRegularDiscount(double totalPrice) {
return super.calculateRegularDiscount(totalPrice) - 50;
}
@Override
public double calculateBonusPointsDiscount(double totalPrice, int points) {
return totalPrice - points * 1;
}
}
public class PlatinumDiscountCalculator extends DiscountCalculator {
@Override
public double calculateRegularDiscount(double totalPrice) {
return super.calculateRegularDiscount(totalPrice) - 100;
}
@Override
public double calculateBonusPointsDiscount(double totalPrice, int points) {
throw new NoSuchMethodException("Not applicable for Platinum orders.");
}
}
What we did here is to add a new type of order- Platinum. We have already
applied the maximum allowed discount, so the bonus points discount is not
applicable for Platinum orders. In this case, we decide to throw
NoSuchMethodException to let the calling methods know that this operation is not
supported for the Platinum type. As the Polymorphism from the OOP
Principles states, we can use any of the child classes calculators derived from
the DiscountCalculator , as if they are the actual DiscountCalculator class.
Thanks to this principle, as you can see the code below, I have created a
collection of DiscountCalculator objects where I can add Silver, Gold, and
Platinum Discount Calculators as if they are instances of the same type -
DiscountCalculator . After that, I can go through the list using the parent customer
object and invoke the calculation methods.
List<DiscountCalculator> discountCalculators = new ArrayList<>();
discountCalculators.add(new SilverDiscountCalculator());
discountCalculators.add(new GoldDiscountCalculator());
discountCalculators.add(new PlatinumDiscountCalculator());
for (var discountCalculator : discountCalculators) {
double bonusPointsDiscount = discountCalculator.calculateBonusPointsDiscount(1250, 10);
}
So far so good, but when the calculateBonusPointsDiscount of the
PlatinumDiscountCalculator is invoked, it leads to NoSuchMethodException . The
problem is that the PlatinumDiscountCalculator object looks like a DiscountCalculator ,
but the implementation of the child object has changed the expected behavior
of the parent method. So, to follow the Liskov principle, we need to create
two interfaces, one for the regular and other for the bonus points discount.
public interface RegularDiscountCalculator {
double calculateRegularDiscount(double totalPrice);
}
The business logic behind the new requirements is that we need to send an
email for gold orders and an SMS for platinum orders since they bring our
company more money.
public class CustomerOrder {
public void create(OrderType orderType) {
try {
// Database code goes here
} catch (Exception ex) {
switch (orderType) {
case NORMAL:
new SmsLogger().createLogEntry(ex.getMessage());
break;
case SILVER:
case GOLD:
new EmailLogger().createLogEntry(ex.getMessage());
break;
case PLATINUM:
new FileLogger().createLogEntry(ex.getMessage());
break;
}
fileLogger.createLogEntry(ex.getLocalizedMessage());
}
}
}
The code violates the Single Responsibility principle again. It should be
focused on creating purchases and deciding which object to be made, while it
is not the job of the CustomerOrder class to determine which instances of the
Logger should be used. The biggest problem here is related to the new
keyword. This is an extra responsibility of making the decision which objects
to be created, so if we delegate this responsibility to someone other than the
CustomerOrder class, that will solve the problem.
We can have different child classes of CustomerOrder for the different types of
orders. Also, the logger can be passed as dependency rather than creating it in
the method itself.
public class CustomerOrder {
private final Logger logger;
NOTE
Keep in mind that the example was oversimplified. In production code, we
usually use special frameworks for the job called inversion of control
containers. The container uses the declared injection interfaces to figure out
the dependencies and based on them to inject the correct dependencies,
instead of passing them as parameters to the methods.
Definition: Maintainability
The ease with which we can customize or change our software solution to
accommodate new requirements, fix problems, improve performance.
Imagine there is a problem in your tests. How much time do you need to
figure out where the problem is? Is it an automation bug or an issue in the
system under test? In the next chapters, we will talk in detail about how to
create maintainable tests using various practices and design patterns. But if at
the beginning, you haven't designed your code in such a way, the changes
may need to be applied to multiple places which can lead to missing some of
them and thus resulting in more bugs. The better the maintainability is, the
easier it is for us to support our existing code, accommodate new
requirements, or just to fix some bugs.
A closely related principle to this definition is the so-called DRY principle-
Don't Repeat Yourself. The most basic idea behind the DRY principle is to
reduce long-term maintenance costs by removing all unnecessary duplication.
NOTE
As Donald Knuth so eloquently said, "Premature optimization is the root of
all evil (or at least most of it) in programming." We should not remove
only the duplicate code and duplicate test implementations, but we also
should remove duplicate test goals. David Thomas and Andrew Hunt
formulated the DRY principle in their book, The Pragmatic Programmer,
by Andrew Hunt and David Thomas, published by Addison-Wesley
Professional. The DRY principle is sometimes referred to as Single Source
of Truth (SSOT) or Single Point of Truth (SPOT) because it attempts to
store every single piece of unique information in a single place only. [Sdp14]
Test Readability
By reading the code, you should be able to find out what the code does easily.
A code that is not readable usually requires more time to read, maintain,
understand and can increase the chance to introduce bugs. Some
programmers use huge comments instead of writing simpler readable code. It
is much easier to name your variables, methods, classes correctly, instead of
relying on these comments. Also, as the time goes by, the comments are
rarely updated, and they can mislead the readers.
API Usability
As we mentioned above, the API is the specification of what you can do with
a software library. When we use the term usability together with the term
API, it means "How easy it is for you, as a user, to find out what the methods
do and how to use them?”. In the case of a Test Library - "How much time a
new user needs to create a new test?"
In the programming community, we sometimes use another term for the same
thing called syntactic sugar. It describes how easy it is to use or read some
expressions. It sweetens the programming languages for humans. The
programming statements become more concise and clearer.
NOTE
The term syntactic sugar was invented by Peter J. Landin in 1964. It
described the syntax of a programming language similar to ALGOL which
was defined semantically in terms of the applicative expressions of lambda
calculus, centered on lexically replacing λ with "where".
Extensibility
One of the hardest things to develop is to allow these generic frameworks to
be extensible and customizable. The whole point of creating a shared
library is to be used by multiple teams across the company. However, the
different teams work in a different context. So, the library code may not be
working out of the box for them. In order to use your library in all these
various scenarios, that you cannot (and shouldn't) consider while developing
it, the engineers should be able to customize some parts of it to fit their needs.
In the case of automated tests, imagine that you have a test suite testing a
shopping cart. The workflow of the test consists of multiple steps- choosing
the product, changing the quantity, adding more products, applying discount
coupons, filling billing info, providing payment info and so on. If a new
requirement comes - "The billing info should be prefilled for logged users.",
how easy would it be to change the existing tests? Did you write your tests in
a way that, if you add this new functionality, it will not affect the majority of
your existing tests?
You don't need to answer these questions yet. We will discuss them in much
more detail in the next chapters, and I will explain how to build such
solutions.
As you can see this high-quality automated test attribute is closely related to
the SOLID principles we already discussed.
Learning Curve
I also like to call this attribute "Easy Knowledge Transfer". The attribute
answers the question "How easy is it for someone to learn how to add new or
maintain the existing tests by himself?".
The learning curve is tightly coupled to the API usability, but at the same
time it means something a bit different. If a new member joins your team, is
he able to learn by himself how to use your test automation framework or he
needs to read the documentation if it exists? Or you have a mentoring
program where you need to teach these new members yourself every time
how to use your code? To the end of the book, I will show you how to
develop your test automation code in such a way that the new members will
be able to learn how to use your solution by themselves.
Summary
In this more theoretical first chapter, we went through some essential terms in
automated testing and high-quality program-ming. We discussed what is a
library, API and how they differ from frameworks. After that, we listed the
different types of tests and defined where our automated tests are situated in
this categorization. Next, I explained with a few examples the different
SOLID principles. At the end of the chapter, you learned what the design
patterns are, and which are the five high-quality test attributes.
In the next chapter, we will create our first automated tests and discuss what
are the most common reasons for them to fail. After that, I will present to you
a couple of best practices and design patterns to make them more stable.
Questions
1. Why is WebDriver not a test framework?
2. What is the difference between integration and system tests?
3. Can we use the term end-to-end test for a test that does not execute exactly
a user scenario?
4. Which principle is not followed if you have copy-pasted multiple times the
same code snippet?
Chapter 2. Optimizing and
Refactoring Legacy Flaky Tests
In this chapter we will create our first automated tests for validating an e-
commerce website. These tests will be a simulation of how similar tests look
as if they were written by someone who is just starting to use Selenium
WebDriver. I will use them to illustrate some common problems. One by one
we will address those problems in the current chapter and will continue to
improve them to the end of the book with various design patterns and best
practices.
The following topics will be covered in this chapter:
Writing the First Real-World Automated Test
Reasons for Failures
Refactoring Tests
Decorator Design Pattern for Fixing WebDriver Unstable Actions
The Test Independence-Isolation Principle
2. Next, we need to click on the 'View cart' button which will lead us to the
cart page.
3. If it's our birthday, we can apply the special discount coupon given to us
by the company.
4. Before proceeding with any operations, we need to make sure that the
loading indicator is not displayed.
6. After the cart is updated, our test needs to check whether the total price has
been changed correctly. If everything is OK, we click on the 'Proceed to
checkout' button.
7. We fill all required information and click on the 'Place order' button.
When the next page is loaded, we need to verify that the order was placed
successfully.
2. Next, we open the Orders tab and using the order number that we saved
on the completion of the last order, we click on this particular row's 'View'
button.
3. When the next page is open, we verify that the correct data is displayed.
First Automated Test Code
Why not now have a look at the code for our first automated test case?
private WebDriver driver;
private static String purchaseEmail;
private static String purchaseOrderNumber;
@BeforeMethod
public void testInit() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test(priority=1)
public void completePurchaseSuccessfully_whenNewClient() {
driver.navigate().to("http://demos.bellatrix.solutions/");
var addToCartFalcon9 = driver.findElement(By.cssSelector("[data-product_id*='28']"));
addToCartFalcon9.click();
Thread.sleep(5000);
var viewCartButton = driver.findElement(By.cssSelector("[class*='added_to_cart wc-forward']"));
viewCartButton.click();
Thread.sleep(5000);
var updateCart = driver.findElement(By.cssSelector("[value*='Update cart']"));
updateCart.click();
Thread.sleep(5000);
var totalSpan = driver.findElement(By.xpath("//*[@class='order-total']//span"));
Assert.assertEquals("114.00€", totalSpan.getText());
Thread.sleep(10000);
var receivedMessage =
driver.findElement(By.xpath("/html/body/div[1]/div/div/div/main/div/header/h1"));
Assert.assertEquals(receivedMessage.getText(), "Order received");
}
Why not see what we did here? We start the Chrome browser in the testInit
method, which means that it will be started for every test and closed after its
completion.
NOTE
To download the correct version of the selected browser's driver, we use a
library called WebDriverManager. I installed it through the Maven
artifact/dependency webdrivermanager. You can find all installed
packages in the project's or modules' pom.xml files.
NOTE
Maven is a build automation tool used primarily for Java projects. Maven,
created by Jason van Zyl, began as a sub-project of Apache Turbine in
2002. It is hosted by the Apache Software Foundation, where it was
formerly part of the Jakarta Project. Maven addresses two aspects of
building software: how software is built and its dependencies. An XML file
describes the software project being built, its dependencies on other
external modules and components, the build order, directories, and required
plug-ins. It comes with pre-defined targets for performing specific, well-
defined tasks such as compilation of code and its packaging. Maven
dynamically downloads Java libraries and Maven plug-ins from one or
more repositories, such as the Maven 2 Central Repository, and stores them
in a local cache.
NOTE
I will not go into much detail about element selectors and WebDriver
syntax since I expect that you already know the basics. You can check
Appendix 2 and Appendix 3 for better understanding of XPath and CSS
selectors.
Thread.sleep(5000);
var placeOrderButton = driver.findElement(By.id("place_order"));
placeOrderButton.click();
Thread.sleep(5000);
var receivedMessage = driver.findElement(By.xpath("//h1[text() = 'Order received']"));
Assert.assertEquals(receivedMessage.getText(), "Order received");
NOTE
One of the most visible enhancements in JDK 10 is the type inference of
local variables with initializers. Until Java 9, we had to mention the type of
the local variable explicitly and ensure it was compatible with the initializer
used to initialize it. We don't provide the data type of the variable. Instead,
we mark it as a var . Afterward, the compiler infers the type from the type
of the initializer present on the right-hand side. Note that this feature is
available only for local variables with the initializer.
NOTE
Originally, Java defined its major releases around introducing a large
feature, which tended to create delays, like those we all experienced with
Java 8 and 9. It also slowed language innovation while other languages
with tighter feedback cycles evolved. Java now has a release every six
months. Each third-year release is called an LTS (long-term support)
release.
Thread.sleep(5000);
var orders = driver.findElement(By.linkText("Orders"));
orders.click();
Thread.sleep(5000);
var viewButtons = driver.findElements(By.linkText("View"));
viewButtons.get(0).click();
Thread.sleep(5000);
NOTE
If you are feeling adventurous and want to destabilize our test suite in a
couple of other ways, you can experiment with the following changes:
- Delete the first test altogether, and only run the second test.
- Run the third test alone without running the first two.
- Decrease the milliseconds of the hard-coded pauses.
NOTE
If you download the source code and try to run the tests, there are a couple
of things you should be aware of. The order of test execution is important.
The tests should be executed in the following order:
completePurchaseSuccessfully_whenNewClient
completePurchaseSuccessfully_whenExistingClient
correctOrderDataDisplayed_whenNavigateToMyAccountOrderSection
The tests may fail because the hard-coded pauses were not enough. This is
the expected behavior showing that this is not the best practice.
Refactoring Tests
After we reviewed why the design of our initial tests is not so good, how
about seeing how we can improve our existing three tests. Here, we will start
with small refactoring- removing some of the pauses. To do it we will use a
WebDriver built-in functionality called “explicit wait” exposed through the
class WebDriverWait . Also, we can create reusable methods for some actions
that are occurring more than once in our code. One of the reasons why we
used pauses in our tests was that WebDriver throws NoSuchElementException ,
e.g. telling us that it was unable to find a particular element.
NOTE
To use ExpectedConditions in your Java tests you need to install a Maven
dependency called selenium-support.
NOTE
The method’s name waitAndFindElement is not the best one since it suggests
that we perform two operations, which breaks the Single Responsibility
principle. Later, we will rename it and move it to another more appropriate
class.
Our next step is to go to our tests and change all calls from driver.findElement to
driver.waitAndFindElement .
Here is a short example of how the elements are found after the changes are
applied.
var billingFirstName = waitAndFindElement(By.id("billing_first_name"));
billingFirstName.sendKeys("Anton");
var billingLastName = waitAndFindElement(By.id("billing_last_name"));
billingLastName.sendKeys("Angelov");
var billingCompany = waitAndFindElement(By.id("billing_company"));
billingCompany.sendKeys("Space Flowers");
var billingCountryWrapper = waitAndFindElement(By.id("select2-billing_country-container"));
billingCountryWrapper.click();
var billingCountryFilter = waitAndFindElement(By.className("select2-search__field"));
billingCountryFilter.sendKeys("Germany");
Definition:
It attaches additional responsibilities to an object dynamically. Decorators
provide a flexible alternative to subclassing for extending functionality.
● You can wrap a component with any number of decorators.
● You can change the behavior of its component by adding new
functionality before and/or after component method is called.
● The decorator class mirrors the type of component it decorates.
● Provides an alternative to subclassing for extending behavior.
Participants
The classes and objects participating in this pattern are:
● Component – Defines the interface for objects that can have
responsibilities added to them dynamically.
● Decorator – Implements the same interface (abstract class) as the
component they are going to decorate. It has a HAS-A relationship with the
object that it is extending, which means that the Component has an
instance variable that holds a reference to the latter.
● ConcreteComponent – The object that is going to be enhanced
dynamically. It inherits the Component .
● ConcreteDecorator – Decorators can enhance the state of the component.
They can add new methods. The new behavior is typically added before
and/or after an existing method in the component.
@Override
public By getBy() {
return by;
}
@Override
public String getText() {
return webElement.getText();
}
@Override
public Boolean isEnabled() {
return webElement.isEnabled();
}
@Override
public Boolean isDisplayed() {
return webElement.isDisplayed();
}
@Override
public void typeText(String text) {
Thread.sleep(500);
webElement.clear();
webElement.sendKeys(text);
}
@Override
public void click() {
waitToBeClickable(by);
webElement.click();
}
@Override
public String getAttribute(String attributeName) {
return webElement.getAttribute(attributeName);
}
NOTE
We will talk much more about test readability in Chapter 4 Test
Readability.
Let’s remember what the letter O stands for in the SOLID - Open/Closed
principle - "software entities (classes, modules, functions, etc.) should be
open for extension but closed for modification". Imagine that we want to add
logging after we perform a particular action in the WebCoreElement class. You
can open the class and add the logic there, but by doing so we will break the
principle. To implement it correctly without modifying the existing code we
will create a decorator for the element component.
public class ElementDecorator extends Element {
protected final Element element;
protected ElementDecorator(Element element) {
Element = element;
}
@Override
public By getBy() {
return element.getBy();
}
@Override
public String getText() {
return element.getText();
}
@Override
public Boolean isEnabled() {
return element.isEnabled();
}
@Override
public Boolean isDisplayed() {
return element.isDisplayed();
}
@Override
public void typeText(String text) {
element.typeText(text);
}
@Override
public void click() {
element.click();
}
@Override
public String getAttribute(String attributeName) {
return element.getAttribute(attributeName);
}
}
A few things to notice here. First, we derive from the abstract Element class.
Next, we hold an instance of the wrapped element, and for all methods and
properties we call the methods of the wrapped element.
Now let’s create our concrete decorator that will create the log entries for
each action.
public class LogElement extends ElementDecorator {
protected LogElement(Element element) {
super(element);
}
@Override
public By getBy() {
return element.getBy();
}
@Override
public String getText() {
System.out.print(String.format("Element Text = %s", Element.getText()));
return element.getText();
}
@Override
public Boolean isEnabled() {
System.out.print(String.format("Element Enabled = %b", Element.isEnabled()));
return element.isEnabled();
}
@Override
public Boolean isDisplayed() {
System.out.print(String.format("Element Displayed = %b", Element.isDisplayed()));
return element.isDisplayed();
}
@Override
public void typeText(String text) {
System.out.print(String.format("Type Text = = %s", text));
element.typeText(text);
}
@Override
public void click() {
System.out.print("Element Clicked");
element.click();
}
@Override
public String getAttribute(String attributeName) {
System.out.print("Element Clicked");
return element.getAttribute(attributeName);
}
}
As you can see, the code is similar to the one of the abstract base decorator.
However, the parent class is ElementDecorator , and in the overridden properties
and methods we added the logging. Also, each decorator is an element object.
This means that we can nest an unlimited number of decorators, where each
layer will add additional logic to the default element action. You will see in a
minute how we will use it in the tests, but before that we need to create a
similar decorator for the WebDriver interface.
Decorator Design Pattern Implementation for WebDriver
Again, we first need to create the abstract component that defines the default
actions.
public abstract class Driver {
public abstract void start(Browser browser);
public abstract void quit();
public abstract void goToUrl(String url);
public abstract Element findElement(By locator);
public abstract List<Element> findElements(By locator);
}
NOTE
We will need to change driver.navigate().to(…) to driver.goToUrl(…) , which will
improve the readability of our tests.
Next, we will create a class called WebCoreDriver that will derive from the
abstract Driver class. A significant improvement that we will develop is that
instead of calling the findElement method directly, we will first use the
WebDriverWait class to wait for the elements to exist, and then return them
using the findElement method. Another enhancement is the creation of an
WebDriver instance through a factory method called start , which will initialize
the correct browser driver based on the Browser enumeration argument.
public enum Browser {
CHROME,
FIREFOX,
EDGE,
OPERA,
SAFARI,
INTERNET_EXPLORER
}
NOTE
The Simple Factory design pattern is a factory class in its simplest form (in
comparison to Factory Method or Abstract Factory design patterns). In this
creational design pattern, we have a class that has a method that returns
different types of an object based on a given input. The creational design
patterns are design patterns that deal with object creation mechanisms,
trying to create objects in a manner suitable to the situation. The basic form
of object creation could result in design problems or added complexity to
the design. Creational design patterns solve this problem by somehow
controlling this object creation.
@Override
public void start(Browser browser) {
switch (browser)
{
case CHROME:
WebDriverManager.chromedriver().setup();
webDriver = new ChromeDriver();
break;
case FIREFOX:
WebDriverManager.firefoxdriver().setup();
webDriver = new FirefoxDriver();
break;
case EDGE:
WebDriverManager.edgedriver().setup();
webDriver = new EdgeDriver();
break;
case OPERA:
WebDriverManager.operadriver().setup();
webDriver = new OperaDriver();
break;
case SAFARI:
webDriver = new SafariDriver();
break;
case INTERNET_EXPLORER:
WebDriverManager.iedriver().setup();
webDriver = new InternetExplorerDriver();
break;
default:
throw new IllegalArgumentException(browser.name());
}
@Override
public void quit() {
webDriver.quit();
}
@Override
public void goToUrl(String url) {
webDriver.navigate().to(url);
}
@Override
public Element findElement(By locator) {
var nativeWebElement =
webDriverWait.until(ExpectedConditions.presenceOfElementLocated(locator));
Element element = new WebCoreElement(webDriver, nativeWebElement, locator);
return logElement;
}
@Override
public List<Element> findElements(By locator) {
List<WebElement> nativeWebElements =
webDriverWait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(locator));
var elements = new ArrayList<Element>();
for (WebElement nativeWebElement:nativeWebElements) {
Element element = new WebCoreElement(webDriver, nativeWebElement, locator);
Element logElement = new LogElement(element);
elements.add(logElement);
}
return elements;
}
}
Please, pay special attention to the findElement and findElements methods. First,
we use webDriverWait to wait for the element to exist. After that, we initialize
our WebCoreElement and then we pass it to its decorator LogElement . If you
create another decorator of the Element class later, you just need to add it here,
and its behavior will be added to the other two.
How about assuming that we want to add a similar logging capability for the
WebCoreDriver class, as we did with the WebCoreElement ? To do it right we need
to implement a DriverDecorator class.
public class DriverDecorator extends Driver {
protected final Driver driver;
@Override
public void start(Browser browser) {
driver.start(browser);
}
@Override
public void quit() {
driver.quit();
}
@Override
public void goToUrl(String url) {
driver.goToUrl(url);
}
@Override
public Element findElement(By locator) {
return driver.findElement(locator);
}
@Override
public List<Element> findElements(By locator) {
return driver.findElements(locator);
}
}
It inherits the abstract Driver and holds it as a protected variable, using
composition. After that again we override all actions and call the wrapped
driver instance. Lastly, let us create the concrete logging decorator for our
Driver class.
public class LoggingDriver extends DriverDecorator {
public LoggingDriver(Driver driver) {
super(driver);
}
@Override
public void start(Browser browser) {
System.out.print(String.format("start browser = %s", browser.name()));
driver.start(browser);
}
@Override
public void quit() {
System.out.print("close browser");
driver.quit();
}
@Override
public void goToUrl(String url) {
System.out.print(String.format("go to url = %s", url));
driver.goToUrl(url);
}
@Override
public Element findElement(By locator) {
System.out.print("find element");
return driver.findElement(locator);
}
@Override
public List<Element> findElements(By locator) {
System.out.print("find elements");
return driver.findElements(locator);
}
}
NOTE
The most popular feature introduced in Java 12 is the Switch Expressions.
New switch statements are not only more compact and readable. They also
remove the need for break statements. The code execution will not fall
through after the first match. Another notable difference is that we can
assign a switch statement directly to the variable. It was not possible
previously.
In Java 13 using yield , we can now effectively return values from a switch
expression.
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test(priority=1)
public void completePurchaseSuccessfully_whenNewClient() {
driver.goToUrl("http://demos.bellatrix.solutions/");
var addToCartFalcon9 = driver.findElement(By.cssSelector("[data-product_id*='28']"));
addToCartFalcon9.click();
var viewCartButton = driver.findElement(By.cssSelector("[class*='added_to_cart wc-forward']"));
viewCartButton.click();
// This pause will be removed when we introduce a logic for waiting for AJAX requests.
Thread.sleep(5000);
var placeOrderButton = driver.findElement(By.id("place_order"));
placeOrderButton.click();
Thread.sleep(10000);
var receivedMessage =
driver.findElement(By.xpath("/html/body/div[1]/div/div/div/main/div/header/h1"));
Assert.assertEquals(receivedMessage.getText(), "Order received");
}
In the testInit method, we create an instance of our LoggingDriver driver
decorator - the line below we invoke the start method to initialize a new
Chrome driver instance. When we call the findElement method in the tests, it
returns an instance of the LogElement class. Because of that, you have access
to the typeText method. When you call the click method, the decorator waits
for the element to be clickable before performing the actual click. And all of
that achieved without changing the code of the tests at all. How cool is that?
Now that we have looked at the Decorator design pattern, would you like to
review another essential principle?
NOTE
UUID (Universally Unique Identifier), also known as GUID (Globally
Unique Identifier), represents a 128-bit long value that is unique for all
practical purposes. The standard representation of the UUID uses hex
digits: 123e4567-e89b-12d3-a456-556642440000
The randomUUID() method creates a version 4 UUID which is the most
convenient way of creating a UUID. The UUID v4 implementation uses
random numbers as the source. The Java implementation is SecureRandom
– which uses an unpredictable value as the seed to generate random
numbers to reduce collisions.
The newly developed method solves the problem with the first test. For the
next two, instead of getting data from the other tests, they can use the data
that we previously created on the test environment. Alternatively, an even
better approach would be to generate the data (orders and users) using
internal API services before each test is run.
Summary
In this chapter, we created our first automated real-world tests. At first, we
didn't apply any complex design patterns, but used directly a simple
WebDriver syntax. However, as you saw, the tests weren't very stable, nor
readable. Because of that, we refactored them by using the WebDriverWait
class. After that, we investigated how you can reuse the improvements in
future tests by incorporating them by implementing the Decorator design
pattern. We even enhanced the test readability and API usability through the
usage of the Simple Factory design pattern. Also, at the end of the chapter,
we discussed briefly the Test Independence-Isolation principle and how
you can apply it.
After we stabilized our tests, in the next chapter we will further optimize
them so that they can be faster and less brittle. First, we will create an
optimization plan then we will refactor the login mechanism and remove the
hard-coded pauses. After that, we will investigate the Observer design
pattern and how it can help us to optimize the browser initialization.
Questions
1. Can you describe the common problems in automated tests using the simple
WebDriver syntax?
2. How would you use the WebDriverWait class to wait for an element to exist
on a web page?
3. How would you wait for an element to be clickable?
4. Can you describe the participants in the Decorator design pattern?
5. Can you explain what is the difference between implicit and explicit wait in
WebDriver?
6. Can you list the benefits of the Test Independence-Isolation principle?
Chapter 3. Strategies for Speeding-
up the Tests
In the previous chapter, we have created a test suite validating our e-
commerce website. At first, the tests weren't very stable, but we managed to
stabilize them. However, they can be further optimized, so that they can even
be faster and less flaky. First, we will look at how we can instrument our
code to find and define various points of improvement. Next, we will
investigate how we can speed-up the tests by refactoring the login method.
Next, we will deal with the outstanding hard-coded pauses that were needed
for handling the asynchronous loading of various forms. After that, we will
further enhance our code by creating a better solution for dealing with
browser initialization. We will look into how to make our code parallelizable
since this is the most powerful way to execute our tests faster. Lastly, we will
talk about how the so-called Black Hole Proxy approach can be used to
optimize our tests.
In the chapter after the current, we will make the test library easier to use and
maintain. We will strive for the tests to be easier to read and understand.
An essential part of decreasing the effort for developing new tests is to build
the test library in a way, so that it is easy to add new features - without
breaking existing logic and tests- a.k.a. Extensibility.
The following topics will be covered in this chapter:
Instrumenting the Test Code to Find Possible Points for Optimization
Optimize Authentication
How to Wait for AJAX in tests?
Optimize Browser Initialization- Observer Design Pattern
Isolated Browser Initialization for Each Test
Running Tests in Parallel
Black Hole Proxy Approach
@BeforeMethod
public void testInit() {
stopwatch = Stopwatch.createStarted();
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
System.out.printf("end browser init: %d", stopwatch.elapsed(TimeUnit.SECONDS));
}
@AfterMethod
public void testCleanup() {
driver.quit();
System.out.printf("afterTest: %d", stopwatch.elapsed(TimeUnit.SECONDS));
stopwatch.stop();
}
@Test(priority=1)
public void completePurchaseSuccessfully_whenNewClient() {
System.out.printf("start completePurchaseSuccessfully_whenNewClient: %d",
stopwatch.elapsed(TimeUnit.SECONDS));
// test's code
System.out.printf("end completePurchaseSuccessfully_whenNewClient: %d",
stopwatch.elapsed(TimeUnit.SECONDS));
}
@Test(priority=2)
public void completePurchaseSuccessfully_whenExistingClient() {
System.out.printf("start completePurchaseSuccessfully_whenExistingClient: %d",
stopwatch.elapsed(TimeUnit.SECONDS));
// test's code
System.out.printf("end completePurchaseSuccessfully_whenExistingClient: %d",
stopwatch.elapsed(TimeUnit.SECONDS));
}
We use the special Java class called Stopwatch to measure the time between
different statement executions. Through the line System.out.printf("afterTest: %d",
stopwatch.elapsed(TimeUnit.SECONDS)); we print to the console how many seconds
have passed since the start.
After running the tests a few times, we will have a few key measurements.
The time needed to initialize a new instance of a browser was ~2 seconds.
The login to the website took ~5 seconds. Also, if we sum up the seconds in
the hard-coded pauses, we get 12 additional seconds. Our first test was
completed in ~40 seconds. This means that if we find a way to optimize the
previous points, we can save from 10 to 15 seconds, or our tests can get ~25-
40% faster.
Optimize Authentication
In most websites, the login is one of the most critical areas. Most of our tests
will need to log in first to be able to make their verifications. However, 99%
of them don't test the login functionality but rather something different. This
means that we can have a test suite only for validating that our login is
working well and integrating with all others, but beyond these tests, all others
don't need to load this page every time, type the username and password, and
so on. A common optimization is to authenticate on the website through
automatic creation of the authentication cookie. Usually, we call an internal
web API to generate the value of the cookie which executes the logic behind
the login itself.
NOTE
There are cases where you cannot merely generate the authentication
cookie. You should examine how the authentication process works and try
to accomplish it by using API calls instead of the UI. You may need to find
a way to bypass the captcha if there is one.
NOTE
Don't worry if you don't know how to implement this yourself. Just mention
to your developers what you need, and they will be happy to help you to
optimize your tests.
If we implement such logic for our tests, this means that we will save ~2
seconds for each test. For 1000 tests this will be equal to over 30 saved
minutes.
// apply coupon
var couponCodeTextField = driver.findElement(By.id("coupon_code"));
couponCodeTextField.typeText("happybirthday");
var applyCouponButton = driver.findElement(By.cssSelector("[value*='Apply coupon']"));
applyCouponButton.click();
Thread.sleep(2000);
var messageAlert = driver.findElement(By.cssSelector("[class*='woocommerce-message']"));
Assert.assertEquals(messageAlert.getText() , "Coupon code applied successfully.");
This is what appears on the screen when we apply the code:
Depending on the environment where our tests are being executed this
loading can happen for a few milliseconds up to a couple of seconds.
However, since we used a hard-coded pause, we will always wait for this
time. We need to improve our test library to handle these AJAX requests by
detecting and waiting for them.
NOTE
The code is coupled to the jQuery library, and because of that, it won't work
on websites that don't use it. Also, it must be used after the load event.
Otherwise, it will throw an exception if jQuery is not defined- "Uncaught
TypeError: Cannot read property 'active' of undefined."
In WebDriver, the until method in the Wait class is simply a loop that
executes the contents of the code block passed to it until the code returns a
true value. We ask JavaScript to return jQuery.active count. In the case of the
waitForAjax method, the exit loop condition is reached when there are 0 active
AJAX requests. If the conditional returns true , all the AJAX requests have
finished, and we are ready to move to the next step.
Now, we can simply use the waitForAjax method anywhere we need our tests
to wait for async requests. We will be replacing the hard-coded sleep method
added earlier, as shown here:
// increase product quantity
var quantityBox = driver.findElement(By.cssSelector("[class*='input-text qty text']"));
quantityBox.typeText("2");
driver.waitForAjax();
var updateCart = driver.findElement(By.cssSelector("[value*='Update cart']"));
updateCart.click();
driver.waitForAjax();
var totalSpan = driver.findElement(By.xpath("//*[@class='order-total']//span"));
Assert.assertEquals(totalSpan.getText() , "114.00€");
// apply coupon
var couponCodeTextField = driver.findElement(By.id("coupon_code"));
couponCodeTextField.typeText("happybirthday");
var applyCouponButton = driver.findElement(By.cssSelector("[value*='Apply coupon']"));
applyCouponButton.click();
driver.waitForAjax();
var messageAlert = driver.findElement(By.cssSelector("[class*='woocommerce-message']"));
Assert.assertEquals(messageAlert.getText() , "Coupon code applied successfully.");
We have conquered the AJAX menace. It's time to move on to another way
we can improve the test execution time - optimizing the browser
initialization.
There is one more place where we need a more special wait logic to remove
the pauses. It is just after we click the ‘Proceed to checkout’ button. There
we need to wait for the entire page to load. Since there aren’t asynchronous
requests, the WaitForAjax method won’t work in this case. Therefore, we can
add one more helper function to our WebCoreDriver decorator. Here is the code:
@Override
public void waitUntilPageLoadsCompletely() {
var javascriptExecutor = (JavascriptExecutor) webDriver;
webDriverWait.until(d -> javascriptExecutor.executeScript("return
document.readyState").toString().equals("complete"));
}
Via JavaScript, we wait for the ready state of the document to be “complete” .
This is how we use it our completePurchaseSuccessfully_whenNewClient test:
var proceedToCheckout = driver.findElement(By.cssSelector("[class*='checkout-button button alt wc-
forward']"));
proceedToCheckout.click();
driver.waitUntilPageLoadsCompletely();
@AfterMethod
public void testCleanup() {
driver.quit();
}
// Tests code starts here
Right now, we start and close the browser for each test. A smarter solution
might be to reuse the browser - start it at the beginning of the whole test suite,
and close it after all tests have finished their execution. This will bring us
considerable execution time improve-ment, but what if we still need to restart
the browser before some of the tests? Because of that, we will need a smarter
solution, where you will be able to configure this behavior per test class or
test method. For the job, we will implement the Observer design pattern.
public ExecutionSubject() {
testBehaviorObservers = new ArrayList<>();
}
@Override
public void attach(TestBehaviorObserver observer) {
testBehaviorObservers.add(observer);
}
@Override
public void detach(TestBehaviorObserver observer) {
testBehaviorObservers.remove(observer);
}
@Override
public void preTestInit(ITestResult result, Method memberInfo) {
for (var currentObserver: testBehaviorObservers) {
currentObserver.preTestInit(result, memberInfo);
}
}
@Override
public void postTestInit(ITestResult result, Method memberInfo) {
for (var currentObserver: testBehaviorObservers) {
currentObserver.postTestInit(result, memberInfo);
}
}
@Override
public void preTestCleanup(ITestResult result, Method memberInfo) {
for (var currentObserver: testBehaviorObservers) {
currentObserver.preTestCleanup(result, memberInfo);
}
}
@Override
public void postTestCleanup(ITestResult result, Method memberInfo) {
for (var currentObserver: testBehaviorObservers) {
currentObserver.postTestCleanup(result, memberInfo);
}
}
@Override
public void testInstantiated(Method memberInfo) {
for (var currentObserver: testBehaviorObservers) {
currentObserver.testInstantiated(memberInfo);
}
}
}
The specific subject knows nothing about the implementations of the
observers. It is working with a list of TestBehaviorObserver objects. The attach
and detach methods add and remove observers to/from the collection. In this
classic implementation of the observer design pattern, the observers are
responsible to associate themselves with the subject class.
Not all the observers need to implement all the notification methods. In order
to support this requirement, we are going to add a base class that is going to
implement the TestBehaviorObserver interface.
public class BaseTestBehaviorObserver implements TestBehaviorObserver {
public BaseTestBehaviorObserver(TestExecutionSubject testExecutionSubject) {
testExecutionSubject.attach(this);
}
@Override
public void preTestInit(ITestResult testResult, Method memberInfo) {
}
@Override
public void postTestInit(ITestResult testResult, Method memberInfo) {
}
@Override
public void preTestCleanup(ITestResult testResult, Method memberInfo) {
}
@Override
public void postTestCleanup(ITestResult testResult, Method memberInfo) {
}
@Override
public void testInstantiated(Method memberInfo) {
}
}
As all notification methods are empty, the child class only needs to override
the necessary ones. Also, the base class constructor requires an
TestExecutionSubject parameter to be able to associate the current observer to the
subject.
@Test(priority=2)
@ExecutionBrowser(browser = Browser.FIREFOX, browserBehavior =
BrowserBehavior.REUSE_IF_STARTED)
public void completePurchaseSuccessfully_whenExistingClient() {
// the test's code
}
// the rest of the code
}
In the example, the new annotation is used to configure the tests to use the
Chrome browser for all tests in the class. However, the test method attribute
is going to override the value at the method level. So, before the
completePurchaseSuccessfully_whenExistingClient test starts, the browser initialized is
going to be Firefox instead of Chrome.
@Target( { ElementType.TYPE, ElementType.METHOD } )
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecutionBrowser {
BrowserBehavior browserBehavior() default BrowserBehavior.RESTART_EVERY_TIME;
Browser browser() default Browser.CHROME;
}
There is nothing special about the ExecutionBrowser , it only holds two getters.
The first one holds the BrowserBehavior enum which controls the behavior of
the browser.
public enum BrowserBehavior {
NOT_SET,
REUSE_IF_STARTED,
RESTART_EVERY_TIME,
RESTART_ON_FAIL,
}
The second getter returns the browser to be started through the Browser enum.
The ExecutionBrowser annotation is configured to be available on test class and
method level through the Target annotation.
The browser controlling and the extraction of the attributes’ information is
happening in the concrete observer implementation called
BrowserLaunchTestBehaviorObserver . It inherits our base observer
BaseTestBehaviorObserver class.
public class BrowserLaunchTestBehaviorObserver extends BaseTestBehaviorObserver {
private final Driver driver;
private BrowserConfiguration currentBrowserConfiguration;
private BrowserConfiguration previousBrowserConfiguration;
@Override
public void preTestInit(ITestResult testResult, Method memberInfo) {
currentBrowserConfiguration = getBrowserConfiguration(memberInfo);
if (shouldRestartBrowser) {
restartBrowser();
}
previousBrowserConfiguration = currentBrowserConfiguration;
}
@Override
public void postTestCleanup(ITestResult testResult, Method memberInfo) {
if (currentBrowserConfiguration.getBrowserBehavior() ==
BrowserBehavior.RESTART_ON_FAIL && testResult.getStatus() ==
ITestResult.FAILURE) {
restartBrowser();
}
}
Boolean shouldRestartBrowser =
browserConfiguration.getBrowserBehavior() ==
BrowserBehavior.RESTART_EVERY_TIME || browserConfiguration.getBrowser() ==
Browser.NOT_SET;
return shouldRestartBrowser;
}
Boolean shouldRestartBrowser =
browserConfiguration.getBrowserBehavior() ==
BrowserBehavior.RESTART_EVERY_TIME || browserConfiguration.getBrowser() ==
Browser.NOT_SET;
return shouldRestartBrowser;
}
If the method is called for the first time the previousBrowserConfiguration variable
is still null , so the method will return true . For the rest of the flow, we check
the values of the BrowserBehavior enum of the current browser configuration.
Also, in the postTestCleanup method, we check whether the test has failed or
not. If it has failed, we will restart the browser, since it may have been left in
an inconsistent state.
@Override
public void postTestCleanup(ITestResult testResult, Method memberInfo) {
if (currentBrowserConfiguration.getBrowserBehavior() ==
BrowserBehavior.RESTART_ON_FAIL && testResult.getStatus() ==
ITestResult.FAILURE) {
restartBrowser();
}
}
NOTE
Before Java 8, developers had to carefully validate values they referred to
because of the possibility of throwing the NullPointerException (NPE). All
these checks demanded a pretty annoying and error-prone boilerplate code.
Java 8 Optional<T> class can help handle situations where there is a
possibility of getting the NPE. It works as a container for type T. It can
return a value of this object if this value is not null. When the value inside
this container is null, it allows some predefined actions instead of throwing
NPE.
Without Optional<T>:
return result;
}
With Optional<T>:
private BrowserConfiguration getBrowserConfiguration(Method memberInfo) {
return Optional.ofNullable(getExecutionBrowserMethodLevel(memberInfo)).
orElse(getExecutionBrowserClassLevel(memberInfo.getDeclaringClass()));
}
The last part of the puzzle is to combine all these classes. This happens in the
BaseTest class.
public class BaseTest {
private static final TestExecutionSubject executionSubject;
private static final Driver driver;
private ITestResult result;
static {
executionSubject = new ExecutionSubject();
driver = new LoggingDriver(new WebCoreDriver());
new BrowserLaunchTestBehaviorObserver(executionSubject, driver);
}
@AfterSuite
public void afterSuite() {
if (driver != null) {
driver.quit();
}
}
@BeforeMethod
public void beforeMethod(ITestResult result) {
setTestResult(result);
var testClass = this.getClass();
var methodInfo = testClass.getMethod(getTestResult().getMethod().getMethodName());
executionSubject.preTestInit(getTestResult(), methodInfo);
testInit();
executionSubject.postTestInit(getTestResult(), methodInfo);
}
@AfterMethod
public void afterMethod() {
var testClass = this.getClass();
var methodInfo = testClass.getMethod(getTestResult().getMethod().getMethodName());
executionSubject.preTestCleanup(getTestResult(), methodInfo);
testCleanup();
executionSubject.postTestCleanup(getTestResult(), methodInfo);
}
NOTE
Constant names use CONSTANT_CASE: all uppercase letters, with each
word separated from the next by a single underscore. But what is a
constant, exactly? I will quote the Google Java Style Guide. "Constants are
static final fields whose contents are deeply immutable and whose methods
have no detectable side effects. This includes primitives, Strings,
immutable types, and immutable collections of immutable types. If any of
the instance's observable state can change, it is not a constant." I checked
many online discussions, and I would add that a final field to be a constant
should be marked as public. In other cases, most probably, your intent is
only to make the private field immutable rather than communicating to the
users of your API that your class provides constants. Thus many developers
don't count private final static/non-static fields as constants, me including.
Through the book, I treat these fields as standard private fields, no matter
that they are marked as final, so they are named as regular private fields.
There are various coding styles, and there isn't one right approach. Feel free
to use another convention if you like.
You will read more about the Google Java Style Guide and other coding
standards in Chapter 4. Test Readability.
Before the explanations of the code's logic, I want to make a note about the
naming of constants. As you can see, I marked the executionSubject variable as
private static final . However, it still follows the naming conventions for private
variables because I don't consider it a constant.
If the test classes need to add its BeforeMethod or AfterMethod logic, they will
now have to override the testInit and testCleanup methods, instead of using the
BeforeMethod and AfterMethod annotations.
There are three important methods that are invoked in the base beforeMethod
method. First, we invoke the preTestInit method of the current subject class
which has the responsibility to invoke preTestInit for each observer. After that,
the testInit method executes or its overridden version. Finally, all observers'
postTestInit methods are invoked through the current subject again. The same
flow is valid for the cleanup methods.
driver = new LoggingDriver(new WebCoreDriver());
new BrowserLaunchTestBehaviorObserver(executionSubject, driver);
In the constructor are created the instances of all desired observers through
passing them the current subject as a parameter. There we moved the creation
of our WebCoreDriver decorator which we pass as an argument to the
BrowserLaunchTestBehaviorObserver .
After the base constructor is executed, the ITestResult variable is populated
from the TestNG execution engine. It is used to retrieve the currently
executed test’s Method meta information.
@AfterSuite
public void afterSuite() {
if (DRIVER != null) {
DRIVER.quit();
}
}
Since all our tests may reuse the current browser, we need to make sure that
at the end of the test run we will close the browser. We do that in the afterSuite
method, which is executed after all tests.
NOTE
Most test frameworks contain similar methods to execute custom logic at
specific points of the test execution. Another popular test framework for
Java is JUnit. Its representation of TestNG's @BeforeMethod and @AfterMethod
are @BeforeEach and @AfterEach . If you want to execute code after all tests in
the class, you need to annotate the method with @AfterAll . In TestNG, it is
called @AfterClass .
NOTE
Through the book, I follow the naming convention to name the classes
using the design pattern's name that they implement. To ease the reading, I
included the prefix 'Base' for some core classes. However, in the real
world, sometimes, it is smarter to provide context-specific shorter names.
For example, in the open-sourced test automation framework I developed
called BELLATRIX, I named the BaseTest for the web - WebTest , in the
context of desktop - DesktopTest . Instead of using the long BehaviorObserver
suffix, I use the word 'Plugin'. BrowserLaunchTestBehaviorObserver is called there
BrowserLaunchPlugin . However, as we mentioned, sometimes it is useful to use
the design pattern's names because they are universal and used as a shared
dictionary between software developers. In the case of the framework, I
decided that it is more important for the code to be concise and easier to
read.
static {
executionSubject = new ThreadLocal<>();
executionSubject.set(new ExecutionSubject());
driver = new ThreadLocal<>();
driver.set(new LoggingDriver(new WebCoreDriver()));
new BrowserLaunchTestBehaviorObserver(executionSubject.get(), driver.get());
}
@AfterSuite
public void afterSuite() {
if (driver.get() != null) {
driver.get().quit();
}
}
@BeforeMethod
public void beforeMethod(ITestResult result) {
setTestResult(result);
var testClass = this.getClass();
var methodInfo = testClass.getMethod(getTestResult().getMethod().getMethodName());
executionSubject.get().preTestInit(getTestResult(), methodInfo);
testInit();
executionSubject.get().postTestInit(getTestResult(), methodInfo);
}
@AfterMethod
public void afterMethod() {
var testClass = this.getClass();
var methodInfo = testClass.getMethod(getTestResult().getMethod().getMethodName());
executionSubject.get().preTestCleanup(getTestResult(), methodInfo);
testCleanup();
executionSubject.get().postTestCleanup(getTestResult(), methodInfo);
}
NOTE
For Java, we will use a lightweight HTTP(S) proxy server called
BrowserMob. You need to install the BrowserMob through the Maven
dependency called browsermob-core. The project is open-source, and you
can find the full source code on GitHub - https://bit.ly/38I4vcQ.
BrowserMob Proxy allows you to manipulate HTTP requests and
responses, capture HTTP content, and export performance data as a HAR
file. BMP works well as a standalone proxy server.
@BeforeMethod
public void testInit() {
WebDriverManager.chromedriver().setup();
proxyServer = new BrowserMobProxyServer();
proxyServer.start();
proxyServer.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
CaptureType.RESPONSE_CONTENT);
proxyServer.newHar();
String proxyDetails = "127.0.0.1:" + proxyServer.getPort();
final Proxy proxyConfig = new Proxy().
setHttpProxy(proxyDetails).
setSslProxy(proxyDetails);
@AfterMethod
public void testCleanup() {
driver.quit();
proxyServer.abort();
}
@Test
public void completePurchaseSuccessfully_whenNewClient() {
driver.navigate().to("http://demos.bellatrix.solutions/");
}
}
The preceding code performs the following actions:
● Before each test in the class, we start the proxy server.
● It creates an instance of the ChromeOptions class
● It configures the HTTP proxy to point to a non-existing proxy on 127.0.0.1
with the port of the created proxy server
● We connect the proxy server with the browser that will be created through
the ChromeOptions setProxy method
● We configure the browser to accept insecure certificates because of the
proxy through the method setAcceptInsecureCerts
In the example, I configured the proxy server to block all image requests
passing a special Regex expression to the blackListrequests method. After that,
when you load a new web page through the driver's created instance, the
proxy will block all of these requests, and we won't spend time waiting for
them.
NOTE
A regular expression is a sequence of characters that forms a search pattern.
When you search for data in a text, you can use this search pattern to
describe what you are searching for. Java does not have a built-in Regular
Expression class, but we can import the java.util.regex package to work with
regular expressions.
Through the Black Hole Proxy pattern, our tests can speed up significantly
by not waiting for 3rd-party services loading. Furthermore, the tests will be
more hermetically sealed by blocking the third-party requests, reducing the
external dependencies that often cause test failures.
NOTE
The code can be further integrated in our browser initialization solution
through attributes. I will leave this to you as an exercise.
Summary
In this chapter, we investigated how we can speed up our test suite in many
ways. First, we started by instrumenting our code so that we can list all
potential optimization points. Next, I showed you how you could improve the
login processes by using API calls. Next, we talked about how to handle
async requests and thus eliminating all hard-coded pauses. Next, we created a
comprehensive solution for browser initialization using the Observer design
pattern that allowed us to reuse the existing browser, saving a couple of
seconds for each test. After that, we looked into how to make our code
parallelizable. Lastly, we discussed the benefits of the Black Hole Proxy
approach by blocking irrelevant for the test 3rd-party services.
In the next chapter, we will work towards making our tests easier to read and
maintain. We are going to introduce some new design patterns and discuss
how to establish high-quality code standards.
Questions
1. How would you decide which part of your code should be optimized?
2. What is the typical approach for speeding up the login process?
3. How can you use WebDriver API to handle asynchronous forms?
4. Can you list the main participants in the Observer design pattern?
5. Can you create the Observer design pattern UML diagram?
6. Which WebDriver method can be used to make sure that each test is not
depending on the previous one if we reuse the browser?
7. What are the advantages of using the Black Hole Proxy approach?
Chapter 4. Test Readability
At this point our tests are passing, we have improved their speed, removed
the need of hard-coded waits, the next step is to make the test library easier to
use and maintain. Also, we will strive for the tests to be easier to read and
understand. An important part of decreasing the effort for developing new
tests is to build the test library in a way, so it is easy to add new features
without breaking existing logic and tests - a.k.a. Extensibility.
First, we will discuss how to hide nitty-gritty low-level WebDriver API
details in the so-called page objects. Next, we will step on this foundation and
make the tests more readable through a few improvements. In the second part
of the chapter, we will talk about coding standards - naming the action and
assertion methods right, as well as variables and parameters. At the end of the
section, we will discuss various tools that can help us enforce all coding
guidelines that we agreed to use.
The following topics will be covered in this chapter:
Page Object Model Design Pattern
Handling Common Page Elements and Actions
Page Object Model Design Pattern - Sections
High Quality Code - Use Meaningful Names
Naming the Methods, The Right Way
Follow Coding Standards – Tools
@FindBy(id = "sb_form_q")
public WebElement searchBox;
@FindBy(xpath = "//label[@for='sb_form_go']")
public WebElement goButton;
@FindBy(css = "b_tween")
public WebElement resultsCountDiv;
NOTE
To be able to use the built-in Page Object Pattern Factory feature of the
framework, you need to install an additional Maven dependency –
selenium-support
NOTE
It is untrue that this way of creating page object models is deprecated. Even
in the .NET world, many people think it was obsolete, but this isn't true
either. It was just moved to another GitHub project and NuGet package. I
don't recommend using it because there are ways to control more precisely
how the elements are found and make the tests more readable. Even Simon
Stewart, Selenium project lead and creator of the PageFactory , recommends
against using the built-in page PageFactory because of the lack of Java
annotations' extensibility. You can listen to his statement at the 2017
Selenium Conference here https://bit.ly/3tskxiZ, talking about PageFactory
from 25:19 to 29:30.
driver.waitForAjax();
var receivedMessage =
driver.findElement(By.xpath("/html/body/div[1]/div/div/div/main/div/header/h1"));
Assert.assertEquals(receivedMessage.getText(), "Order received");
}
In order to use the Page Object Model design pattern, we need to refactor
the code and create 5 different page objects for each specific page. We are
going to develop the first two pages together, but for the next ones, you can
download the source code and exercise yourself.
The first page object will be a representation of the main page of our website.
We will move all the existing code related to this page to a page object class
called MainPage .
public class MainPage {
private final Driver driver;
private final String url = "http://demos.bellatrix.solutions/";
NOTE
Please note that I didn’t completely follow the naming conventions for
getters. If I did, I had to name the elements’ getters – getTotalSpan and
getMessageAlert . I believe this leads to improved readability and API usability
of the library. We will talk much more about enhancing API Usability in
Chapter 6.
NOTE
Domain-specific languages (DSLs) are languages developed to solve
problems in a specific domain, which distinguishes them from general
purpose languages (GPLs). One characteristic of DSLs is that they support
a restricted set of concepts, limited to the domain.
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test(priority=1)
public void completePurchaseSuccessfully_whenNewClient() {
mainPage.addRocketToShoppingCart();
cartPage.applyCoupon("happybirthday");
cartPage.increaseProductQuantity(2);
Assert.assertEquals("114.00€", cartPage.getTotal());
cartPage.clickProceedToCheckout();
Common sections with the home page are the top section - logo and search
input, the navigation, the cart info, and the footer. The unique part here is
positioned below the cart title, that is one new section that is common only to
this and next cart pages - the breadcrumb.
After we analyzed the sections of our page, we found that some of them are
shared between many pages. We can allow the users to use search on the
pages or click on the menu with the current design of our page objects, this
means that we will have a duplicated logic between our pages, which is in
almost all cases a bad practice since with each copied code the
maintainability costs rise. Moreover, the pages will do more than one thing
which means that we won't follow the Single Responsibility Principle which
we discussed in chapter one.
NOTE
DRY- Do Not Repeat Yourself Principle - A well-implemented and Page
Objects library has one and only way to accomplish any action. This
prevents duplicate implementation of the same searchItem or fillBillingInfo
methods.
NOTE
KISS refers to the famous acronym "Keep it Simple, Stupid". It can also
mean "Keep it Short and Simple" or "Keep it Simple and Straightforward".
It is tightly connected to the DRY principle we discussed earlier. As
programmers, we are sometimes tempted to think of very smart and
complex solutions upfront, thinking about the future that may never come.
This in many situations leads to highly complex, hard to maintain and
understand code. So, the principle reminds us to start with the shortest and
simplest solution first and then, if it is not working for us, we can try to
enhance it.
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
The cart page is twice as short compared to before. However, our simplest
solution has a drawback. We moved the breadcrumb logic to the base class.
Although even though the breadcrumb is displayed only on the cart pages,
now you can access it from the MainPage as well. This could mislead someone
to use it on a page where the breadcrumb does not exist.
In the next part of the chapter, we will examine a much better approach to
solve such problems.
Search
Main menu
Cart info
Breadcrumb
Having such groups, we can create four separate page objects that will keep
each group’s elements and operations. Since they are not entire pages, I will
name them with the suffix - Section instead of Page.
public class SearchSection {
private final Driver driver;
I did almost the same refactoring to the CartPage , except for the added
BreadcrumbSection .
More or less, this version is much closer to the desired outcome. However, if
you take a closer look, you will notice that there still is some duplicated code
between the pages. The first three sections should be present on all pages
which means that with each addition of a new page, I will have to copy these
three properties and their initialization. In order to fix this, we will bring back
the base class that we created earlier.
Page Sections Usage in Page Objects - Version Two
As mentioned, we can move the shared sections and their initialization to a
base class.
public abstract class BaseEShopPage {
protected final Driver driver;
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
mainPage.open();
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void openBlogPage() {
mainPage.mainMenuSection().openBlogPage();
// verify page title
}
@Test
public void searchForItem() {
mainPage.searchSection().searchForItem("Falcon 9");
// add the item to cart
}
@Test
public void openCart() {
mainPage.cartInfoSection().openCart();
// verify items in the cart
}
}
We need to use the new section getters in order to use their publicly exposed
logic. This further improves the readability since you can assume where on
the page the action will be performed.
Naming Classes
Class names are written in UpperCamelCase. Class names are typically nouns
or noun phrases.
Do not use Hungarian notation. Do not use underscores, hyphens, or any
other non-alphanumeric characters. Avoid using names that conflict with
keywords such as new, class and so on.
NOTE
Hungarian notation is a naming convention in programming, in which the
name of a variable or method indicates its intention and type. For example,
bBusy for Boolean or iSize for Integer .
NOTE
The suggested “case” may vary based on which programming language you
use. The examples in the book are for Java.
NOTE
There is one provided by Oracle called “Code Conventions for the Java
Programming Language”. You can find it here - https://bit.ly/3bRkpDL
However, when you open the page you will see the notice: “This page is
not being actively maintained. Links within the documentation may not
work and the information itself may no longer be valid. The last revision to
this document was made on April 20, 1999” I believe most of the standards
are still valid but still you can refer to newer documents.
Another alternative that I use often is the “Google Java Style Guide” -
https://bit.ly/3bMyemP
A third option is the “Spring Framework Code Style” -
https://bit.ly/38DElYH
NOTE
Coding standards are a set of guidelines, best practices, programming
styles, and conventions that developers adhere to when writing the source
code for a project.
Enforcing Coding Standards Using EditorConfig
EditorConfig helps programmers to define shared coding styles between
different editors. The project consists of a file format for determining coding
styles that will be followed, and a collection of text editor plugins that enable
IDEs to read the file format. After that, warning messages are displayed in
case of non-adherence.
In the Project view, right-click a source directory containing the files whose
code style you want to define and choose New | EditorConfig from the
context menu. Select EditorConfig standard.
You can override the global settings through a .editorconfig file placed on
project level.
When some of the users of your library don't follow some of the defined
rules, the editor displays warnings.
For Java projects, you can change the project's settings so that all warnings
are treated as errors, and by doing so all users will be forced to fix the errors
immediately instead of ignoring the warnings for years.
Another popular solution for applying coding standards for IntelliJ is the
CheckStyle-IDEA plug-in.
Questions
1. What are the primary advantages of the Page Object Model design
pattern?
2. What does a page object class consist of?
3. How do you use page objects in the tests?
4. Can you define the common sections of your company's website?
5. Can you explain what composition means in the context of computer
programming?
6. Why is it always much better to use sections through composition
instead of inheritance?
7. What are the coding standards?
8. Why is it important to enforce them in an automated way?
Chapter 5. Enhancing the Test
Maintainability and Reusability
We will see how to reuse more code among page objects through the
Template Method design pattern. Also, we will see a 3rd type of page
object, where the assertions and elements will be used as getter methods
instead of coming from base classes. This will demonstrate the benefits of the
composition over inheritance principle. However, we will have a separate
section discussing how we can reuse both of them through generics and
reflection.
In the second part of the chapter, we will discuss how to reuse common test
workflows through the Facade design pattern. We will talk about how the
Template Method design pattern will help us test different versions of the
same web page (new and old) with maximum code reuse.
The following topics will be covered in this chapter:
Navigatable Page Objects - Template Method Design Pattern
Composition Principle for Assertions and Elements of Page Objects
Reuse Elements and Assertions via Base Pages
Reuse Test Workflows - Facade Design Pattern
Test Different Page Versions - Facades Combined with Template
Method Design Pattern
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
// web elements and methods
}
Participants
The objects participating in this pattern are:
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected void waitForPageLoad() {
addToCartFalcon9().waitToExists();
}
NOTE
In real-world scenarios, you may decide to have only one base class for all
page types, even if there are some cases where it doesn't make much sense
to navigate to a particular page. Initially, in the framework I invented, many
people gave me feedback that it is hard to orient which base class to use. In
the end, my team and I decided to combine all of these base classes, even if
this meant violating some of the principles stated in the book. Sometimes,
we need to sacrifice something to gain another benefit. In this case, we
surrendered following some of the suggested practices in the book to
improve API usability and the framework users' understandability.
NOTE
Some developers relate code maintainability to the time needed to change a
piece of code (meet new requirements, correcting defects) or to the risk of
breaking something. Code readability and coupling are tightly connected to
it. Related terms are evolvability, modifiability, technical debt, and code
smells. There is an official metric called- maintainability index, which is
calculated using lines of code measures, McCabe, and Halstead complexity
measures.
Using Composition Principle
Another maintainability problem that you may have spotted is that right now
our page objects are still not following the Single Responsibility principle
precisely. They have more than one reason to change. What will happen if we
need to update the element locators due to design change, or figure out new
page action, or add new assertion method? We have at least three reasons to
change our page class. Similar to how we handled the navigation issue, we
can do something similar here.
However, we cannot create more base classes, since this will be impractical,
but we can make use of composition. We looked into what the composition
was in the previous chapter when we introduced the web sections. We can
use it again here to separate the actions, elements, assertions, and make our
page objects much more maintainable - by giving them only a single reason
to change.
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
@Override
protected void waitForPageLoad() {
couponCodeTextField().waitToExists();
}
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected void waitForPageLoad() {
elements().addToCartFalcon9().waitToExists();
}
NOTE
Please note that again I didn’t completely follow the naming conventions
for getters. If I did, I had to name the new getters - getElements and
getAssertions . I did the same for the get methods returning the web elements. I
believe this leads to improved readability and API usability of the library.
NOTE
If you follow the “Google Java Style Guide” it suggests to arrange all
members based on their modifiers – “Class and member modifiers, when
present, appear in the order recommended by the Java Language
Specification: public protected private abstract default static final transient
volatile synchronized native strictfp”. However, you will notice that I don’t
completely follow this suggestion for the page object models. There I group
the methods by meaning to improve the readability. First, I put the
protected getUrl and waitForPageLoad . Then I place the elements and
assertions getters or the private elements getters. They are followed by page
actions and, eventually, assertion methods if there are any and are not
placed in a separate class. Note that this is completely OK, depending on
which standards you follow. I will quote the “Oracle Code Conventions
for the Java Programming Language” file organization section - “The
methods should be grouped by functionality rather than by scope or
accessibility. For example, a private class method can be in between two
public instance methods. The goal is to make reading and understanding
the code easier”.
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
mainPage.open();
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void falcon9LinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
@Test
public void saturnVLinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
As you can see, we access the assertion methods and the publicly exposed
elements directly from the page. This is how we are accessing them now:
public class ProductPurchaseTestsWithPageObjects {
private Driver driver;
private static MainPage mainPage;
private static CartPage cartPage;
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
mainPage.open();
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void falcon9LinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertions().assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
@Test
public void saturnVLinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
After the refactoring, we need to use the assertions to access the assertion
methods and the еlements getter in order to access the elements. With these
changes, the readability decreased a bit, but the writing experience has
improved since when you type '.' after the page instance name, the list of
available options is now decreased significantly. By following a naming
convention for the elements and assertions getters, you will always know
which getters you should use in order to access them.
NOTE
As you can see from the above examples, there are no perfect solutions.
Some changes improve the maintainability and reusability but decrease the
readability and understandability. The third part of the book will be
dedicated to an evaluation system that can help you assess which design is
the most appropriate in a particular situation.
NOTE
Generics were added to Java to ensure type safety. To ensure that generics
wouldn't cause an overhead at runtime, the compiler applies a process
called type erasure on generics at compile time. Type erasure removes all
type parameters and replaces them with their bounds or Object if the type
parameter is unbounded. Thus the bytecode after compilation contains
only standard classes, interfaces, and methods, thus ensuring that no new
types are produced.
Let’s review again how we defined the elements and assertions getters till
now.
public class MainPage extends NavigatableEShopPage {
public MainPage(Driver driver) {
super(driver);
}
NOTE
The Java bytecode is the instruction set for the Java Virtual Machine. It acts
similar to an assembler, which is an alias representation of a C++ code. As
soon as a Java program is compiled, the Java bytecode is generated. It is a
machine code in the form of a .class file. Through it, we achieve platform
independence. When we compile a Java program, the compiler compiles it
and produces bytecode. After that, we can run it on any platform. The Java
Virtual Machine can run it, taking into account the current processor, which
means that we only need to have a basic Java installation on any platform.
NOTE
The Reflection API is used to examine or modify the behavior of methods,
classes, and interfaces at runtime. The required classes for reflection are
provided under java.lang.reflect package. It gives us information about the
class to which an object belongs and the methods of that class that can be
executed using the object. We can invoke methods at runtime irrespective
of the access specifier used with them.
The first one is quite simple. By deriving from it we will eliminate the
declaration of the Driver variable to every concrete elements’ class.
public abstract class BaseAssertions<ElementsT extends BaseElements> {
protected ElementsT elements() {
try {
var elementsClass = (Class<ElementsT>)
((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
return elementsClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
return null;
}
}
}
We are casting the first generic type argument ElementsT of the
class BaseAssertions to a class of its type. In Java, type parameters cannot be
instantiated directly, so we need to use Reflection API to instantiate the class
from the type parameter. The type parameters are being erased when the code
is compiled. Since the parameter doesn't have a constructor yet, the
methods getDeclaredConstructor and newInstance give us warnings about an
unhandled exception, so we need to wrap it in a try-catch block. It is not the
best practice to return null in the catch block, but we may never get to this
point anyway. When derived, this generic class is going to provide direct
access to the element map.
NOTE
Sometimes you will need to restrict the kinds of types allowed to be passed
to a type parameter. For example, in the above code, we limit the generic
type to be only a class that derives from the base class BaseElements . This is
what bounded type parameters are for. To declare a bounded type
parameter, list the type parameter's name, followed by the extends
keyword, followed by its upper bound.
The first thing to notice is that the class now requires two generic parameters
ElementsT that is necessary to derive from our new BaseElements and AssertionsT ,
which should extend the BaseAssertions abstract class. Similarly to
BaseAssertions , we have two important new methods: elements and assertions . The
first one is marked as protected since we don’t want to expose the getter to
the public, meaning that your library’s user won’t be able to access the page’s
web elements.
For the NavigatableEShopPage upgrade, we need only to put the generic
parameters, bounded type parameters, and extend another abstract base class,
EShopPage .
public abstract class NavigatableEShopPage<ElementsT extends BaseElements, AssertionsT extends
BaseAssertions<ElementsT>> extends EShopPage<ElementsT, AssertionsT> {
public NavigatableEShopPage(Driver driver) {
super(driver);
}
There is one huge drawback of this approach – now, all child pages should
provide both elements’ and assertions’ implementations. However, this is not
the case with most of the pages. Some don’t perform any assertions and thus
don’t force us to create an additional class. An example of this is the CartPage .
public class CartPage extends NavigatableEShopPage<CartPageElements, CartPageAssertions> {
public CartPage(Driver driver) {
super(driver);
}
// the rest of the code
}
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected void waitForPageLoad() {
elements().addToCartFalcon9().waitToExists();
}
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
checkoutPage = new CheckoutPage(driver);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void purchaseFalcon9WithoutFacade() {
mainPage.open();
mainPage.addRocketToShoppingCart("Falcon 9");
cartPage.applyCoupon("happybirthday");
cartPage.assertions().assertCouponAppliedSuccessfully();
cartPage.increaseProductQuantity(2);
cartPage.assertions().assertTotalPrice("114.00€");
cartPage.clickProceedToCheckout();
checkoutPage.fillBillingInfo(purchaseInfo);
checkoutPage.assertions().assertOrderReceived();
}
@Test
public void purchaseSaturnVWithoutFacade() {
mainPage.open();
mainPage.addRocketToShoppingCart("Saturn V");
cartPage.applyCoupon("happybirthday");
cartPage.assertions().assertCouponAppliedSuccessfully();
cartPage.increaseProductQuantity(3);
cartPage.assertions().assertTotalPrice("355.00€");
cartPage.clickProceedToCheckout();
checkoutPage.fillBillingInfo(purchaseInfo);
checkoutPage.assertions().assertOrderReceived();
}
}
When we need to test a new product or a new discount code, we will copy
one of the existing tests and change the data. There is one huge drawback of
such a solution. If the workflow changes, for example, we move the step with
the discount coupon to the last page or add a new discount type, we need to
go through the code and rearrange the steps for all tests.
We can solve the problem in a more elegant way through the usage of the
Facade design pattern.
Participants
The classes and objects participating in this pattern are:
● Facade Class - the main class that the user will use to access the
simplified API. It usually contains public methods which use the dependent
classes' logic.
● Dependent Classes - they hold specific logic which is later used in the
facade. They are usually used as parameters in the facade.
Facade Design Pattern Implementation
In our scenario, we can create a facade for creating purchases. It will contain
a single method called verifyItemPurchase that will call the pages' methods in the
right order.
public class PurchaseFacade {
private final MainPage mainPage;
private final CartPage cartPage;
private final CheckoutPage checkoutPage;
NOTE
To make the examples simpler, the method doesn’t follow all best
practices. In theory, it is OK to have methods up to 5-7 parameters. In this
case, we may consider moving all of them to a new class, as I usually do
when I have more than 4 parameters.
NOTE
It is a matter of preference whether to include or not assertion methods in
the facade's methods. It is entirely OK to move them outside of the facades,
making the tests more understandable. We will talk much more about
comparing different architecture variants in Chapter 8. Assessment System
for Tests' Architecture Design.
Also, we make assertions inside the function, which may seem that it breaks
the Single Responsibility principle. However, in this particular case, we
define the test structure, which is the primary purpose of the method. A better
name may be - verifyPurchaseOfItem or something similar. You may decide to
move the assertion methods to separate facade methods. However, I prefer
my facade methods to be more concise since we perform a few verifications
after some of the actions, not just at the end of the test.
Shall we examine how the tests will look if we use the new facade?
public class ProductPurchaseTestsWithPageObjects {
private Driver driver;
private static MainPage mainPage;
private static CartPage cartPage;
private static CheckoutPage checkoutPage;
private static PurchaseFacade purchaseFacade;
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
checkoutPage = new CheckoutPage(driver);
purchaseFacade = new PurchaseFacade(mainPage, cartPage, checkoutPage);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void purchaseFalcon9WithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
@Test
public void purchaseSaturnVWithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
@Override
protected void addItemToShoppingCart(String itemName) {
mainPage.open();
mainPage.addRocketToShoppingCart(itemName);
}
@Override
protected void applyCoupon(String couponName) {
cartPage.applyCoupon(couponName);
}
@Override
protected void assertCouponAppliedSuccessfully() {
cartPage.assertions().assertCouponAppliedSuccessfully();
}
@Override
protected void increaseProductQuantity(int quantity) {
cartPage.increaseProductQuantity(quantity);
}
@Override
protected void assertTotalPrice(String expectedPrice) {
cartPage.assertions().assertTotalPrice(expectedPrice);
}
@Override
protected void proceedToCheckout() {
cartPage.clickProceedToCheckout();
}
@Override
protected void fillBillingInfo(PurchaseInfo purchaseInfo) {
checkoutPage.fillBillingInfo(purchaseInfo);
}
@Override
protected void assertOrderReceived() {
checkoutPage.assertions().assertOrderReceived();
}
}
As with the initial design of the pattern, the page objects were initialized in
the constructor, and we used composition to store the pages needed for the
implementation of our method. However, here we don't define the workflow
method verifyItemPurchase since we inherit it from the base class. Instead, we
only need to override and implement all protected abstract methods, and this
is where we call the logic from the dependent page objects. We can create a
similar facade for the new shopping cart with its own page objects that will
be used in the same workflow.
The usage in tests remains identical, we just use the concrete facade -
NewPurchaseFacade instead of PurchaseFacade .
public class ProductPurchaseTestsWithPageObjects {
private Driver driver;
private static MainPage mainPage;
private static CartPage cartPage;
private static CheckoutPage checkoutPage;
private static NewPurchaseFacade purchaseFacade;
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
checkoutPage = new CheckoutPage(driver);
purchaseFacade = new NewPurchaseFacade(mainPage, cartPage, checkoutPage);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void purchaseFalcon9WithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
@Test
public void purchaseSaturnVWithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
Questions
1. Can you explain what test maintainability is?
2. What metric do we use to measure maintainability?
3. What is the difference between composition and inheritance?
4. What are the participants in the Template Method design pattern?
5. What is the Facade design pattern used for?
Chapter 6. API Usability
In the previous chapter, we talked about how to increase the maintainability
and reusability in our tests. Now we will investigate how we can ease the use
of all these library classes that are part of our framework. We will learn how
to make the test library API easy to use, learn, and understand. In the
beginning, we will apply the Interface Segregation principle by splitting the
driver API into smaller, but more focused parts.
After that, we will talk about different approaches on how to use the already
developed page objects through Singleton design pattern or App design
pattern. We will also look in another interesting approach called Fluent API
or chaining methods. At the end of the section, we will talk about whether it
is a good idea to expose the page objects elements to the users of your test
library.
The following topics will be covered in this chapter:
Interface Segregation Principle for WebCoreDriver Decorator
Use Page Objects Through Singleton Design Pattern
App Design Pattern for Creating Page Objects
Fluent API Page Objects
Page Objects Elements Access Styles
@Override
public void start(Browser browser) {
driver.start(browser);
}
@Override
public void quit() {
driver.quit();
}
@Override
public void goToUrl(String url) {
driver.goToUrl(url);
}
@Override
public Element findElement(By locator) {
return driver.findElement(locator);
}
@Override
public List<Element> findElements(By locator) {
return driver.findElements(locator);
}
@Override
public void waitForAjax() {
driver.waitForAjax();
}
@Override
public void waitUntilPageLoadsCompletely() {
driver.waitUntilPageLoadsCompletely();
}
}
The following code block shows how we have been using the Driver
decorator in our page objects till now.
public class CartPage extends NavigatableEShopPage {
public CartPage(Driver driver) {
super(driver);
}
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
NOTE
Strive to apply the Interface Segregation principle to all your most
complex classes. This will make not only the usage of your classes easier
but will make your tests much more readable.
Use Page Objects Through Singleton Design
Pattern
In the rest of the chapter we will investigate different approaches for using
our page objects. How about recalling how we used them till now in our
tests?
public class ProductPurchaseTestsWithPageObjects {
private Driver driver;
private static MainPage mainPage;
private static CartPage cartPage;
@BeforeMethod
public void testInit() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
mainPage.open();
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test
public void falcon9LinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertions().assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
@Test
public void saturnVLinkAddsCorrectProduct() {
mainPage.open();
mainPage.assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
In the testInit method we always start a new browser and create all pages. The
page objects are shared and used through local variables in the test class.
Now, we will discuss an alternative approach using the Singleton design
pattern.
Participants
The classes and objects participating in this pattern are:
Base Page - holds a static getter method returning the instance of the
class. It is created on the first call to the class. The class cannot be
created through the usage of the keyword new .
Concrete Page - inherits the base page. Can be used in tests through
the getInstance getter but cannot be created through the usage of the
keyword new .
return instance;
}
// rest of the code
}
However, this basic implementation won't work out of the box for us for two
reasons:
To handle the first issue, we can make our Driver class singleton too. To
resolve the second, we can move the singleton code to our base classes.
Making Driver Decorator Singleton
How about making our LoggingDriver singleton? To do so we need to introduce
a static variable and a property as in the following example:
public class LoggingSingletonDriver extends DriverDecorator {
private static LoggingSingletonDriver instance;
return instance;
}
@Override
public void start(Browser browser) {
System.out.printf("start browser = %s", browser.name());
driver.start(browser);
}
// rest of the code
}
protected EShopPage() {
this.elementFindService = LoggingSingletonDriver.getInstance();
}
protected NavigatableEShopPage() {
this.navigationService = LoggingSingletonDriver.getInstance();
}
private MainPage() {
}
return instance;
}
NOTE
Lazy initialization is a technique that defers the creation of an object until
the first time it is needed. In other words, the initialization of the object
happens only on demand. Note that the terms lazy initialization and lazy
instantiation mean the same thing - they can be used interchangeably.
private SingletonFactory() {
}
NOTE
Boilerplate is the term used to describe code sections that have to be
included in many places with little or no alteration. It is often used when
referring to languages that are considered verbose, i.e., the programmer
must write a lot of code to do minimal jobs.
We keep a static hashmap that holds all created instances and uses the class's
name as a key. If it was already there, we return it. Otherwise, we create it via
the Java Reflection API. In case of any exceptions, we return null. As a
second parameter, we pass an arbitrary number of arguments that some of our
objects might require.
The usage in pages is straightforward.
public class MainPage extends NavigatableEShopPage {
private MainPage() {
}
private SingletonFactory() {
}
NOTE
Using synchronized across multiple processing cores is often far more
expensive than you expect because synchronization forces code to execute
sequentially, which works against the goal of parallelism. Multicore CPUs
have separate caches (fast memory) attached to each processor core.
Locking requires these to be synchronized, requiring relatively slow cache-
coherency-protocol inter-core communication. [Kia 18]
@AfterMethod
public void testCleanup() {
LoggingSingletonDriver.getInstance().quit();
}
@Test
public void falcon9LinkAddsCorrectProduct() {
MainPage.getInstance().open();
MainPage.getInstance().assertions().assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
@Test
public void saturnVLinkAddsCorrectProduct() {
MainPage.getInstance().open();
MainPage.getInstance().assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
We access the pages through their getInstance method. For all tests in our
project, we will create a single static instance for every page object.
return page;
}
@Override
public void close() {
if (disposed) {
return;
}
LoggingSingletonDriver.getInstance().quit();
disposed = true;
}
}
There are a couple of interesting parts of the code above. First, we get a
singleton instance to the Driver decorator through the LoggingSingletonDriver
class which we use in the constructor to start the browser. We expose through
public getters the BrowserService , CookiesService , DialogService and NavigationService
interfaces. The service for finding elements is not exposed since it is up to the
page objects to locate the web elements.
We implement the Java AutoCloseable interface through the close method in
order to properly close the browser. This allows us to use the App class in try
statements, at the end of which the browser will be automatically closed.
Here is how the code looks if we use a try statement. In this case, the close
method is called automatically at the end of the using scope:
try (var app = new App(Browser.CHROME)) {
var mainPage = app.goTo(MainPage.class);
app.create(MainPage.class).assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
NOTE
The try-with-resources statement is a try statement that declares one or
more resources. A resource is an object that must be closed after the
program is finished with it. The try-with-resources ensures that each
resource is closed at the end of the statement. Any object that implements
java.lang.AutoCloseable , which includes all objects which implement
java.io.Closeable , can be used as a resource. The try-with-resources statement was
introduced in Java 7.
NOTE
The pattern for disposing of an object referred to as a dispose pattern,
imposes order on the lifetime of an object. The dispose pattern is used only
for objects that access unmanaged resources, such as file and pipe handles,
registry handles, wait handles or pointers to blocks of unmanaged memory.
This is because the garbage collector is very efficient at reclaiming unused
managed objects, but it is unable to reclaim unmanaged objects. To help
ensure that resources are always cleaned up appropriately, a close method
should be callable multiple times without throwing an exception.
The create and goTo methods are implemented as generic ones. The only
difference between the create and goTo methods is that the former is just
creating and returning an instance of the page. The latter is doing the same,
but at the same time navigating to the page. To ease the creation of pages and
use them as singleton objects, we use the previously developed SingletonFactory
class. As a second parameter, we pass the already initialized instance of the
Driver through the LoggingSingletonDriver .
NOTE
Reflection API in Java and C# allows you to examine the objects at
runtime, manipulate internal properties, dynamically create an instance of a
type, invoking its methods, or accessing its fields or properties no matter of
their access modifiers.
In the SingletonFactory class we use Reflection API to create the instances,
invoking the first public constructor. However, the usage of Reflection API
for such cases is a little bit “hacky” and slower than using the new
keyword. The better way to handle it is to use an inversion of control
container (IoC Container). However, I will not refactor it further to avoid
complicating the examples.
@BeforeClass
public void beforeClass() {
app = new App(Browser.CHROME);
}
@AfterClass
public void afterClass() {
app.close();
}
@Test
public void falcon9LinkAddsCorrectProduct() {
var mainPage = app.goTo(MainPage.class);
mainPage.assertions().assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
@Test
public void saturnVLinkAddsCorrectProduct() {
var mainPage = app.goTo(MainPage.class);
app.create(MainPage.class).assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
Since the App is created in the beforeClass , this means that the browser will be
reused for all tests in the class. You can always create the App in each test
but then you can benefit from the implementation of the AutoClosable
interface. You can create the object wrapped in a try-with-resources
statement without worrying about calling the close method each time (it will
be called at the end of the statement).
@Test
public void saturnVLinkAddsCorrectProduct() {
try (var app = new App(Browser.CHROME)) {
var mainPage = app.goTo(MainPage.class);
app.create(MainPage.class).assertions().assertProductBoxLink("Saturn V",
"http://demos.bellatrix.solutions/product/saturn-v/");
}
}
NOTE
The Fluent Interface was first defined by Eric Evans and Martin Fowler as
an implementation of an OOP API that gives the user a more concise and
readable code.
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/cart/";
}
@Override
protected void waitForPageLoad() {
elements().couponCodeTextField().waitToExists();
}
@BeforeClass
public void beforeClass() {
app = new App(Browser.CHROME);
}
@AfterClass
public void afterClass() {
app.close();
}
@Test
public void completePurchaseSuccessfully_WhenNewClient() {
var mainPage = app.goTo(MainPage.class);
mainPage.addRocketToShoppingCart();
var cartPage = app.goTo(CartPage.class);
cartPage.applyCoupon("happybirthday")
.assertCouponAppliedSuccessfully()
.increaseProductQuantity(2)
.assertTotalPrice("114.00€")
.clickProceedToCheckout();
}
}
As you can see the interesting part here is how you call the page methods one
after another. This is the so-called Method Chaining, which is achieved by
returning the page itself as a return type for each function.
NOTE
The term "vanilla" means using pure WebDriver library code directly in
tests without any abstraction on top of it. The term comes from Vanilla
JavaScript which differentiates pure JS code from libraries such
Now let us investigate a couple of variations on how you can access web
elements in tests.
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected void waitForPageLoad() {
elements().addToCartFalcon9().waitToExists();
}
If you want to use some of the elements directly in the tests, you can do it
through the Elements public property of the page.
@Test(priority = 1)
public void completePurchaseSuccessfully_whenNewClient() {
mainPage.open();
mainPage.elements().addToCartFalcon9().click();
mainPage.assertions().assertProductBoxLink("Falcon 9",
"http://demos.bellatrix.solutions/product/falcon-9/");
}
With this variant, there is some code duplication in the searching for the
element. We can refactor the code by creating a private variable of the
element, and then wrap it through the getter.
private Element firstName = elementFindService.findElement(By.id("firstName"));
Questions
1. What is the benefit of splitting the WebDriver interface into smaller
interfaces?
2. How can you make a class to be a singleton?
3. What is the primary purpose of the App design pattern?
4. What changes do you need to make to a regular page object to make it
use the Fluent API?
5. Define three approaches for accessing elements in tests?
Chapter 7. Building Extensibility in
Your Test Library
In the previous chapter, we talked about how we can ease the use of all these
framework library classes. We applied the Interface Segregation principle
to split the fat WebDriver interface into smaller, easier to use components. We
also used Singleton and App design patterns to improve the framework's
API usability.
In this chapter, we will discuss maybe an even more important topic - how to
make the framework's code easier to modify and customize while following
the Open/Closed principle. We will look at how the Strategy design
pattern can help us build such extensibility for locating and waiting for
elements. At the end of the chapter, you will read about the existing Vanilla
WebDriver functionality that allows you to add and execute custom code at
various points of the WebDriver execution flow.
The following topics will be covered in this chapter:
Building extensibility for elements finding through Strategy design
pattern
Building extensibility for elements waiting through Strategy design
pattern
Adding extensibility points through EventFiringWebDriver
NOTE
You can find the WebDriver Java source code in the official GitHub
repository of the project. It is located under the java folder of Selenium
repository.
Participants
The classes and objects participating in this pattern are:
return logElement;
}
@Override
public List<Element> findElements(By locator) {
List<WebElement> nativeWebElements =
webDriverWait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(locator));
var elements = new ArrayList<Element>();
for (WebElement nativeWebElement : nativeWebElements) {
Element element = new WebCoreElement(webDriver, nativeWebElement, locator);
Element logElement = new LogElement(element);
elements.add(logElement);
}
return elements;
}
// rest of the code
}
We use the interface through composition rather than inheritance. Later, the
two methods and some of the static methods of the By class are called.
public class CartPageElements {
private final ElementFindService elementFindService;
@Override
public By convert() {
return By.id(getValue());
}
}
3. Now shall we review how our new Id containing strategy will look like:
public class IdContainingFindStrategy extends FindStrategy {
public IdContainingFindStrategy(String value) {
super(value);
}
@Override
public By convert() {
return By.cssSelector(String.format("[id*='%s']", getValue()));
}
}
Since there isn't a native method for finding web elements by Id containing,
we use the cssSelector method, where we build a CSS locator expression.
You need to locate an element by inner text containing a particular string? No
problem - this time instead of CSS we will use XPath.
public class InnerTextContainsFindStrategy extends FindStrategy {
public InnerTextContainsFindStrategy(String value) {
super(value);
}
@Override
public By convert() {
return By.xpath(String.format("//*[contains(text(), '%s')]", getValue()));
}
}
Refactoring ElementFindService
In our case, the context class from the diagram is the WebCoreDriver decorator.
We need to make changes in it and in the ElementFindService interface in order
to start using the new find strategies.
public interface ElementFindService {
Element findElement(By locator);
List<Element> findElements(By locator);
}
We added a parameter holding the find strategy.
public interface ElementFindService {
List<Element> findAll(FindStrategy findStrategy);
Element find(FindStrategy findStrategy);
}
@Override
public List<Element> findAll(FindStrategy findStrategy) {
List<WebElement> nativeWebElements =
webDriverWait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(findStrategy.convert()
var elements = new ArrayList<Element>();
for (WebElement nativeWebElement:nativeWebElements) {
Element element = new WebCoreElement(webDriver, nativeWebElement,
findStrategy.convert());
Element logElement = new LogElement(element);
elements.add(logElement);
}
return elements;
}
// rest of the code
}
We pass the find strategy as a parameter to the find and findAll methods of
the WebCoreDriver decorator. After that, we use the strategy to return the
locator to find the native web elements. When the element is found, we wrap
it in the element decorators and return it.
NOTE
Maybe you have noticed that our wait for elements to exist logic
disappeared. We will add it to the ElementFinderService in the next section.
How about examining the usage of the methods after the refactoring?
public class CartPageElements {
private final ElementFindService elementFindService;
@Override
public Element findByXPath(String xpath) {
return find(new XPathFindStrategy(xpath));
}
@Override
public Element findByTag(String tag) {
return find(new TagFindStrategy(tag));
}
@Override
public Element findByClass(String cssClass) {
return find(new ClassFindStrategy(cssClass));
}
@Override
public Element findByCss(String css) {
return find(new CssFindStrategy(css));
}
@Override
public Element findByLinkText(String linkText) {
return find(new LinkTextFindStrategy(linkText));
}
@Override
public List<Element> findAllById(String id) {
return findAll(new IdFindStrategy(id));
}
@Override
public List<Element> findAllByXPath(String xpath) {
return findAll(new XPathFindStrategy(xpath));
}
@Override
public List<Element> findAllByTag(String tag) {
return findAll(new TagFindStrategy(tag));
}
@Override
public List<Element> findAllByClass(String cssClass) {
return findAll(new ClassFindStrategy(cssClass));
}
@Override
public List<Element> findAllByCss(String css) {
return findAll(new CssFindStrategy(css));
}
@Override
public List<Element> findAllByLinkText(String linkText) {
return findAll(new LinkTextFindStrategy(linkText));
}
@Override
public List<Element> findAll(FindStrategy findStrategy) {
List<WebElement> nativeWebElements =
webDriverWait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(findStrategy.convert()));
var elements = new ArrayList<Element>();
for (WebElement nativeWebElement:nativeWebElements) {
Element element = new WebCoreElement(webDriver, nativeWebElement,
findStrategy.convert());
Element logElement = new LogElement(element);
elements.add(logElement);
}
return elements;
}
@Override
public Element find(FindStrategy findStrategy) {
var nativeWebElement =
webDriverWait.until(ExpectedConditions.presenceOfElementLocated(findStrategy.convert()));
Element element = new WebCoreElement(webDriver, nativeWebElement,
findStrategy.convert());
Element logElement = new LogElement(element);
return logElement;
}
// rest of the code
}
After these changes, the usage of the find API has been improved
significantly. Let’s see how:
public class CartPageElements {
private final ElementFindService elementFindService;
NOTE
The extension methods are special methods that allow adding logic to an
existing type without the need to derive a type or to recompile the code.
They are static methods that can be called as they were instance methods of
the type. Natively Java doesn’t support extension methods. However, there
are some alternatives, such as the Manifold project. To use it, you need to
install a plug-in for IntelliJ and add some dependencies to Maven’s
pom.xml. Keep in mind that their plug-in is paid.
An extension class does not physically alter its extended class. The methods
defined in an extension are not inserted into the extended class. Instead, the
Java compiler and Manifold cooperate in calling the extension's static
method to look like a call to an instance method on the extended class. As a
consequence, extension calls dispatch statically.
@Extension
public class ElementFindServiceExtensions {
public static Element findByIdContaining(@This ElementFindService elementFindService,
string idContaining) {
return driver.find(new IdContainingFindStrategy(idContaining));
}
}
To create such an extension method for locating elements, we created a class
called ElementFindServiceExtensions and added the new method. There are two
important notes here. First, the method should be static . Second, you need to
use the annotation @This to specify the class that you want to extend and it
must be the first parameter, in our case the ElementFindService interface
(later when you use the method you don't need to supply this argument).
To use the extension methods in your code, you need to include the package
of the extension class: import core.locators.extensions;
public class CartPageElements {
private final ElementFindService elementFindService;
NOTE
Keep in mind that some of Lombok’s features are marked as experimental.
Thus you may not be able to use them. At the moment of writing the book,
the extension methods are still in this stage. There is some on-going effort
to allow IntelliJ Lombok’s plug-in to support experimental features.
NOTE
Another free alternative to Manifold is project Lombok. In their
experimental features, they support extension methods too. I believe their
version is even simpler. The Lombok plug-in is installed by default to the
latest version of IntelliJ.
Let’s quickly review the Lombok version. The declaration of the extension
method is even simpler since you don’t need to use any annotations.
public class ElementFindServiceExtensions {
public static Element findByIdContaining(ElementFindService elementFindService, string
idContaining) {
return driver.find(new IdContainingFindStrategy(idContaining));
}
}
To use the extension method as part of the ElementFindService , you need to
annotate the class with the @ExtensionMethod(ElementFindServiceExtensions.class)
annotation.
@ExtensionMethod(ElementFindServiceExtensions.class)
public class CartPageElements {
private final ElementFindService elementFindService;
NOTE
In order to make these Lombok annotations work, you need to enable the
annotations processing. You need to go to the Preferences | Build,
Execution, Deployment | Compiler | Annotation Processors and make
sure of the following:
• Enable annotation processing box is checked
• Obtain processors from project classpath option is selected
Keep in mind that some of Lombok’s features are marked as experimental.
Thus you may not be able to use them. At the moment of writing the book,
the extension methods are still in this stage. There is some on-going effort
to allow IntelliJ Lombok’s plug-in to support experimental features.
Building Extensibility for Waiting for
Elements through Strategy Design Pattern
Waiting for web elements' workable state is essential for every stable
automated test. In the previous versions of our framework we already used
the WebDriver wait API for waiting for elements to be present on the page,
but sometimes we need more methods for waiting other attributes to reach
specific state. Such as whether the element is clickable, visible or contains
text.
@Override
public List<Element> findElements(By locator) {
List<WebElement> nativeWebElements =
webDriverWait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(locator));
var elements = new ArrayList<Element>();
for (WebElement nativeWebElement : nativeWebElements) {
Element element = new WebCoreElement(webDriver, nativeWebElement, locator);
Element logElement = new LogElement(element);
elements.add(logElement);
}
return elements;
}
// rest of the code
}
@Override
public void waitUntil(SearchContext searchContext, WebDriver driver, By by) {
waitUntil((x) -> elementExists(searchContext, by), driver);
}
NOTE
Lambda Expressions were added in Java 8. A lambda expression is a short
block of code that takes in parameters and returns a value. Lambda
expressions are similar to methods, but they do not need a name, and they
can be implemented right in the body of a method.
Shall we develop one more concrete wait strategy, for example the one for
waiting for element to be clickable?
public class ToBeClickableWaitStrategy extends WaitStrategy {
public ToBeClickableWaitStrategy(int timeoutIntervalSeconds, int sleepIntervalSeconds) {
super(timeoutIntervalSeconds, sleepIntervalSeconds);
}
@Override
public void waitUntil(SearchContext searchContext, WebDriver driver, By by) {
waitUntil((x) -> elementIsClickable(searchContext, by), driver);
}
Creating ElementWaitService
As mentioned till now we didn't have any API for allowing users to wait for
elements explicitly. How about encapsulating all our wait logic in a service
that will be only responsible for elements waiting? To do that we need to
follow a few steps:
NOTE
We have overloads without timeout parameters. Inside the methods’ body,
we use the default timeouts in Chapter 10. Test Data Preparation and Test
Environments, we will review how to set/read these default values from
configuration files based on the environment against which we execute the
automated tests.
4. The factory works together with a static class which is used for syntax
sugar for easier usage and making the code more readable.
public class Wait {
public static WaitStrategyFactory to() {
return new WaitStrategyFactory();
}
}
5. Here is how we use it. From the code you can see why we didn't
follow the best practices for naming methods and classes.
public class CartPage extends NavigatableEShopPage {
private final BrowserService browserService;
private final ElementWaitService elementWaitService;
NOTE
If we want to enable our framework users to add their custom wait
strategies, they can create an extension method for the WaitStrategyFactory and
Wait classes. Lombok or Manifold projects can be used for the job in the
same way we developed find element extensions.
Integrating EventFiringWebDriver
We can add the EventFiringWebDriver logic to our WebCoreDriver decorator. The
first version will look like this:
public class WebCoreDriver extends Driver {
private WebDriver webDriver;
private WebDriverWait webDriverWait;
private EventFiringWebDriver eventFiringWebDriver;
@Override
public void start(Browser browser) {
switch (browser)
{
case CHROME:
WebDriverManager.chromedriver().setup();
webDriver = new ChromeDriver();
break;
case FIREFOX:
WebDriverManager.firefoxdriver().setup();
webDriver = new FirefoxDriver();
break;
case EDGE:
WebDriverManager.edgedriver().setup();
webDriver = new EdgeDriver();
break;
case OPERA:
WebDriverManager.operadriver().setup();
webDriver = new OperaDriver();
break;
case SAFARI:
WebDriverManager.safaridriver().setup();
webDriver = new SafariDriver();
break;
case INTERNET_EXPLORER:
WebDriverManager.iedriver().setup();
webDriver = new InternetExplorerDriver();
break;
default:
throw new IllegalArgumentException(browser.name());
}
webDriverWait = new WebDriverWait(webDriver, 30);
eventFiringWebDriver = new EventFiringWebDriver(webDriver);
eventFiringWebDriver.register(new LoggingListener());
}
// rest of the code
}
When we initialize WebDriver in the start method, we create an instance of
the EventFiringWebDriver , and we register our listener classes. LoggingListener
prints to the console a message when a particular action happens. The
LoggingListener class needs to implement the interface WebDriverEventListener .
public class LoggingListener implements WebDriverEventListener {
@Override
public void beforeNavigateRefresh(WebDriver driver) {
System.out.print("before navigate refresh");
}
@Override
public void afterNavigateRefresh(WebDriver driver) {
System.out.print("after navigate refresh");
}
@Override
public void beforeFindBy(By by, WebElement element, WebDriver driver) {
System.out.print("before find by");
}
@Override
public void afterFindBy(By by, WebElement element, WebDriver driver) {
System.out.print("after find by");
}
@Override
public void beforeClickOn(WebElement element, WebDriver driver) {
System.out.print("before click on");
}
@Override
public void afterClickOn(WebElement element, WebDriver driver) {
System.out.print("after click on");
}
// rest of the code
}
Summary
In this chapter, we discussed topics related to the framework extensibility.
How to make the test code easier to extend and customize following the
Open/Closed principle. We used the Strategy design pattern to build
extensibility for locating and waiting for elements. At the end of the chapter,
we talked about the existing Vanilla WebDriver support for adding execution
of custom code at various points of the WebDriver execution flow.
With this chapter we ended up the second part of the book where we
discussed the attributes of the high-quality tests - test readability, test
maintainability/reusability, API usability and extensibility. In the next part,
we will learn a systematic method of evaluating and deciding which is the
best possible approach for test writing - having in mind the various attributes
of the designs. Also, we will talk about how to use most appropriately test
data and create the right environment for test execution.
Questions
1. Can you define the main participants in the Strategy design pattern?
2. Why is the vanilla WebDriver By class not easy to extend?
3. How can you wait for a specific element to be clickable?
4. What is the primary role of the EventFiringWebDriver class?
Chapter 8. Assessment System for
Test Architecture Design
In the previous chapter, we discussed topics related to the framework's
extensibility - how to make the framework's code easier to extend and
customize, following the Open/Closed principle. We used the Strategy
design pattern to build such extensibility for locating and waiting for
elements. At the end of the chapter, we talked about the existing Vanilla
WebDriver support for allowing you to add and execute custom code at
various points of the WebDriver execution flow. With this, we ended up the
second part of the book, where we discussed the high-quality attributes - test
readability, test maintainability/reusability, API usability and extensibility.
In the next part, we will learn a systematic method of evaluating and deciding
which is the best possible approach for test writing - having in mind various
attributes of the designs. Also, we will talk about how to use most
appropriately test data and create the right environment for test execution.
First, we will start discussing the test's design assessment system, defining its
criteria, explaining how to apply it in practice. In the second section of the
chapter, we will evaluate three of the designs we already used in the book -
tests without page objects, the ones that used the pattern and at the end tests
that use the Facade design pattern.
The following topics will be covered in this chapter:
Assessment System Introduction Assessment
System Usage Examples
Criteria Definitions
What is the automated test designs' assessment system? It contains 7 criteria
which will be used later to evaluate each design. For each of them, we assign
ratings based on our judgment. Most of them are not tool evaluated, but
rather they depend on our understanding of the terms and architecture know-
how. Don't worry, reading previous chapters should give you enough
knowledge to be able to put proper ratings. We will discuss later more
thoroughly the exact steps to incorporate the system in your team. The whole
second section of the chapter is dedicated to sample usages of it. Let us start
with defining the assessment criteria. We already reviewed some of the
definitions in chapter one, but I will go through them again with some
additions. Additionally, we had whole chapters discussing some of the
criteria, so till now, you should have a good feel for what the terms mean.
1. Readability
By reading the code, you should be able to find out what the code does easily.
A code that is not readable usually requires more time to read, maintain,
understand and can increase the chance to introduce bugs. Some
programmers use huge comments instead of writing more simple and
readable code. It is much easier to name your variables, methods, classes
correctly instead of relying on these comments. With time passing, the
comments tend not to be maintained, and they can mislead the readers.
2. Maintainability
“The ease with which we can customize or change our software solution to
accommodate new requirements, fix problems, improve performance.”
Imagine there is a problem in your tests. How much time do you need to
figure out where the problem is? Is it an automation bug or an issue in the
system under test? It is tightly related to the design of your tests - how the
tests are grouped, what design patterns are used, where your elements'
locators are placed. It depends on the extensibility and reusability of your
code but as a whole it should measure the ease with which you analyze the
test failures and keep the tests running. The better the maintainability is, the
easier it is for us to support our existing code, accommodate new
requirements or just fix some bugs.
3. Reusability
It is tightly connected to maintainability. A related principle is the so-called
DRY- Don't Repeat Yourself. The most basic idea behind the DRY principle
is to reduce long-term maintenance costs by removing all unnecessary
duplication. But if at the beginning you haven't designed your code in such a
way, the changes may need to be applied to multiple places, which can lead
to missing some of them and thus resulting in more bugs. Moreover, this can
slow down the new tests’ development, analysis of test failures, and existing
tests' maintenance.
4. API Usability
The API is the specification of what you can do with a software library. API
usability means how easy it is for you as a user to find out what the methods
do and figure out how to use them. If we are talking about a test library - how
much time is needed for a new user to create a new test? It is related to the
learning curve.
In the programming community, we sometimes use another term for the same
thing called syntactic sugar. It describes how easy it is to use or read some
expressions. It sweetens the programming languages for humans. The
programming statements become more concise and clearer.
5. Extensibility
One of the hardest things to develop is to allow these generic frameworks to
be extensible and customizable. The whole point of creating a shared
library is to be used by multiple teams across the company. However, the
different teams work in a different context. They may have to test a little bit
different thing. So, the library code may not be working out of the box for
them. Thus, the engineers should be able to customize some parts to fit it to
their needs.
In the case of automated tests, we already looked into a test suite used for
testing an online shopping cart. The testing workflow consists of multiple
steps- choosing the product, changing the quantity, adding more products,
applying discount coupons, filling billing info, providing payment info and so
on. If a new requirement comes that the system should prefill the billing info
for logged users- how easy would it be for you to change the existing tests?
Did you write your tests in a way that if you add this new functionality, it
would not affect your existing tests?
NOTE
The Halstead complexity measures were introduced in 1977 by Maurice
Halstead. They are software metrics part of the empirical science of
software development. He observed that the metrics should reflect the
expression or implementation of algorithms in different languages, but at
the same time, be independent from their execution on a specific platform.
Cyclomatic Complexity - Below you can find the formula for Cyclomatic
complexity.
NOTE
The numbers in the table are an example. We used them in my teams to
measure the code complexity index. They are based on research in books
that developers have used to evaluate the Cyclomatic complexity. However,
the numbers varied significantly, so feel free to modify them as you please
or do your own research.
NOTE
The Cyclomatic Complexity was invented by Thomas J. McCabe, Sr. in
1976 in his paper where he explained the idea. He borrowed concepts from
graph theory to represent the programs' code as a graph. The goal is to
capture the complexity of a module in a single number.
7. Learning Curve
I also like to call this attribute "Easy Knowledge Transfer". The attribute
answers the question- "How easy is it for someone to learn by himself?". The
learning curve is tightly connected to API usability, but it means something a
bit different. If a new member joins your team, is he able to learn by himself
how to use your test automation framework or he needs to read the
documentation if it exists? Or you have a mentoring program where you need
to teach these new members yourself every time how to use your code?
8. KISS*
The last criterion is not something that we will calculate rather it is a
principle- "Keep It Simple Stupid". The below quote from Martin Fowler
best describes the principle.
“Any fool can write code that a computer can understand. Good
programmers write code that humans can understand.”
We can use it in case all other numbers/ratings/index are equal, which in my
practice haven't happened. But if in some rare case happens the rule of thumb
is that you chose the simplest design. Later we will assess a design that uses
page objects and the more sophisticated one that uses facades. If for some
reason they have equal ratings, and we apply the KISS rule, we will most
probably pick the page objects design since it is simpler than the facade one.
NOTE
Martin Fowler is a British software developer. He is an international
speaker and author of several popular software development books. Two of
his most famous pieces are Refactoring: Improving the Design of Existing
Code and Patterns of Enterprise Application Architecture.
Assessment Ratings
For each of the above criteria, we assign a rating, which is basically a
number. To calculate the final TDI (Test Design Index), we sum all of the
criteria ratings. The design with better TDI score wins the competition.
(1) Very Poor
(2) Poor
(3) Good
(4) Very Good
(5) Excellent
Steps to Apply
The test architecture design assessment is a team effort. You can do it on
your own, but this way you may not be too objective. When you have the
TDI ratings from a couple of experienced colleagues, it will decrease the
personal preferences during the final decision. Here is the recommended
assessment flow.
1. Create a research & development branch different from the one where
your tests are right now
2. Create separate projects for each test design
3. Develop a few tests using the proposed design
4. Implement the same set of test cases for each design
5. Present the solutions to the team members that will be part of the
assessment
6. Every participant uses the system and assigns ratings
7. Create a final assessment meeting
@BeforeMethod
public void testInit() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
}
@AfterMethod
public void testCleanup() {
driver.quit();
}
@Test(priority=1)
public void completePurchaseSuccessfully_whenNewClient() {
driver.navigate().to("http://demos.bellatrix.solutions/");
var addToCartFalcon9 = waitAndFindElement(By.cssSelector("[data-product_id*='28']"));
addToCartFalcon9.click();
var viewCartButton = waitAndFindElement(By.cssSelector("[class*='added_to_cart wc-
forward']"));
viewCartButton.click();
// This pause will be removed when we introduce a logic for waiting for AJAX requests.
Thread.sleep(5000);
var placeOrderButton = waitAndFindElement(By.id("place_order"));
placeOrderButton.click();
Thread.sleep(10000);
var receivedMessage =
waitAndFindElement(By.xpath("/html/body/div[1]/div/div/div/main/div/header/h1"));
Assert.assertEquals(receivedMessage.getText(), "Order received");
}
@Test(priority=2)
public void completePurchaseSuccessfully_whenExistingClient() {
driver.navigate().to("http://demos.bellatrix.solutions/");
// This pause will be removed when we introduce a logic for waiting for AJAX requests.
Thread.sleep(5000);
var placeOrderButton = waitAndFindElement(By.id("place_order"));
placeOrderButton.click();
@Test(priority=3)
public void correctOrderDataDisplayed_whenNavigateToMyAccountOrderSection() {
driver.navigate().to("http://demos.bellatrix.solutions/");
2. Maintainability
As far as we have only a single class, it may not be so much work to maintain
the tests. However, with the increase of added tests, it will get harder and
harder to fix the logic. It is true that we can use find and replace to change
locators, but imagine that we haven't moved some of the logic in a private
method and it is duplicated in all tests- then you will need to go and change it
everywhere. Also, since the tests are copy-pasted in most parts but with small
modifications, when you have lots of failed tests, you cannot be sure that the
reasons for the failures in the workflow- you will need to debug and
investigate each failure separately. If you have only a few tests, then the
maintainability won't be as bad as described but still it won’t be great. I gave
a Poor rating of 2.
Criteria Rating
Readability 2
Maintainability 2
Reusability
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
3. Reusability
As you can see from the example, our code reuse is limited to a couple of
private methods which as mentioned in the quick fix scenario will be copy-
pasted in new test classes. Without further review of the design which means
will make the tests even harder to maintain. My rating here is very poor (1).
Criteria Rating
Readability 2
Maintainability 2
Reusability 1
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
4. API Usability
How easy is it to use the design API? Well, in this particular case, I cannot
say that we have an API at all. We have a few private methods, but that is all.
I don't think it is fair to rate how easy it is to use the vanilla WebDriver API
since the whole idea of the exercise is to create our abstraction layer for more
robust and stable tests. Again, I gave a rating of very poor (1).
Criteria Rating
Readability 2
Maintainability 2
Reusability 1
API Usability 1
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
5. Extensibility
Imagine that we want to add a new step to the online shopping cart purchase
workflow. What will we do in the case of non-page-object tests? If the logic
is a private method, we can add it there in a single place, but this would be
true only if it is not copy-pasted among other test classes. If it is not reused,
the code for extending may appear in multiple tests, then we will need to
analyze which of them to refactor and copy-paste the new logic, which again
with each addition will worsen the maintenance. I gave a rating of poor (2).
Criteria Rating
Readability 2
Maintainability 2
Reusability 1
API Usability 1
Extensibility 2
Code Complexity Index
Learning Curve
Test Design Index
We will use only the Class metrics so you can uncheck the rest. From the
Class metrics section, choose Coupling between Objects, Depth of
Inheritance tree, Weighted method complexity (Total methods’ Cyclomatic
Complexity), Lines of code, and Halstead volume.
Then, in the Class Metrics Results window, you can find the code metrics
for each class part of your solution.
If your design contains more than one class - we get the numbers for each
class part of the designs and get the average rating. To ease all of these
calculations, we can copy the data to an Excel sheet. There, created a new
column for calculating the maintainability index based on the formula
Maintainability Index = MAX(0, (171 – 5.2 * ln(Halstead Volume) – 0.23
* (Cyclomatic Complexity) – 16.2 * ln(Lines of Code))*100 / 171)
Let us review the table again. It helps us to determine the index of each of
the components used in the formula.
Ratings Maintainability Cyclomatic Class Depth of
Index Complexity Coupling Inheritance
(5) > 70 < 10 < 10 =< 3
Excellent
(4) Very > 60 10-12 < 15 4
Good
(3) Good 40-60 12-15 < 20 5
(2) Poor 20-40 15-20 < 30 6-8
(1) Very < 20 > 20 > 30 >8
Poor
The maintainability index is equal to 26.1, so we put an index poor (2). For
Cyclomatic Complexity = 9 - excellent (5). Class Coupling = 0 - excellent
(5). Depth of Inheritance = 1 excellent (5). After we have each separate
component index, we replace them in the formula to get the Code Complexity
Index Rating.
Code Complexity Index Rating = (2 + 5 + 5 + 5)/4 = 4
Criteria Rating
Readability 2
Maintainability 2
Reusability 1
API Usability 1
Extensibility 2
Code Complexity Index 4
Learning Curve
Test Design Index
7. Learning Curve
This type of design is the easiest to start with since it doesn't require any
previous knowledge besides how to use WebDriver. However, the tests'
bodies tend to be much longer when they contain low-level WebDriver API
details, which makes understanding of the test cases harder. Moreover, since
some of the code is copy-pasted, another part is reused as private methods, it
can get confusing for newcomers. Therefore, I give only a good (3) rating.
Criteria Rating
Readability 2
Maintainability 2
Reusability 1
API Usability 1
Extensibility 2
Code Complexity Index 4
Learning Curve 3
Test Design Index
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/";
}
@Override
protected void waitForPageLoad() {
elements().addToCartFalcon9().waitToExists();
}
@Override
public void close() {
if (disposed) {
return;
}
LoggingSingletonDriver.getInstance().quit();
disposed = true;
}
}
Finally, here is what our code looks like.
public class ProductPurchaseTestsWithPageObjects {
private static App app;
@BeforeMethod
public void testInit() {
app = new App(Browser.CHROME);
}
@AfterMethod
public void testCleanup() {
app.close();
}
@Test
public void completePurchaseSuccessfully_WhenNewClient() {
var mainPage = app.goTo(MainPage.class);
mainPage.addRocketToShoppingCart();
var cartPage = app.goTo(CartPage.class);
cartPage.applyCoupon("happybirthday")
.assertCouponAppliedSuccessfully()
.increaseProductQuantity(2)
.assertTotalPrice("114.00€")
.clickProceedToCheckout();
}
}
This particular implementation of page objects uses the Fluent API, which
we discussed in Chapter 6. API Usability. Here the App class is declared as a
private static variable, which means it cannot be shared among test classes. If
there are changes in the workflow, we will need to go through each test
separately. The usage of Fluent API makes the usage of find-and-replace
harder.
1. Readability
I think the readability here is excellent (5). If you name your methods and
page objects right, you can understand the whole business use case from the
code. At the same time, the page objects hide the low-level details of the
vanilla WebDriver API.
Criteria Rating
Readability 5
Maintainability
Reusability
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
2. Maintainability
Debugging and fixing tests using the page objects is relatively easy since we
reuse most of the code and the fixes happen in a single place. The only
drawback which I previously stated is that if there are changes in the test
workflow (changes in the purchase workflow), then we need to go and
change that in every single test. The same is valid if you add or remove
method parameters to the page objects actions. Therefore, I give here the
rating of (4) very good.
Criteria Rating
Readability 5
Maintainability 4
Reusability
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
3. Reusability
I rate the reusability as very good (4). Most of the previously stated
arguments are valid for it too.
Criteria Rating
Readability 5
Maintainability 4
Reusability 4
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
4. API Usability
I think Fluent API is quite intuitive to use. The page object design has one
drawback compared to facade ones - you must know the order of the pages
inside the user workflow. Moreover, you need to write much more code and
as in this case, call much more methods to describe the purchase workflow.
This is why I rate the API Usability as only very good (4).
Criteria Rating
Readability 5
Maintainability 4
Reusability 4
API Usability 4
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
5. Extensibility
If you need to add a new section for all pages, you will add it to the pages'
base classes. If you need to add a behavior to page creation, you will do it in
the App class. If you need to add something to a particular page specific
action, you will do it in the page object itself. I start to repeat myself a bit, but
again the drawback here is if you need to add new behavior to the existing
tests workflow, then you need to go through each one of the tests. This is why
again I put a rating of (4) very good.
Criteria Rating
Readability 5
Maintainability 4
Reusability 4
API Usability 4
Extensibility 4
Code Complexity Index
Learning Curve
Test Design Index
7. Learning Curve
The tests are relatively understandable - this is true. However, this design has
lots of moving parts - base page objects, page objects classes, the App
factory, and the test class. As pointed before you need to learn what is the
sequence of the pages and how to call their methods. This is why I give a
rating of (4) very good.
Criteria Rating
Readability 5
Maintainability 4
Reusability 4
API Usability 4
Extensibility 4
Code Complexity Index 5
Learning Curve 4
Test Design Index
Test Design Index
Criteria Rating
Readability 5
Maintainability 4
Reusability 4
API Usability 4
Extensibility 4
Code Complexity Index 5
Learning Curve 4
Test Design Index 4.29
Test Design Index = (5 + 4 + 4 + 4 + 4 + 5 + 4) / 7 = 4.29
@BeforeClass
public void beforeClass() {
driver = new LoggingDriver(new WebCoreDriver());
driver.start(Browser.CHROME);
mainPage = new MainPage(driver);
cartPage = new CartPage(driver);
checkoutPage = new CheckoutPage(driver);
purchaseFacade = new PurchaseFacade(mainPage, cartPage, checkoutPage);
}
@AfterClass
public void afterClass() {
driver.quit();
}
@Test
public void purchaseFalcon9WithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
@Test
public void purchaseSaturnVWithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("[email protected]");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lotzowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+00498888999281");
1. Readability
Using facades, the automated tests are much shorter compared to the page
objects ones, which makes them a bit easier to understand. You can view the
data used in the test. However, if you want to know what the exact workflow
is, you will need to go inside the workflow method of the facade - you cannot
view it from the test itself. But this is usually a one-time effort. Here I give a
rating of (5) excellent.
Criteria Rating
Readability 5
Maintainability
Reusability
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
2. Maintainability
With this design, you have all the benefits of using page objects. Moreover, it
solves the only maintainability problem I see with page objects - extending
and fixing the overall test workflow. If you need to add something to the
workflow or fix something in it, you can do it in a single place - inside the
facade's action methods. Of course, there are no perfect solutions. You need
to be careful not to cause regression. But still, this is not enough of a reason
to decrease the rating. I give a rating excellent (5).
Criteria Rating
Readability 5
Maintainability 5
Reusability
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
3. Reusability
Everything said till now is valid for the reusability too. My rating here is
again 5. We reuse everything to the optimal maximum.
Criteria Rating
Readability 5
Maintainability 5
Reusability 5
API Usability
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
4. API Usability
The facades provide much fewer methods compared to the many page
objects. Moreover, you don't need to know the exact order of page objects
calls. It is now a hidden detail in the facade itself. The only thing you need to
know is the bare minimum of initializing the data correctly for the facade's
methods. API Usability is excellent (5).
Criteria Rating
Readability 5
Maintainability 5
Reusability 5
API Usability 5
Extensibility
Code Complexity Index
Learning Curve
Test Design Index
5. Extensibility
Everything we said about the page objects solution is valid for this design as
well, with the addition that we can extend the tests' workflow. Therefore the
rating is increased to 5 (excellent).
Criteria Rating
Readability 5
Maintainability 5
Reusability 5
API Usability 5
Extensibility 5
Code Complexity Index
Learning Curve
Test Design Index
6. Code Complexity Index
Here are the code metrics for this test design.
7. Learning Curve
I think it is even easier to learn how to write tests with facades compared to
page objects because you use a single API to write tests. You don't need to
know how many pages you have or all their methods. You need to learn how
to initialize the data objects supplied to facade's methods and which method
you should call first. However, the number of methods is much smaller
compared to other solutions. This is why I give a rating of (5) excellent.
Criteria Rating
Readability 5
Maintainability 5
Reusability 5
API Usability 5
Extensibility 5
Code Complexity Index 5
Learning Curve 5
Test Design Index
Test Design Index
Criteria Rating
Readability 5
Maintainability 5
Reusability 5
API Usability 5
Extensibility 5
Code Complexity Index 5
Learning Curve 5
Test Design Index 5
Test Design Index = (5 + 5 + 5 + 5 + 5 + 5 + 5) / 7 = 5
Final Assessment
Proposed Design Architecture TDI Rating
Tests without Page Objects 2.43
Tests with Page Objects 4.29
Tests with Facades 5
Summary
In the chapter, we discussed a test design assessment system, defined its
criteria, and explained how to apply it in practice. In the second section of the
chapter, we evaluated three of the designs we already used in the book - tests
without page objects, ones that used the pattern and at the end - tests that use
the Facade design pattern. The ratings we assigned pointed out that in our
use case the best choice of a design is the one that uses the Facade design
pattern.
In the next chapter, we will talk about benchmarking and how to measure the
performance of various automated test components.
Questions
1. Can you list the seven criteria of the assessment system?
2. What aspects of the code do you evaluate for the Reusability
criterion?
3. Why is the learning curve a critical evaluation point?
4. What is the Test Design Index?
5. How do you calculate code metrics in IntelliJ?
6. What steps do you need to follow to apply the assessment system in
your project?
Chapter 9. Benchmarking for
Assessing Automated Test
Components Performance
In the previous chapter, we looked into a system for evaluating different test
architecture designs. However, the evaluation of core quality attributes is not
enough to finally decide which implementation is better or not. The test
execution time should be a key component too. For example, which method
for searching in an HTML table is more performant - using vanilla
WebDriver or a hybrid approach using in-memory calculations? Which
button click approach is better - the Vanilla WebDriver one, or clicking
through JavaScript?
In this chapter, we will examine a library that can help us answer these and
many more questions. Also, you will read about how to use the
benchmarking tooling for exporting the results to various formats, profiling
your test components’ CPU, memory and hard drive utilization, and more. In
the end, we will integrate this library with our existing solution for reusing
the browser through the Observer design pattern.
What Is Benchmarking?
Before defining benchmarking, we need to answer the question - what is
performance?
Our goal is to create benchmarking tooling to measure the performance of
our components. Since there is more than one way to achieve the same result,
we won’t reinvent the wheel but, instead, use a standard solution.
Definition: Performance
In the context of automated testing, it can mean two things- certain
operations to take less time, e.g., run faster. A second interpretation is
reducing resource usage and allocations. Or said with other words- “doing
more with less”.
Here is the official definition for “performance efficiency” by ISTQB
Glossary:
“The degree to which a component or system uses time, resources and
capacity when accomplishing its designated functions.”[Isg 19]
Main Features
JMH has a lot of great features for in-depth performance investigations:
● Standard benchmarking routine - generating an isolation per each
benchmark method; auto-selection of iteration amount; warmup; overhead
evaluation; and so on.
● Execution control - JMH tries to choose the best possible way to evaluate
performance, but you can also manually control the number of iterations,
switch between cold start and warmed state, set the accuracy level, tune
JMH parameters, change environment variables, and more.
● Statistics - by default, you will see the essential statistics like mean and
standard deviation;
● Memory diagnostics - the library not only measures the performance of
your code, but also prints information about memory, traffic and the
amount of GC collections.
● Parametrization - performance can be evaluated for different sets of
input parameters- like in popular unit test frameworks
● Command-line support - you can run and configure micro benchmarks
from the command line
JMH Example
It's straightforward to start using JMH. Let's look at an example:
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StringBuilderBenchmark {
@Benchmark
public void testStringAdd() {
String a = "";
for (int i = 0; i < 10; i++) {
a += i;
}
print(a);
}
@Benchmark
public void testStringBuilderAdd() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
print(sb.toString());
}
Result "benchmark.string.StringBuilderBenchmark.testStringAdd":
9001.776 ±(99.9%) 253.496 ops/ms [Average]
(min, avg, max) = (8288.588, 9001.776, 9493.148), stdev = 291.926
CI (99.9%): [8748.280, 9255.272] (assumes normal distribution)
# Fork: 1 of 2
# Warmup Iteration 1: 27202.528 ops/ms
# Warmup Iteration 2: 26500.586 ops/ms
# Warmup Iteration 3: 27190.346 ops/ms
Iteration 1: 27891.257 ops/ms
Iteration 2: 28704.541 ops/ms
Iteration 3: 27785.951 ops/ms
Iteration 4: 26841.454 ops/ms
Iteration 5: 26024.288 ops/ms
Iteration 6: 25592.494 ops/ms
Iteration 7: 25626.875 ops/ms
Iteration 8: 25302.248 ops/ms
Iteration 9: 25519.780 ops/ms
Iteration 10: 25275.334 ops/ms
# Run progress: 75.00% complete, ETA 00:01:02
# Fork: 2 of 2
# Warmup Iteration 1: 30376.008 ops/ms
# Warmup Iteration 2: 25131.064 ops/ms
# Warmup Iteration 3: 25622.342 ops/ms
Iteration 1: 25386.845 ops/ms
Iteration 2: 25825.139 ops/ms
Iteration 3: 26029.607 ops/ms
Iteration 4: 25531.748 ops/ms
Iteration 5: 25374.934 ops/ms
Iteration 6: 25204.530 ops/ms
Iteration 7: 22934.211 ops/ms
Iteration 8: 23907.677 ops/ms
Iteration 9: 24337.963 ops/ms
Iteration 10: 24660.626 ops/ms
Result "benchmark.string.StringBuilderBenchmark.testStringBuilderAdd":
25687.875 ±(99.9%) 1167.955 ops/ms [Average]
(min, avg, max) = (22934.211, 25687.875, 28704.541), stdev = 1345.019
CI (99.9%): [24519.920, 26855.830] (assumes normal distribution)
# Run complete. Total time: 00:04:08
Benchmark Mode Cnt Score Error Units
StringBuilderBenchmark.testStringAdd thrpt 20 9001.776 ± 253.496 ops/ms
StringBuilderBenchmark.testStringBuilderAdd thrpt 20 25687.875 ± 1167.955 ops/ms
NOTE
The easiest way to get started with JMH is to generate a new JMH project
using the JMH Maven archetype. The JMH Maven archetype will create a
new Java project with a single benchmark Java class. The Maven pom.xml
file contains the correct dependencies to compile and build your JMH
microbenchmark suite.
mvn archetype:generate
-DinteractiveMode=false
-DarchetypeGroupId=org.openjdk.jmh
-DarchetypeArtifactId=jmh-java-benchmark-archetype
-DgroupId=com.jenkov
-DartifactId=first-benchmark
-Dversion=1.0
These are the Maven dependencies you need and will be installed
automatically for you by the archetype:
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>
</dependencies>
@Setup(Level.Iteration)
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.navigate().to(testPage);
}
@TearDown(Level.Iteration)
public void tearDown() {
driver.close();
}
@Benchmark
public void benchmarkWebDriverClick() {
var buttons = driver.findElements(By.xpath("//input[@value='Submit']"));
for (var button:buttons) {
button.click();
}
}
@Benchmark
public void benchmarkJavaScriptClick() {
JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
var buttons = driver.findElements(By.xpath("//input[@value='Submit']"));
for (var button:buttons) {
javascriptExecutor.executeScript("arguments[0].click();", button);
}
}
}
I created a simple console application containing the main method. Inside of
it, we configure the benchmark runner through the OptionsBuilder class. Instead
of using annotations on top of the class, you can use OptionsBuilder to
configure the runner. In our case, I specified the number of parallel threads
using the forks method. Thought annotations, we set that 3 iterations will be
used for warmup and 8 ones for measurement. We measure the average time
in milliseconds. The method marked with the @Setup annotation will be
called once for iterations since we configured it to do so via the argument
Level.Iteration . The same happens for the tearDown where we close the browser.
The two methods annotated with @Benchmark contains the code that we want
to compare. After you run the main program, the benchmarking process
begins, and at the end of it, the final score results are printed on the console.
Here are the results after the execution.
It looks like the vanilla WebDriver native click is much less performant than
the JavaScript approach. Mean 4960 ms against 1001 ms, which is almost a
395% increase in time!
JMH Profiling
JMH has a few very helpful profilers that aid in understanding your
benchmarks. While these profilers are not the substitute for full-fledged
external profilers, they can quickly dig into the benchmark behavior in many
cases. When you are doing many cycles of tuning up the benchmark code
itself, it is vital to have a quick turnaround for the results. There are quite a
few profilers, and this sample would expand on a handful of the most useful
ones.
StackProfiler
Stack profiler is useful to see if the code we are stressing executes quickly.
Like many other sampling profilers, it is susceptible to sampling bias: it can
overlook quickly running methods, for example.
public static void main(String[] args) {
Options opt = new OptionsBuilder()
.include(BenchmarkRunner.class.getSimpleName())
.addProfiler(WinPerfAsmProfiler.class)
.addProfiler(StackProfiler.class)
.addProfiler(GCProfiler.class)
.forks(1)
.build();
new Runner(opt).run();
}
After the execution, you get similar results:
....[Thread state: RUNNABLE]........................................................................
99.0% 99.0% org.openjdk.jmh.samples.JMHSample_35_Profilers$Maps.test
0.4% 0.4%
org.openjdk.jmh.samples.generated.JMHSample_35_Profilers_Maps_test.test_avgt_jmhStub
0.2% 0.2% sun.reflect.NativeMethodAccessorImpl.invoke0
0.2% 0.2% java.lang.Integer.valueOf
0.2% 0.2% sun.misc.Unsafe.compareAndSwapInt
....[Thread state: RUNNABLE]........................................................................
78.0% 78.0% java.util.TreeMap.getEntry
21.2% 21.2% org.openjdk.jmh.samples.JMHSample_35_Profilers$Maps.test
0.4% 0.4% java.lang.Integer.valueOf
0.2% 0.2% sun.reflect.NativeMethodAccessorImpl.invoke0
0.2% 0.2%
org.openjdk.jmh.samples.generated.JMHSample_35_Profilers_Maps_test.test_avgt_jmhStub
GC Profiler
To view the disassembly of your code, use the DisassemblyDiagnoser attribute.
public static void main(String[] args) {
Options opt = new OptionsBuilder()
.include(BenchmarkRunner.class.getSimpleName())
.addProfiler(WinPerfAsmProfiler.class)
.addProfiler(StackProfiler.class)
.addProfiler(GCProfiler.class)
.forks(1)
.build();
new Runner(opt).run();
}
After the execution, you get similar results:
There, we can see that the tests are producing quite some garbage. gc.alloc
would say we are allocating 28 MB of objects per second. gc.churn would say
that GC removes the same amount of garbage from Eden space every second.
WinPerfAsmProfiler Profiler
Dealing with microbenchmarks like these requires looking into the abyss of
runtime, hardware, and generated code. Luckily, JMH has a few handy tools
that ease the pain. If you are running Linux, then perf_events are probably
available as a standard package. This kernel facility taps into hardware
counters and provides the data for user space programs like JMH. Windows
has less sophisticated facilities but also usable.
new Runner(opt).run();
}
After the execution, you get similar results:
We can already see this benchmark goes with а good IPC, does lots of loads
and lots of stores, all of them are more or less fulfilled without misses.
Optimized Browser Initialization
Benchmark Integration
If you recall Chapter 3. Strategies for Speeding-up the Tests we wrote a
solution for reusing the browser through attributes using the Observer
design pattern. Let us investigate how we can achieve the same in our
benchmarks. Otherwise you won’t be able to benchmark your framework’s
code. We need to refactor our code since the first version of our solution was
using the TestNG framework to start the browser, but here we run the
experiments without it in a console application.
Here is how our button experiment will change after the refactoring -
producing the same results but controlling the browser through an attribute.
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
@Warmup(iterations = 0)
@Measurement(iterations = 10)
@ExecutionBrowser(browser = Browser.CHROME, browserBehavior =
BrowserBehavior.RESTART_EVERY_TIME)
public class BenchmarkRunner extends BaseBenchmark {
private final String TEST_PAGE = "https://bit.ly/3rSaqTZ";
new Runner(opt).run();
}
@Override
public void setup(PluginState pluginState) {
setCurrentClass(BenchmarkRunner.class);
super.setup(pluginState);
}
@Override
public void init(Driver driver) {
driver.goToUrl(TEST_PAGE);
}
@Benchmark
public void benchmarkWebDriverClick(PluginState pluginState) {
var buttons = PluginState.getDriver().findElements(By.xpath("//input[@value='Submit']"));
for (var button : buttons) {
button.click();
}
}
@Benchmark
public void benchmarkJavaScriptClick(PluginState pluginState) {
var buttons = pluginState.getDriver().findElements(By.xpath("//input[@value='Submit']"));
for (var button:buttons) {
pluginState.getDriver().executeScript("arguments[0].click();", button);
}
}
}
The new things are that we derive from the BaseBenchmark class and the usage
of the iteration setup and cleanup methods. In short, we created the same
plug-in architecture as for TestNG but now for JVM. Shall we examine all
updates?
@State(Scope.Thread)
public static class PluginState {
private static TestExecutionSubject _currentTestExecutionSubject;
private static Driver _driver;
@Setup(Level.Trial)
public void doSetup() {
_currentTestExecutionSubject = new ExecutionSubject();
_driver = new WebCoreDriver();
new BrowserLaunchTestBehaviorObserver(_currentTestExecutionSubject, _driver);
}
@TearDown(Level.Trial)
public void doTearDown() {
if (getDriver() != null) {
System.out.println("Do TearDown");
getDriver().quit();
}
}
@Setup(Level.Invocation)
public void setup(PluginState pluginState) {
PluginState.getCurrentTestExecutionSubject().preTestInit(currentClass);
init(PluginState.getDriver());
PluginState.getCurrentTestExecutionSubject().postTestInit(currentClass);
}
@TearDown(Level.Invocation)
public void tearDown(PluginState pluginState) {
PluginState.getCurrentTestExecutionSubject().preTestCleanup(currentClass);
cleanup(PluginState.getDriver());
PluginState.getCurrentTestExecutionSubject().postTestCleanup(currentClass);
}
Questions
1. Why is it important to measure the performance of our code?
2. What is benchmarking?
3. How can you start using JMH?
4. What types of results can you generate in JMH?
Chapter 10. Test Data Preparation
and Test Environments
Test data is a crucial part of automated tests. Building successful automation
is not only about good programming knowledge, using best practices, and
design patterns. It is not only about stable test locators or test environments
that you control. Of course, these are prerequisites, but in my opinion, the
essential thing that many teams miss is the importance of proper test data
preparation and its role in the success of any automated testing solution.
Therefore, we end with this chapter - because, without this fundamental
knowledge, you will be doomed to fix flaky tests until the rest of the days.
We will investigate strategies on how to handle test data in different test
environments using configuration files. After that, we will check how to
utilize code libraries for data generation. We will talk about using the DB and
APIs for creating and managing the necessary data.
The following topics will be covered in this chapter:
Stop hard-coding input data
Introducing test fixtures
Using an API as a source of fixture data
Using an API or DB for verification
URL of the website - like any web project, we have several testing
environments- DEV, STAGING, UAT (User Acceptance Testing),
and so on. Our tests have the URL of the application hardcoded. So,
without changing the test code, we cannot have the test run on both
the staging and dev environments.
Hard-coded product - different test environments do not share the
same identical data such as products. Furthermore, most
environments will only have a subset of the products available in
production. Test environments in particular will have products that
never did and never will exist in production.
User data - due to legal reasons, our test environment should never
contain user data from the production environment. This is doubly
true for sensitive user information, such as credit card numbers and
emails.
Our tests should be able to run in any environment we have. But this is not
possible if every single piece of data we use is hard-coded for a specific test
environment.
Configuration Transformations
How about implementing a code that reads data from a JSON configuration
file and later using it in our test? I think there are three major places for
which we can use such data: website URLs, WebDriver timeouts, and default
billing information. The main goal will be to have separate data sets for
different test environments such as LOCAL, DEV, or UAT.
First, we need to add a JSON config file to our project. Name
it testFrameworkSettings.json. We will have separate JSON files for each
environment, which will be changed by setting a particular environmental
variable during execution called environment. For example, dev value will
execute the tests against the local DEV environment. The qa value against
the QA environment in CI. This means that we will have two configuration
files: one will be named testFrameworkSettings.dev.json and
another testFrameworkSettings.qa.json.
NOTE
Continuous Integration (CI) is a development practice where developers
frequently integrate code into a shared repository, preferably several times
a day. An automated build and automated tests can then verify each
integration. While automated testing is not strictly part of CI, it is typically
implied. Popular CI tools are Jenkins, Azure DevOps, Bamboo, GitLab CI,
TeamCity, and many others.
{
"webSettings": {
"baseUrl": "http://demos.bellatrix.solutions/",
"elementWaitTimeout": "30",
"chrome": {
"pageLoadTimeout": "120",
"scriptTimeout": "5",
"artificialDelayBeforeAction": "0"
},
"firefox": {
"pageLoadTimeout": "30",
"scriptTimeout": "1",
"artificialDelayBeforeAction": "0"
},
"edge": {
"pageLoadTimeout": "30",
"scriptTimeout": "1",
"artificialDelayBeforeAction": "0"
},
"internetExplorer": {
"pageLoadTimeout": "30",
"scriptTimeout": "1",
"artificialDelayBeforeAction": "0"
},
"opera": {
"pageLoadTimeout": "30",
"scriptTimeout": "1",
"artificialDelayBeforeAction": "0"
},
"safari": {
"pageLoadTimeout": "30",
"scriptTimeout": "1",
"artificialDelayBeforeAction": "0"
}
},
"urlSettings": {
"shopUrl": "http://demos.bellatrix.solutions/cart/",
"accountUrl": "http://demos.bellatrix.solutions/account/"
},
"billingInfoDefaultValues": {
"email": "[email protected]",
"company": "Space Flowers",
"country": "Germany",
"firstName": "Anton",
"lastName": "Angelov",
"phone": "+00498888999281",
"zip": "10115",
"city": "Berlin",
"address1": "1 Willi Brandt Avenue Tiergarten",
"address2": "2 Willi Brandt Avenue Tiergarten"
}
}
This is the dev config which contains the values for the LOCAL
environment. In the qa file, you can change the values for particular fields if
there are differences.
There are two ways how you can set this environment variable. The first way
to do it is to insert it through IntelliJ. You can set it from Run | Edit
Configurations. As a key-value pair, set the desired environmental variables
in the Environment variables text field. You can configure these variables
from the other settings to be applied across the whole project, particular
module, or only a few packages.
NOTE
The project can contain one or more related modules. A project is a
convenient way to develop related, interdependent applications, libraries in
concert.
Each module is a separate library, application and can be a jar, ear, or war.
The modules aren't just Java, either. You can have modules for ruby, scala,
or something else as well. Module folders are subfolders of the project
folder.
An artifact is an assembly of your project assets that you put together to
test, deploy, or distribute your software solution or its part.
The second way is to set it from the command line if you run your tests in
continuous integration.
java -cp "pathClassFilesOrTestngJarFilePath" yourPackageName testng.xml -Denvironment=qa
Through the -D argument, we can set the environment variable.
We won’t use the native Java properties files support since it is too primitive
for our purposes. So, we need to create a new service for accessing the
configuration data. You need to add a new Maven dependency for working
with JSON files called com.google.code.gson.
For each separate config section such as urlSettings or webSettings, we
have a Java class representation.
This is the Java class representation for the web settings section.
public class WebSettings {
private String baseUrl;
private BrowserSettings chrome;
private BrowserSettings firefox;
private BrowserSettings edge;
private BrowserSettings opera;
private BrowserSettings internetExplorer;
private BrowserSettings safari;
private int elementWaitTimeout;
environment = p.getProperty("environment");
}
else {
environment = environmentOverride;
}
}
var jsonObject =
JsonParser.parseString(jsonFileContent).getAsJsonObject().get(sectionName).toString();
try {
mappedObject= gson.fromJson(jsonObject, configSection);
} catch (Exception e) {
e.printStackTrace();
}
return mappedObject;
}
@Override
protected String getUrl() {
return ConfigurationService.get(WebSettings.class).getBaseUrl();
}
@Override
protected void waitForPageLoad() {
elements().addToCartFalcon9().waitToExists();
}
@Override
protected String getUrl() {
return UrlDeterminer.getShopUrl("cart");
}
// rest of the code
}
public PurchaseInfo() {
var billingInfoDefaultValues = ConfigurationService.get(BillingInfoDefaultValues.class);
this.firstName = billingInfoDefaultValues.getFirstName();
this.lastName = billingInfoDefaultValues.getLastName();
this.company = billingInfoDefaultValues.getCompany();
this.country = billingInfoDefaultValues.getCountry();
this.address1 = billingInfoDefaultValues.getAddress1();
this.address2 = billingInfoDefaultValues.getAddress2();
this.city = billingInfoDefaultValues.getCity();
this.zip = billingInfoDefaultValues.getZip();
this.phone = billingInfoDefaultValues.getPhone();
this.email = billingInfoDefaultValues.getEmail();
this.shouldCreateAccount = true;
this.shouldCheckPayment = true;
}
// rest of the code
}
The usage in tests is straightforward.
@Test
public void purchaseFalcon9WithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseFacade.verifyItemPurchase("Falcon 9", "happybirthday", 2, "114.00€", purchaseInfo);
}
NOTE
POJO is an acronym for Plain Old Java Object. It is initially introduced to
designate a simple, lightweight Java object without implementing any
javax.ejb interface, as opposed to heavyweight EJB 2.x (especially Entity
Beans, Stateless Session Beans are not that bad IMO). Today, the term is
used for any simple object with no extra stuff. It was coined by Martin
Fowler, Rebecca Parsons, and Josh MacKenzie in September 2000.
Browser(String value){
this.value = value;
}
@Override
public String toString() {
return value;
}
NOTE
A significant new feature in JDK 8 is the stream functionality –
java.util.stream . It contains classes for processing sequences of elements. A
stream is not a data structure. Instead, it takes input from the Collections,
Arrays, or I/O channels. Streams don't change the original data structure but
rather only provide the result as per the pipelined methods. Each
intermediate operation is lazily executed and returns a stream. As a result,
the various intermediate operations can be pipelined. Terminal operations
mark the end of the stream and yield the outcome. Collections are mostly
about storing and accessing data, whereas Streams are mostly about
describing computations on data. Stream API also simplifies multithreading
by providing the parallelStream method that runs operations over the
stream's elements in parallel mode.
For example we can rewrite preTestInit method part of the ExecutionSubject class
as follows:
@Override
public void preTestInit(ITestResult result, Method memberInfo) {
testBehaviorObservers.forEach(o -> o.preTestInit(result, memberInfo));
}
Later we can use the new code in the browser launch observer as follows:
var defaultBrowser =
Browser.fromText(ConfigurationService.get(WebSettings.class).getDefaultBrowser());
Environmental Variables
Sometimes there is information such as credentials that it is not appropriate to
be persisted even in configuration files. Usually, such is the case for
credentials for accessing Selenium Grid cloud providers. One way is to put
such info in environment variables and later read them from the tests.
Here is an example for login into the website using environment variables.
private void loginWithEnvironmentalVariables(string userName) {
var userNameTextField = driver.findElement(By.id("username"));
userNameTextField.typeText(userName);
var passwordField = driver.findElement(By.id("password"));
string userNamePass = System.getenv(String.format("%s_pass", userName));
passwordField.typeText(userNamePass );
var loginButton = driver.findElement(By.xpath("//button[@name='login']"));
loginButton.Click();
}
The environmental variable should be named aangelov_pass if it will hold the
password for the user aangelov .
@Test
public void purchaseSaturnVWithFacade() {
var purchaseInfo = new PurchaseInfo();
purchaseFacade.verifyItemPurchase("Saturn V", "happybirthday", 3, "355.00€", purchaseInfo);
}
@DataProvider
public Object[][] getPurchaseInfoData(){
Object[][] data = new Object[2][4];
data[0][0] = "Falcon 9";
data[0][1] = "happybirthday";
data[0][2] = 2;
data[0][3] = "114.00€";
return data;
}
@BeforeMethod
public void testInit() {
WebDriverManager.chromedriver().setup();
proxyServer = new BrowserMobProxyServer();
proxyServer.start();
proxyServer.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,
CaptureType.RESPONSE_CONTENT);
proxyServer.newHar();
String proxyDetails = "127.0.0.1:" + proxyServer.getPort();
final Proxy proxyConfig = new Proxy().
setHttpProxy(proxyDetails).
setSslProxy(proxyDetails);
@AfterMethod
public void testCleanup() {
driver.quit();
proxyServer.abort();
}
@Test
public void requestRedirected_when_usingProxyRedirect() {
proxyServer.rewriteUrl("https://secure.gravatar.com/js/gprofiles.js?ver=2021Junaa",
"https://stub.gravatar.com/js/gprofiles.js?ver=2021Junaa");
driver.navigate().to("http://demos.bellatrix.solutions/");
}
}
Again, we use BrowserMob web proxy. We set the URLs to be redirected
before the tests. We do that through the rewriteUrl method. In the example,
when the page is loaded, the gravatar service requests are redirected to our
local stub implementation of the service. When a request against the original
URL is made, BrowserMob redirects it to the stub version.
Using an API or DB for Verification
Modern web automated tests are doing much more than just testing the UI.
As you saw in the previous section, we go more and more into using DB or
APIs for test data generation. The same is valid for assertion mechanisms.
You can verify only a few things directly through the UI. There are many
things usually that happen underneath - hidden from users' eyes. This is why
we can leverage the existing test data generation infrastructure to verify those
aspects too. This is why I like to call these kinds of tests - system tests or
system of system tests instead of the generic term UI test. Any test that uses
API or DB to set up or verify data, for me is a system test.
Here is an example of how we can use the UsersRepository to verify whether a
user was created successfully from the registration form of the website.
@Test
public void newUserCreatedSuccessfully_whenUsedNewRegistrationForm() {
// create a new user through the UI form.
// ...
// verify the new user in DB.
var actuallyCreatedUser = usersRepository.getUserByEmail("[email protected]");
Assert.assertEquals(actuallyCreatedUser.getFirstName(), "Anton");
Assert.assertEquals(actuallyCreatedUser.getLastName(), "Angelov");
Assert.assertEquals(actuallyCreatedUser.getPassword(), "superSecret");
}
Summary
In this last chapter of the book, we investigated ways to make our tests much
more stable across test environments' executions. Instead of hard-coding the
test data, we used JSON configuration files for each particular environment.
We saw examples of configuring the websites' URLs, WebDriver timeouts,
and billing default values. Also, we talked about the importance of using data
stubs in the application under test.
There are three appendix chapters. The first one is discussing the problems
that automated testing solutions must solve. It is a summary of several topics
we discussed across the book. The last two chapters contain a comprehensive
list of the various CSS and XPath selectors. The reason for including them
here is that I believe that knowing all of them and using them in your tests,
we will improve the stability and readability of your automated tests
significantly.
Questions
1. Which dependencies do you need in order to use the JSON utilities?
2. How do you generate new values using the Faker library?
3. Why the hard-coding of test data leads to flaky tests?
Thank you for reaching the end of the book!
As a bonus, you can find video recordings with explanations for each chapter.
To get them, you can join for free the book's LinkedIn group. There you can
find even more info about design patterns in automated testing and best
practices or use it as an easy way to reach me. Before joining, you need to
provide proof that you purchased the book. Just go to https://bit.ly/3eGTAUl
If you want to discuss something from the book or have a short question, I
am always glad to connect on LinkedIn and chat. In case you need more in-
depth assistance or desire for even more in-depth knowledge, I lead C# and
Java training series regarding automated testing (web, desktop, mobile, or
API). Additionally, we offer consulting, mentoring, and professional services.
You can always find me in the LinkedIn group or connect directly.
https://bit.ly/2NjWJ19
Now the bonus three Appendix chapters!
Appendix 1. Defining the Primary
Problems that Test Automation
Frameworks Solve
To list the various benefits of test automation frameworks I must say that
they naturally derive and extend the multiple benefits that come from test
automation as a whole. Of course, before talking about their advantages we
need to define what are the problems they try to solve. Like most things in
business and software development- there is a constant battle for resources-
money and time. People often ignore these costs because they are not the
ones paying the bills and receive their checks every month no matter what.
To define what is needed to deliver high-quality software, we have to
understand what the problems are in the first place.
Subsequent Problems
SP-1: Automated Tests Are Not Stable
Many teams face this issue. Their tests are not correctly written, and because
of that, they fail randomly. Or even worse- providing false-positive or false-
negative results.
NOTE
False-positive - a test result in which a defect is reported although no such
defect actually exists in the test object. False-negative - a test result which
fails to identify the presence of a defect that is actually present in the test
object.
Subsequent Problems
SP-3: Initial Investment Creating Shared Test Libraries
Sometimes to create a generic solution that can be generalized for testing
many scenarios and many test cases takes lots of effort. Sometimes you may
not have the right people to create the library, since significant coding and
design skills may be required. Or even if you have these engineers, they may
need to work on something else. It may be a significant time and money
investment to code it right.
SP-4: Are Automated Tests Trustworthy? Test the Tests?
Even if you have the time to create these libraries. How do you make sure
that they work correctly? It is code, so it may contain bugs. Do you spare
time to develop tests for the tests or accept the risk- the checks not working
correctly? Do you go to the next test once you see that your assertion has
passed? Did you check that it can actually fail?
SP-5: Library Modifications
Creating the initial version of the library may be easy. But it is a different
thing whether it is designed in a way to support future modifications.
Changes in the requirements and applications are something natural, and with
them, our tests should evolve. Many IT professionals forgot that, and in many
cases, it is easier for them to rewrite the tests, instead of making a little tuning
because the code may not support that.
SP-6: Library Customization- Another Team's Context?
The whole point of creating a shared library is to be used by multiple teams
across the company. However, the different teams work in a different context.
They may have to test a little bit different thing. So, the library code may not
be working out of the box for them. How easy will it be for them to extend or
customise it? Is it even possible to do it? Will they need to invest time to
create the same test feature again with the required modifications?
SP-7: Knowledge Transfer
Shared libraries are great, but do the engineers across all teams know whether
they have a particular feature of your framework or not? Do they know that
you already created some automated checks?
If they don't know that something exists, they will create it again. This is
especially true for new team members. If you were a part of the team building
the test features, then you know most of them. However, when you have just
joined the team if you don't have a way to find this information, someone has
to teach you. Otherwise, you will face the problems we mentioned.
SP-8: Test API Usability
Is it easy for the users to understand how to use the library? Even if people
know that something is there but don't know how to use or make it work, this
will result in creating the same thing again.
If the API is not concise and it is confusing, then you will need to invest time
to teach the new team members one by one how to utilize it.
Subsequent Problems
SP-9: Test Speed
As mentioned, the automated tests are fast. However, this is not always the
case. If the tests are not written properly, they can get quite slow. Some
engineers tend to use big pauses in their automation to handle various
challenges in automating web or mobile apps. Another possible scenario
where tests can get quite slow is when you put retry logic for various failing
statements till the test succeeds. Or just retry all failing tests multiple times.
Subsequent Problems
SP-9: Test Speed
SP-10: CI and CD Readiness
Some older automation solutions are tough to be executed from the command
line interface- meaning it is hard to integrate them into your continuous
integration or continuous deployment tools. Whatever tool or framework you
use, it now should allow being integrated relatively easy in CI systems. The
same is valid for produced test results by the tool or framework. Usually,
after CI execution you want to publish them somewhere.
Sub-Problem 6 - Ping-pong
With a dedicated QA team, there was the so-called ping-pong game of
throwing bugs. How does this happen?
1. The developer produces a new version of the app and deploys it without
any testing.
2. The QA starts testing when he gets free (this may not happen
immediately). Testing begins 4 hours after the new version.
3. The QA logs a bug 2 hours later. However, the developer may head home.
4. The developer fixes the bug early in the morning and deploys a new
version without testing it. This takes 1 hour.
5. The QA starts testing 3 hours later and finds that the first bug got fixed, but
another regression one appeared.
6. The QA logs the regression bug 30 minutes later.
7. The developer is still there but started to work on a new feature, so
promises to fix the new bug tomorrow.
Automation Solution Hypothesis
ASH-4: Execute All Tests before Deploying App
ASH-5: Developers Execute Automated Tests Locally
So, for a single feature to be tested and released you needed from 2-3 days. If
you have automated tests, the developer could execute them on his/her
machine or at least the most important ones. Even if this is not possible,
nowadays the most critical tests are performed right after the code is
deployed even if we are talking about UI/System tests. Which means that if
any regression bugs appear, you are going to catch them within 1 hour after
the code's submission.
Subsequent Problems
SP-9: Test Speed
SP-10: CI and CD Readiness
SP-11: Easiness of Local Test Solution Setup
How easy is it for a new colleague to set up everything needed in order to run
the automated tests locally? Do you have instructions? How much time will
he/she need? There are some cases where a man needs half a day to install
and configure everything required. Which is a bummer since we
programmers tend to be lazy most of the time or have so many other things to
code. So, many people will prefer to skip the process and just not follow it
because there are too many steps.
SP-12: Easiness of Local Test Execution
If you have lots of tests most probably the developer won't be able to execute
all of the tests before check-in his/her code.
How easy is it to locate which tests he/she needs to execute?
When the tests are executed, is it possible to continue working or should go
to drink a coffee? Some automation frameworks make it impossible to work
during the test run. Browsers appear on top of all other programs, tests get the
mouse focus, move your mouse or make real typing with your keyboard.
Is it possible for the tests to be executed locally but in an isolated
environment like Docker?
Can the tests be executed smoothly against the developer's local environment
instead of against the shared test environment?
Subsequent Problems
SP-13: Cross-technology and Cross-platform Readiness
Is it possible to execute the tests with no code modifications on various
browsers? Are the tests behaving the same way in different browsers? For
example, if you use a pure WebDriver code, it will be almost impossible.
How much code do you have to write to change the size of the browser or
change the mobile device on which the tests will be executed? In most cases,
it is better to skip the large configuration boilerplate code.
SP-14: Cloud Readiness
Some companies have their own farms of devices and computers with various
browser configurations. However, nowadays the cloud providers such as
SauceLabs, BrowserStack or CrossBrowserTesting are a reasonable solution
to the problem. Is it possible for your tests to be executed there? How much
code is needed to switch from cloud to local tests execution?
SP-15: Docker Support
Another alternative of the cloud providers is to run your tests in Docker
containers. However, the setup can be hard. Is it possible for your automation
solution to be integrated easily with Docker (Selenoid, Selenium Docker)?
Does it provide pre-created configured images with all browser versions or
mobile emulators?
SP-16: Complex Code Configuration
It is one thing your tool or framework to support various browsers, devices,
cloud or Docker integrations. But it is entirely another how much code you
need to write to make it happen. It is not only about the initial creation but
also if the requirements change, how easy is it to reconfigure the tests to use
another configuration?
Subsequent Problems
SP-1: Automated Tests Are Not Stable
SP-4: Are Automated Tests Trustworthy? Test the Tests?
SP-17: Test Speed Creation
Time spent on maintaining the existing tests is essential. It can take
significant time from the capacity of the QA team. However, for people to be
motivated, it should be relatively easy to create new tests. For sure, QA
engineers will be frustrated if for the whole sprint they can produce only a
few tests. It shouldn't be rocket science to create a simple test. Or even if the
scenario is more complicated, it shouldn't be so hard to understand what
needs to be done to automate it.
SP-18: Troubleshooting, Debuggability, Fixing Failing Tests
Easiness
As we mentioned, a big part of maintainability is troubleshooting existing
tests. Most in-house solutions or open-source ones don't provide lots of
features to make your life easier. This can be one of the most time-consuming
tasks. Having 100 failing tests and finding out whether there is a problem
with the test or a bug in the application. If you use plugins or complicated
design patterns, the debugging of the tests will be much harder, requiring lots
of resources and expertise. Even if you spot the problem, how easy is it to fix
it? Do you fix the code only in one place to fix multiple tests? In case the
library didn't reuse most of the logic but for example copy-paste it, then the
fixing will take much more time. If you use a 3rd party framework (open-
source or commercial one), is its support fast and helpful?
Sub-Problem 10 - Questioned
Professionalism
Every QA wants to be a professional and be recognized for the job done well.
However, as mentioned if you have tight deadlines, management pressure to
test and release the app as soon as possible, you cut out of the scope of your
testing by executing only the most important test cases. Anyhow, most of the
time when a new bug is found on Production, QAs feel responsible that they
didn't catch it. In some cases, developers are not aware that this is not our
fault and we had to cut out of the scope. So, they start to question how good
we are. Even in some edge cases people get fired or don't get bonuses.
Automation Solution Hypothesis
ASH-4: Execute All Tests before Deploying App
ASH-5: Developers Execute Automated Tests Locally
ASH-8: Test Automation Novelty
ASH-9: More Time Writing New Tests- Exploratory Testing
With time you will have more and more automated tests, checking for
regression bugs. This will reduce the time for performing the same tests over
and over again. Also, the locating-fixing bug cycle will be drastically
shortened, since the developers will be able to execute the tests locally or all
tests will run before deploying the app. The QA team will be more motivated
since it will execute more exciting tasks- thinking how to automate more
complicated and challenging scenarios. Moreover, we will be able to spend
more time experimenting- using manual testing techniques such as
exploratory testing to locate new bugs.
Subsequent Problems
SP-1: Automated Tests Are Not Stable
SP-4: Are Automated Tests Trustworthy? Test the Tests?
SP-17: Test Speed Creation
SP-19: Upgradability
Fixing unstable tests is not the only time-consuming task of having an in-
house automation framework. Every two weeks new browser versions are
released. With each of them, a new version of the low-level automation
libraries is released- WebDriver, Appium, WinAppDriver. However, since
these libraries and tools are open-source, nobody can guarantee that they will
be bug-free or backward compatible with everything you have. From my own
experience, I can assure you that this task takes at least 3-4 hours per week if
no significant problems appear. If a problem occurs, it can take much more
time. This is especially true if you need to support all browsers and a couple
of their versions (not only the last one). The same is even more valid for
mobile automation low-level libraries since there is an unlimited number of
devices and configurations.
Because of these problems, many teams don't upgrade so often to spare some
time. However, not testing on the latest versions of the browsers hide many
risks of not locating regression bugs.
Subsequent Problems
SP-7: Knowledge Transfer
SP-8: Test API Usability
SP-20: Test Code Readability
SP-21: Test Results, Report, Logs
Subsequent Problems
SP-5: Library Modifications
SP-17: Test Speed Creation
SP-18: Troubleshooting, Debuggability, Easiness Fixing
Failing Tests
SP-22: Learning Curve
Is it easy to figure out how to create these complex performance or load tests?
Do you need to read huge documentations (if they even exist) or you can
figure out everything from the demo example or the public API comments
while typing in the IDE? How much time does a new team member need to
learn to write and maintain these tests?
Do you need to spend countless hours passing your knowledge to him/her, or
the authors of the framework give you better alternatives?
Sub-Problem 14 – Consistency
If there is no documentation on how to write automated tests, different people
might write them in different ways. For example, one may use page object
models. Another may use vanilla WebDriver directly, and so on. The same is
valid for the naming of methods, elements, and tests. These inconsistencies
lead to hard to understand/read/maintain test code.
Subsequent Problems
SP-14: Cloud Readiness
SP-15: Docker Support
SP-24: Parallel and Distributed Test Execution
Does your framework support your automated tests to be executed in
parallel? Is there a runner for your tests that can run them in parallel or
distribute them across multiple machines?
Summary
As you can see, there are lots of reasons why test automation is necessary.
However, like all things that evolve, first generations of test automation tools
were not as good as promised. This is why test automation frameworks
started to be a much more popular solution. However, creating stable, fast
and easily maintained tests with them requires lots of upfront planning and
thought. Many more subsequent problems occur that tool and framework
vendors don't mention on their marketing pages. I believe that if you are
aware of all these problems you will be able to make much more educated
choices about where to invest or not.
Appendix 2. Most Exhaustive CSS
Selectors Cheat Sheet
A big part of the job of writing maintainable and stable web automation is
related to finding the proper element's selectors. Here will look into a
comprehensive list of CSS selectors.
Element Selectors
Selector Explanation
ul#myId <ul> element with @id= 'myId'
#myUniqueId any element with @id='myId'
ul.myForm <ul> element with @class = 'myForm'
.myForm.front any element with @classes = 'myfor' and
'front'
Contextual Selectors
Selector Explanation
ul#myUniqueId > li direct child element
ul#myUniqueId li sub child element
div > p - all <p> elements that are a direct descendant of a
<div> element
div + p - all <p> elements that are the next sibling of a <div>
element (i.e. placed directly after)
div ~p - all <p> elements that follow, and are siblings of <div>
elements
form myForm.front + ul next sibling
div.row * selects all elements that are descendant (or
child) of the elements with div tag and 'row'
class
Attribute Selectors
Selector Explanation
ul[name = <ul> element with attributes @name
"automateName"][style = =‘automateName’ and @style= 'style name'
"style_name"]
ul[@id] elements with @id attribute
ul[id = "myId"] <ul > element with @id='myId'
*[name='N'][value= 'v '] elements with name N and specified value 'v'
ul[id ^= "my"] all elements with an attribute beginning with
'my'
ul[id$= "Id"] all elements with an attribute ending with 'Id'
ul[id *= "unique"] all elements with an attribute containing the
substring 'unique'
ul[id ~= “unique"] all elements with an attribute containing the
word 'unique'
a[href='url'] anchor with target link 'url'
Useful n Values
● odd or 2n+1 - every odd child or element
● even or 2n - every even child or element
● n - every nth child or element
● 3n - every third child or element (3, 6, 9, ...)
● 3n+1 - every third child or element starting with 1 (1, 4, 7, ...)
● n+6 - all but first five children or elements (6, 7, 8, ...)
● -n+5 - only first five children or elements (1, 2, ..., 5)
Further Reading
Download CSS Selectors Cheat Sheet PDF - https://bit.ly/3rUFJxM
Appendix 3. Most Exhaustive XPath
Selectors Cheat Sheet
The other types of very useful selectors are the XPath ones. Knowing them in
detail can help you significantly improve the stability and the readability of
your tests.
Contextual Selectors
Selector Explanation
//img image element
//img/*[1] first child of element img
//ul/child::li first child 'li' of 'ul'
//img[1] first img child
//img/*[last()] last child of element img
//img[last()] last img child
//img[last()-1] second last img child
//ul[*] 'ul' that has children
Attribute Selectors
Selector Explanation
//img[@id='myId'] image element with @id= 'myId'
//img[@id!='myId'] image elements with @id not equal to
'myId'
//img[@name] image elements that have name attribute
//*[contains(@id, 'Id')] element with @id containing
//*[starts-with(@id, 'Id')] element with @id starting with
//*[ends-with(@id, 'Id')] element with @id ending with
//*[matches(@id, 'r')] element with @id matching regex ‘r’
//*[@name='myName'] image element with @name= 'myName'
//*[@id='X' or @name='X'] element with @id X or a name X
//*[@name="N"][@value="v"] element with @name N & specified
@value ‘v’
//*[@name="N" and element with @name N & specified
@value="v"] @value ‘v’
//*[@name="N" and element with @name N & not specified
not(@value="v")] @value ‘v’
//input[@type="submit"] input of type submit
//a[@href="url"] anchor with target link 'url'
//section[//h1[@id='hi']] returns <section> if it has an <h1>
descendant with @id= 'hi'
//* cell by row and column
[@id="TestTable"]//tr[3]//td[2]
//input[@checked] checkbox (or radio button) that is
checked
//a[@disabled] all 'a' elements that are disabled
//a[@price > 2.50] 'a' with price > 2.5
XPath Methods
Selector Explanation
//table[count(tr) > 1] return table with more than 1 row
//*[.="t"] element containing text 't' exactly
//a[contains(text(), "Log anchor with inner text containing 'Log Out'
Out")]
//a[not(contains(text(), anchor with inner text not containing 'Log Out'
"Log Out"))]
//a[not(@disabled)] all 'a' elements that are not disabled
Axis Navigation
Selector Explanation
//td[preceding- cell immediately following cell containing 't'
sibling::td="t"] exactly
//td[preceding- cell immediately following cell containing 't'
sibling::td[contains(.,"t")]]
//input/following-sibling::a 'a' following some sibling 'input'
//a/following-sibling::* sibling element immediately following 'a'
//input/preceding-sibling::a 'a' preceding some sibling 'input'
//input/preceding-sibling::* sibling element immediately preceding
[1] 'input'
//img[@id='MyId']::parent/* the parent of image with id
Math Methods
Selector Explanation
ceiling(number) evaluates a decimal number and returns the
smallest integer greater than or equal to the
decimal number
floor(number) evaluates a decimal number and returns the
largest integer less than or equal to the decimal
number
round(decimal) returns a number that is the nearest integer to
the given number
sum(node-set) returns a number that is the sum of the numeric
values of each node in a given node-set
String Methods
Selector Explanation
contains(space-string, determines whether the first argument string
planet-string) contains the second argument string and
returns boolean true or false
concat(string1, string2 concatenates two or more strings and returns
[string]*) the resulting string
normalize-space(string) strips leading and trailing white-space from a
string, replaces sequences of whitespace
characters by a single space, and returns the
resulting string
starts-with(spacetrack, checks whether the first string starts with the
space) second string and returns true or false
string-length([string]) returns a number equal to the number of
characters in a given string
substring(string, start returns a part of a given string
[length])
substring- returns a string that is the rest of a given string
after(spacetrack, track) after a given substring
Further Reading
Download XPath Selectors Cheat Sheet PDF - https://bit.ly/3rSlzEi
Bibliography
[Sgt 18] “Standard Glossary of Terms used in Software Testing Version 3.2”,
ISTQB, Feb 2018
[Sdp 14] “Selenium Design Patterns and Best Practices”, Dima Kovalenko,
Sep 2014
[Isg 19] “ISTQB Glossary”, International Software Testing Qualification
Board, Dec 2019
[Rid 18] “Refactoring: Improving the Design of Existing Code”, Martin
Fowler, Nov 2018
[Caa 17] “Clean Architecture: A Craftsman's Guide to Software Structure and
Design”, Robert C. Martin, Sep 2017
[Cgt 18] “Complete Guide to Test Automation: Techniques, Practices, and
Patterns for Building and Maintaining Effective Software Projects”, Arnon
Axelrod, Sep 2018
[Hfd 04] “Head First Design Patterns: A Brain-Friendly Guide”, Eric
Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates, Oct 2004
[Sfd 18] “Selenium Framework Design in Data-Driven Testing: Build data-
driven test frameworks using Selenium WebDriver, AppiumDriver, Java, and
TestNG”, Carl Cocchiaro, Jan 2018
[Fdg 08] “Framework Design Guidelines: Conventions, Idioms, and Patterns
for Reusable .NET Libraries (Microsoft Windows Development Series), 2nd
Edition”, Krzysztof Cwalina, Oct 2008
[Dpe 94] “Design Patterns: Elements of Reusable Object-Oriented Software”,
John Vlissides, Richard Helm, Ralph Johnson, Erich Gamma, Nov 1994
[Kia 18] “Kotlin in Action”, Dmitry Jemerov, Svetlana Isakova, Feb 2017
[Mja 18] “Modern Java in Action”, Raoul-Gabriel Urma, Mario Fusco, Alan
Mycroft, Sep 2018
[Jms 13] “The Java Module System”, Nicolai Parlog, Jun 2019
[Wcc 14] “What Is Clean Code and Why Should You Care?”, Carl Vuorinen,
Apr 2014