1. Dependencies
WebTester generally handles its dependencies by relying on the host project to provide them in the version it wants them as.
This means, as an example, that even though you declare a dependency on
info.novatec.testit:webtester-support-assertj3
you will not inherit assertj
automatically.
For a base setup of WebTester you could declare the following dependencies:
<dependencies>
<dependency>
<groupId>info.novatec.testit</groupId>
<artifactId>webtester-core</artifactId>
</dependency>
<dependency>
<groupId>info.novatec.testit</groupId>
<artifactId>webtester-support-assertj3</artifactId>
</dependency>
<dependency>
<groupId>info.novatec.testit</groupId>
<artifactId>webtester-support-junit5</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-support</artifactId>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
2. Browser Abstraction
WebTester provides an abstraction layer on top of Selenium’s WebDriver called
(rather fittingly) Browser
. There are several important interfaces related to
browsers.
2.1. Browser
A Browser
provides a streamlined and context centric API for the interaction
with a web browser. It is the main entry point to the framework.
2.2. BrowserBuilder
A BrowserBuilder
provides a builder API for initializing Browser
instances
and setting custom service implementations like the Configuration
.
2.3. WebDriverBrowser
The WebDriverBrowser
class implements Browser
and is used to wrap a Selenium
WebDriver
. Instances can be created by using the WebDriverBrowser’s
factory
methods:
-
WebDriverBrowser.forWebDriver(webDriver).build();
-
WebDriverBrowser.buildForWebDriver(webDriver);
Both of these are equal. The first method can be used to customize same aspects of the browser before it is build.
Examples
// Initialization of a new WebDriverBrowser instance
WebDriver webDriver = createWebDriver();
Browser browser = WebDriverBrowser.buildForWebDriver(webDriver);
Browser browser = WebDriverBrowser.forWebDriver(webDriver).build();
Browser browser = new WebDriverBrowserBuilder(webDriver).build();
// Initialization of a new WebDriverBrowser instance with custom service implementations
Configuration config = createConfiguration();
PageObjectFactory factory = createFactory();
Browser browser = WebDriverBrowser.forWebDriver(webDriver)
.withConfiguration(config)
.withFactory(factory)
.build();
Browser browser = new WebDriverBrowserBuilder(webDriver)
.withConfiguration(config)
.withFactory(factory)
.build();
3. Creating Browser Instances
3.1. BrowserFactory
A BrowserFactory
creates an abstraction over the Browser
initialization
based on project-global settings. They are intended to allow easy browser
initialization and encapsulation of the underlying configuration /
initialization processes. Most projects implement their own factories according
to their specific environment. In case you just want to get started, we provide
factories for the most common browsers:
-
ChromeFactory
-
EdgeFactory
-
FirefoxFactory
-
MarionetteFactory
(new Firefox Geckodriver) -
OperaFactory
-
InternetExplorerFactory
-
RemoteFactory
All of these are provided by the webtester-core
module, but need you to
provide the corresponding Selenium WebDriver
dependencies yourself.
3.2. ProxyConfiguration
In order to configure a proxy you can either configure it manually when
initializing the WebDriver
or you can implement a ProxyConfiguration
and
provide it to the BrowserFactory
before creating a Browser
instance.
ProxyConfiguration pc = createProxyConfiguration();
Browser browser = new FirefoxFactory().withProxyConfiguration(pc).createBrowser();
3.3. Provided Factories
The webtester-core
modules provides a number of BrowserFactory
implementations out of the box.
3.3.1. ChromeFactory
This BrowserFactory
uses the selenium-chrome-driver
to create new Chrome
Browser
instances.
Default Driver Configuration
In order to optimize testing the following properties are set when creating a
WebDriver
using the ChromeFactory
:
-
Native events are disabled → Selenium does not simulate human typing.
-
Untrusted certificates are always accepted.
Additional Service Executable
The ChromeDriver
needs an additional executable to communicate with a Chrome
browser. It can be downloaded
here. The
path to the executable must be declared as a system or environment property
named: webdriver.chrome.driver
You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.
Additional Information:
3.3.2. EdgeFactory
This BrowserFactory
uses the selenium-edge-driver
to create new Edge
Browser
instances.
Default Driver Configuration
In order to optimize testing the following properties are set when creating a
WebDriver
using the EdgeFactory
:
-
Native events are disabled → Selenium does not simulate human typing.
-
Untrusted certificates are always accepted.
Additional Service Executable
The EdgeDriver
needs an additional executable to communicate with an Edge
browser. It can be downloaded
here.
Please make sure to choose the release version equal to your Windows 10 build.
The path to the executable must be declared as a system or environment property
named: webdriver.edge.driver
You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.
3.3.3. FirefoxFactory and MarionetteFactory
These BrowserFactory
implementations use the selenium-firefox-driver
to
create new Firefox Browser
instances. To drive Firefox browsers up to version
46, the FirefoxFactory
can be used. In order to drive newer versions (47++),
the MarionetteFactory
must be used. The only real difference between these
two factories is the activation of the marionnette
capability, but sadly in doing so
you will need to provide the location of a GeckoDriver
installation.
Default Driver Configuration
In order to optimize testing the following properties are set when creating a
WebDriver
using the FirefoxFactory
:
-
Native events are disabled → Selenium does not simulate human typing.
-
Untrusted certificates are always accepted.
Additional Service Executable
Using the Marionette-activated WebDriver
will force you to also specify the
location of a GeckoDriver
instance. This is basically a proxy between Selenium
and the actual Firefox (like with the ChromeDriver
). it can be downloaded
here The path to the
executable must be declared as a system or environment property named:
webdriver.gecko.driver
You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.
Additional Information:
3.3.4. InternetExplorerFactory
This BrowserFactory
uses the selenium-ie-driver
to create new Internet
Explorer Browser
instances.
Default Driver Configuration
In order to optimize testing the following properties are set when creating a
WebDriver
using the InternetExplorerFactory
:
-
Native events are disabled → Selenium does not simulate human typing.
-
Untrusted certificates are always accepted.
Additional Service Executable
The InternetExplorerDriver
needs an additional executable to communicate with
a IE browser. It can be downloaded
here. The path
to the executable must be declared as a system or environment property named:
webdriver.ie.driver
You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.
Additional Information:
3.3.5. RemoteFactory
This BrowserFactory
uses the RemoteWebDriver
to connect to a
Selenium Grid.
Default Driver Configuration
In order to optimize testing the following properties are set when creating a
WebDriver
using the RemoteFactory
:
-
Native events are disabled → Selenium does not simulate human typing.
-
Untrusted certificates are always accepted.
-
Selenium Grid Host:
localhost:4444
-
Default Browser:
firefox
with Marionette activated
The connection to the Selenium Grid can be configured in two ways:
-
Set properties in configuration file.
-
Set system properties to override the configuration at runtime (eg.
-Dremote.browser.name=chrome
).
4. Configuration
The behavior of WebTester can be configured on a browser by browser basis by
providing a Configuration
instance while creating the Browser
.
A Configuration
instance can be created by using a ConfigurationBuilder
implementation. That instance can then be used to customize the browser’s
configuration using a BrowserBuilder
.
4.1. BaseConfigurationBuilder
The BaseConfigurationBuilder
will use a BaseConfiguration
instance as a
starting point. It takes ConfigurationAdapter
and ConfigurationExporter
instances as options before the build()
operation.
4.2. ConfigurationAdapter
A ConfigurationAdapter
is used to change properties of an existing
Configuration
. This is done using a callback method adapt(Configuration c)
.
The following adapters are provided by the webtester-core
module:
-
ClasspathPropertiesFileConfigurationAdapter
-
GlobalFileConfigurationAdapter
-
LocalFileConfigurationAdapter
4.2.1. ClasspathPropertiesFileConfigurationAdapter
This adapter can be used to load properties from any file on the classpath.
new ClasspathPropertiesFileConfigurationAdapter("config/foo.properties");
In addition to the file name, you can optionally provide an importance level like this:
new ClasspathPropertiesFileConfigurationAdapter("config/foo.properties", Importance.REQUIRED);
This will define the behaviour in case the file is not found on the classpath.
There are three levels of importance:
-
OPTIONAL
: there will be an info log message -
RECOMMENDED
: there will be a warning log message -
REQUIRED
: there will be an exception
4.2.2. GlobalFileConfigurationAdapter
This adapter extends ClasspathPropertiesFileConfigurationAdapter
and uses the
a testit-webtester-global.properties
file on the classpath’s root level. Its
importance level is set to OPTIONAL
.
It is intended to be used in collaboration with the LocalFileConfigurationAdapter
.
The global adapter would define defaults for any number of projects and be provided by
a common codebase / framework.
4.2.3. LocalFileConfigurationAdapter
This adapter extends ClasspathPropertiesFileConfigurationAdapter
and uses the
a testit-webtester.properties
file on the classpath’s root level. Its
importance level is set to RECOMMENDED
.
As stated by the importance level, this file is recommended to be present on the classpath because it is the default way of configuring WebTester for your project.
4.3. ConfigurationExporter
A ConfigurationExporter
is used to "export" a Configuration
to another
System. This is done by using a callback method export(String key, Object
value)
for each key / value pair of the Configuration
.
The following exporters are provided by the webtester-core
module:
-
SystemPropertyConfigurationExporter
4.3.1. SystemPropertyConfigurationExporter
This exporter can be used to export each key / value pair as system properties
in order to make them accessible using System#getProperty(String key)
.
4.4. Default Configuration
Since not everyone needs to customize the configuration in the context of his or her project a 'DefaultConfigurationBuilder' is provided which uses the following adapters (in order) and no exporters to build a Configuration :
-
GlobalFileConfigurationAdapter
-
LocalFileConfigurationAdapter
This builder is also used in case a Browser
is build without providing a
custom Configuration
instance.
ConfigurationAdapter adapter1 = ...;
ConfigurationAdapter adapter2 = ...;
ConfigurationExporter exporter = ...;
// this will adapt a base configuration first with 'adapter1' and then with 'adapter2'
// after that the final configuration will be exported using the 'exporter'
Configuration config = new BaseConfigurationBuilder()
.withAdapters(adapter1, adapter2)
.withExporter(exporter)
.build();
4.5. Default Properties
These are all the named properties loaded by default:
# Whether or not the events should be fired.
# TYPE: boolean [true, false]
events.enabled = true
# The amount of time actions should be decelerated (i.e. for demonstrations).
# TYPE: int [milliseconds]
actions.deceleration = 0
# URL of the default entry point for the application under test.
# TYPE: String [Resource URL]
# defaults.entry-point =
# Folder in which to save screenshots if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.screenshots = screenshots
# Folder in which to save source code of pages if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.page-sources = sourcecode
# Folder in which to save log files if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.logs = logs
# Whether or not color highlighting of used elements should be active or not.
# TYPE: boolean [true, false]
markings.enabled = false
# Color to use for the background of used elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.used.background = #ffd2a5
# Color to use for the outline of used elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.used.outline = #916f22
# Color to use for the background of read elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.read.background = #90ee90
# Color to use for the outline of read elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.read.outline = #008000
# Default timeout for wait operations.
# TYPE: int [seconds]
wait.timeout = 2
# Default interval in which to check a condition for wait operations.
# TYPE: int [milliseconds]
wait.interval = 100
# Name of the browser to use in Selenium Grid
# TYPE: String [firefox, chrome, safari, ...]
remote.browser.name = firefox
# Version of the browser to use in Selenium Grid. If not specified, any version will be used!
# TYPE: String [eg. 46.0.1]
# remote.browser.version =
# Whether the Marionette driver (Firefox 47++) should be used.
# TYPE: boolean [true, false]
remote.firefox.marionette = true
# Host or IO address where Selenium Grid is running
# TYPE: String [localhost, 192.168.0.1, ...]
remote.host = localhost
# Host or IO address where Selenium Grid is running
# TYPE: Integer
remote.port = 4444
5. Page Objects
5.1. The Page Object Pattern
The WebTester framework’s architecture and design is based around the Page Object Pattern. For more information about Page Object Pattern see:
5.2. Pages
The Page
interface is the parent for all pages used to implement the Page
Object Pattern. It provides base methods every page will need. Pages can be
initialized using the create(pageClass)
method of a Browser
instance or from
within another page.
The following example represents an application with two pages. On each page the same navigation menu is displayed. The page’s content differs from page to page. This demonstrates composition of page information.
// the navigation menu widget
public interface NavigationMenu extends PageFragment {
@IdentifyUsing("#firstLink")
Link firstLink();
@IdentifyUsing("#secondLink")
Link secondLink();
}
// a 'trait' interface declaring the property of having a navigation menu
public interface HasNavigationMenu {
/* The navigation menu is identified by its ID
* This automatically limits the search scope for the navigation menu's fragments to
* everything contained inside the tag with the ID "navMenu" */
@IdentifyUsing("#navMenu")
NavigationMenu navigation();
}
// a page containing a table with the ID "fooTable" and inheriting the navigation menu trait
public interface FooPage extends Page, HasNavigationMenu {
@IdentifyUsing("#fooTable")
Table fooTable();
...
}
// a page containing a table with the ID "barTable" and inheriting the navigation menu trait
public interface BarPage extends Page, HasNavigationMenu {
@IdentifyUsing("#barTable")
Table fooTable();
...
}
5.3. Page Fragments
As can be seen in the previous example a page consists of different
PageFragment
declarations. The fragments can be accessed / initialized by
invoking a method annotated with @IdentifyUsing
. For mor information about
page fragments see the next chapter.
5.4. Collections of Page Fragments
Multiple page fragments of a page can be retrieved as a Set
, List
or
Stream
by simply declaring any of those as the return type of a
@IdentifyUsing
annotated method:
public interface CollectionPage extends Page {
@IdentifyUsing(".text-field")
List<TextField> textFieldList();
@IdentifyUsing(".text-field")
Set<TextField> textFieldSet();
@IdentifyUsing(".text-field")
Stream<TextField> textFieldStream();
}
5.5. Relevant Annotations
There are several annotations which can be used within the context of a Page
:
-
@Action
-
@IdentifyUsing
-
@Named
-
@PostConstruct
-
@PostConstructMustBe
-
@WaitUntil
5.6. Anatomy of a Page
Pages generally provide a number of distinct method types. The following sections will describe the most important ones.
5.6.1. Actions
Actions are methods which change the state of a page without leaving it. This could be the input of text in a text field or the selection of a value in a select menu.
Rules:
-
Method name represents an action: "setUsername", "changeDataOfBirth" etc.
-
Method returns the same instance of the Page for fluent API support.
-
Method does not change multiple states.
default LoginPage setFirstName(String name){
firstName.setText(name);
return this;
}
default LoginPage selectBirthMonth(String month){
birthMonth.selectByText(month);
return this;
}
5.6.2. Navigations
Navigations are methods which execute an action that leads to a page change. This could be the click of a link or the direct opening of an URL. The difference between navigations and actions is, that a navigation has to declare what page comes "next". This means that for a single navigation "action" there might me multiple methods. I.g. this is necessary to declare "bad case" paths through the application. E.g. if a login fails or a process could not be finished.
Rules:
-
Method name represents an action: "clickLogin", "clickLoginExpectingError" etc.
-
Method returns a new instance of the target page’s Page for fluent API support.
-
Method does not change multiple states.
default MainPage clickLogin(){
login.click();
return create(MainPage.class);
}
default LoginPage clickLoginExpectingError(){
login.click();
return create(LoginPage.class);
}
5.6.3. Workflows
Workflows combine different methods in order to allow for "fast" navigation over pages. I.g. they combine a set of actions with a navigation. This could e.g. be a single method to log into a system.
Rules:
-
Method name represents a process: "login", "register" etc.
-
Method’s return type and value depends on the last command in the workflow: Is it an action or a navigation?
-
Method does change multiple states.
default MainPage login(User user){
return setUsername(user.getUsername())
.setPassword(user.getPassword())
.clickLogin();
}
default LoginPage loginExpectingError(User user){
return setUsername(user.getUsername())
.setPassword(user.getPassword())
.clickLoginExpectingError();
}
5.6.4. Information Getter
Information getter are methods which retrieve information from a page. This could be the text of a displayed error message or the content of a certain text field.
Rules:
-
Method name represents a request: "getErrorMessages", "getNumberOfDisplayedTableEntries" etc.
-
Method’s return type might be anything but a Page.
-
Method does not change any states.
default String getErrorMessage () {
return errorMessage.getText();
}
default int getNumberOfSearchResults () {
int counter = 0;
// some logic
return counter;
}
6. Page Fragments
Page fragments are parts of a Page
. They extend the PageFragment
interface
and represent any number of thing. From a single text field to a widget.
WebTester provides a number of functional page fragments out of the box. These
map more or less to HTML elements:
-
Button
-
Checkbox
-
Div
-
EmailField
-
Form
-
Headline
-
IFrame
-
Image
-
Link
-
ListItem
-
MultiSelect
-
NumberField
-
OrderedList
-
Paragraph
-
PasswordField
-
RadioButton
-
SearchField
-
SingleSelect
-
Span
-
Table
-
TableField
-
TableRow
-
TelephoneField
-
TextArea
-
TextField
-
UnorderedList
-
UrlField
-
GenericElement
-
GenericList
-
GenericSelect
-
GenericTextField
The Button
for example is mapped to <button/>
, <input type="reset">
,
<input type="submit">
and <input type="button">
while the SingleSelect
maps to a very specific type of <select>
(where the multiple
attribute is
not set).
In contrast to the generic interactions offered by Selenium’s WebElement
interface these functional classes provide only those methods which are useful
for the given context / their type. A SingleSelect
does not provide methods to
change its text, but it will have methods to change selection based on index,
value or text.
Example
public interface SearchWidget extends PageFragment {
@IdentifyUsing("#query")
SearchField query();
@IdentifyUsing("#submit")
Button submit();
}
6.1. Validation
By default a PageFragment
will match any HTML tag in form of a WebElement.
Functional page fragments on the other hand are limited to a certain amount of
valid HTML tags and attribute combinations. This is done by annotating them with
@Mapping
for a single mapping. This annotation can be used multiple times in
case there is more then one valid combination.
Annotating any page fragment with @Mapping
will trigger a validation logic
anytime the underlying web element is resolved. In case the validation fails a
MappingException
is thrown.
The @Mapping
annotation is used to define a valid combination of tag,
attribute and attribute values of a web element to be used with a page
fragment class.
There are a number of different ways to use this:
-
@Mapping(tag="div")
Will be evaluated as 'valid' in case the web element has the tag 'div'. -
@Mapping(tag="select", attribute="multiple")
Will be evaluated as 'valid' in case the web element has the tag 'select' and the 'multiple' attribute is present. -
@Mapping(tag="select", attribute="!multiple")
Will be evaluated as 'valid' in case the web element has the tag 'select' and the 'multiple' attribute is not present. -
@Mapping(tag="input", attribute="type", values={"text", "password"})
Will be evaluated as 'valid' in case the web element has the tag 'input' and the 'type' attribute has either the 'text' oder 'password' value. -
@Mapping(validator=FooValidator.class)
Will create a new instance of the given validator class and use it to evaluate the web element.
@Mapping(tag = "span")
public interface Span extends PageFragment {
...
}
@Mapping(tag = "select", attribute = "multiple")
public interface MultiSelect extends PageFragment {
...
}
6.2. Inheritance
Since all PageFragments
are interfaces and Java currently does not support
inheritance of annotations on interfaces because of a conceptual problem with
multiple inheritance, it is neccessary to re-annotate PageFragment
sub-classes
when extending or methods when overriding them.
@Mapping(tag = "span")
public interface Span extends PageFragment {
...
}
public interface MySpan extends Span {
// will no longer check @Mapping validity
}
6.3. Relevant Annotations
There are several annotations which can be used within the context of a
PageFragment
:
-
@Action
-
@Attribute
-
@IdentifyUsing
-
@Mark
-
@Named
-
@PostConstruct
-
@PostConstructMustBe
-
@Produces
-
@WaitUntil
6.4. Generic Page Element
The GenericElement
PageFragment
interface is basically the
WebElement
of page fragments. It opens up all methods of WebElement
which were not already implemented in PageFragment
(maybe with a
different name).
It mainly intended for the Ad-Hoc find API in order to minimize the number of calls needed to make when rapidly prototyping or looking up deeply nested elements.
6.4.1. Casting
GenericElement
provides a method as(Class)
which allows the 'cast'
of the generic element to any other PageFragment
interface.
// find returns a GenericElement
Button b = browser.find(#button).as(Button.class);
7. Ad-Hoc Finding
It is not always the best solution to declare page fragments via public
@IdentifyUsing
method. Sometimes it is necessary to find a certain fragment
programmatically:
-
Maybe the fragment is only used in very special cases and should therefore not be public…
-
Maybe you need a list of fragments fitting certain parameters which can not be expressed with CSS or XPath..
-
Or maybe are just prototyping your approach and don’t want to implement page objects, yet…
For these cases the Ad-Hoc finding API was developed. This API can be accessed
through a Browser
, Page
or a PageFragment
. Depending on where the API is
accessed the search context for fragments might differ:
-
Browser
: The whole HTML page is searched for the fragment. -
Page
: The whole HTML page is searched for the fragment. -
PageFragment
: The area between the page fragment’s open and close tags is searched for the fragment.
There are several ways to start finding fragments, here are a few examples:
// find an element by it's ID 'fooId' as a generic element
GenericElement element = getBrowser()
.find("#fooId");
// find many elements by their shared CSS class 'foo' as a stream of generic elements
Stream<GenericElement> elements = getBrowser()
.findMany(".foo");
// find an element by it's ID 'textField' as a text field (identifier first)
TextField textField = getBrowser()
.findBy(id("textField"))
.as(TextField.class);
// find an element by it's ID 'textField' as a text field (class first)
TextField textField = getBrowser()
.find(TextField.class)
.by(id("textField"));
// find all all elements with the CSS class 'foo'
// within an element with ID 'group' as a stream of text fields
Stream<TextField> textFields = getBrowser()
.find("#group")
.findBy(css(".foo"))
.asMany(TextField.class);
8. Event System
WebTester provides an event mechanism where listeners can be registered to be infomed of things that occured inside the framework (e.g. navigation, button clicks etc.).
Examples
static EventListener customListener;
@BeforeClass
public static void registerEventListener () {
customListener = (event) -> System.out.println(event);
browser.events().register(customListener);
}
@AfterClass
public static void deregisterEventListener () {
browser.events().deregister(customListener);
}
8.1. The EventSystem Class
The EventSystem
is a service providing methods for firing and listening for
Events
. EventListener
instances can be registered at the system as well as
deregistered once they are no longer needed. Each Browser
instance has it’s
own instance of EventSystem
8.2. The Event Interface
An Event
contains all the information needed to understand what happened.
Since it is an interface implementing a custom event is very easy. In general it
is recommended to treat events as data objects. They should not contain
references to services or larger parts of the system. The webtester-core
module provides a number of events for it’s actions:
Event | Description |
---|---|
|
An alert was closed by accepting it. |
|
The browser was closed. |
|
A browser window was closed. |
|
An Alert was closed by declining it. |
|
A Window was maximized. |
|
A backwards navigation was executed. |
|
A forwards navigation was executed. |
|
An URL was opened. |
|
The current page was refreshed. |
|
Source code was saved. |
|
The window positon was changed. |
|
The window size was changed. |
|
The |
|
The |
|
The |
|
A screenshot was taken. |
|
An input element was clear of any set value. |
|
A click was executed. |
|
A context click was executed. |
|
Every option of a |
|
An option of a |
|
An option of a |
|
An option of a |
|
A double click was executed. |
|
The 'Enter' key was pressed. |
|
A form was submitted. |
|
The value of a |
|
An option of a |
|
Multiple options of a |
|
An option of a |
|
Multiple options of a |
|
An option of a |
|
Multiple options of a |
|
The selection of a |
|
The value of an input field was appended with additional text. |
|
The value of an input field was changed. |
8.3. The EventListener Interface
An EventListener
is a simple interface providing a single method void
eventOccurred(Event event);
. Instances of this interface can be registered at
the EventSystem
in order to be called every time an Event
is fired.
8.4. Configuration
The firing of events can be disabled by setting the events.enabled
property to
false
. This will disable the firing of all events except ExceptionEvent
instances fired by using 'EventSystem#fireExceptionEvent(e)'.
9. Annotations
9.1. @Action
This annotation can be added to methods of Page
or PageFragment
subclasses
in order to mark these methods as actions. Currently the only effect of this
annotation the option to delay the execution of annotated methods by setting the
property actions.deceleration
to a certain amount of milliseconds.
Examples
// actions work on pages...
public interface FooPage extends Page {
@Action
default void doSomething() {
...
}
}
// ... as well as on page fragments
public interface BarFragment extends PageFragment {
@Action
default void doSomething() {
...
}
}
9.2. @Attribute
This annotation can be used within a PageFragment
subclass in order to
retrieve attributes of the underlying element. Attribute values (which are
returned as Strings from the WebElement
) will be parsed to the method’s return
type.
Supported Types:
-
String
-
Boolean
-
Long
-
Integer
-
Float
-
Double
-
Optional
of any of these types
Constraints:
-
Annotated method must not have arguments!
-
Annotated methods have to be part of a page fragment!
It is possible to declare annotated methods in any interface, as long as they are used from a page fragment.
Example of different attribute methods:
public interface FooFragment extends PageFragment {
// returns the string value of the 'value' attribute
@Attribute("value")
String value();
// returns the long value of the 'number' attribute
@Attribute("number")
Long number();
// returns the optional string value of the 'optional' attribute
@Attribute("optional")
Optional<String> optional();
}
Example of trait interface with attribute method:
public interface HasValue {
@Attribute("value")
String value();
}
public interface BarFragment extends PageFragment, HasValue {
// will have access to working 'value()' method
}
9.3. @IdentifyUsing
This annotation is used to tell the framework how the fragment(s) should be resolved when a page fragment returning method is invoked. The annotation provides all necessary information to identify the corresponding element(s) in the DOM of the displayed page:
-
how
: WhichByProducer
to use. I.e.CssSelector
,Id
,XPath
etc. Defaults toCssSelector
. -
value
: The value to be used by the mechanism.
Methods annotated with @IdentifyUsing
must not have arguments and can only
return:
-
Subclass of
PageFragment
-
List
of subclass ofPageFragment
-
Set
of subclass ofPageFragment
-
Stream
of subclass ofPageFragment
Examples
public interface FooPage extends Page {
// using the default CSS Selector and an ID
@IdentifyUsing("#foo")
TextField fooField();
// using an explicit ID selector
@IdentifyUsing(value = "#bar", how = Id.class)
TextField barField();
// all text fields as a stream identified by their common class 'text-field'
@IdentifyUsing(".text-field")
Stream<TextField> allTextFields();
}
public interface SearchWidget extends PageFragment {
@IdentifyUsing("#query")
SearchField query();
@IdentifyUsing("#submit")
Button submit();
}
9.4. @Mark
This annotation can be added to methods of PageFragment
subclasses in order to
mark the fragment in case the method is invoked. The 'marking' is done by
setting different style attributes for the underlying element.
The following marking types are available:
-
USED
- the state of the fragment was changed -
READ
- the state of the fragment was read
The marking feature can be activated / deactivated by setting the
markings.enabled
property. The color of each of these types can be configured
by changing any of the following properties:
-
markings.used.background
-
markings.used.outline
-
markings.read.background
-
markings.read.outline
Colors are specified as HEX RGB color codes, i.e. #ffaa99
.
Example
public interface FooFragment extends PageFragment {
// calling this method will mark the FooFragment as used
@Mark(As.USED)
default void doSomething() {
...
}
}
9.5. @Named
This annotation can be added to @IdentifyUsing
annotated PageFragment
returning methods of Page
or PageFragment
subclasses in order to override
the name of the returned fragment.
Collections of fragments can’t be named at the moment!
This is useful in cases where the element IDs are not very clear or event cryptic. The name is used in any logs where the fragment is referenced. As well as events fired by the framework.
Example
public interface FooPage extends Page {
@Named("The Foo Widget 42")
@IdentifyUsing("#foo")
FooWidget widget();
...
}
9.6. @PostConstruct
This annotation can be added to methods of Page
or PageFragment
subclasses.
Every annotated method will be invoked after an instance of this subclass was
initialized. These methods should be used to verify that the correct page is
displayed or the fragment has 'working' state. The order in which multiple
annotated methods are invoked is not deterministic.
Each method should work on it’s own and not depend on another method being invoked!
Since these methods are invoked using reflection, it is not possible to have method arguments!
As an alternative for @PostConstruct
the @PostConstructMustBe
annotation can
be used on page fragment returning methods.
Examples
public interface FooPage extends Page {
@IdentifyUsing("#foo")
FooWidget widget();
@PostConstruct
void assertThatWidgetIsVisible () {
assertThat(widget).is(visible());
}
...
}
public interface FooWidget extends PageFragment {
@IdentifyUsing("#one")
TextField fieldOne();
@IdentifyUsing("#two")
TextField fieldTwo();
@PostConstruct
void assertThatTextFieldsAreVisible () {
assertThat(fieldOne).is(visible());
assertThat(fieldTwo).is(visible());
}
...
}
9.7. @PostConstructMustBe
This annotation can be added to @IdentifyUsing
annotated methods of Page
or
PageFragment
subclasses. Every annotated method will be invoked after an
instance of this subclass was initialized and the condition provided by the
annotation will be checked. This mechanism is intended to be used in order to
prevent unnecessary @PostConstruct
methods to check basic conditions of parts
of the page / fragment. As with @PostConstruct
, the order in which these
methods are invoked / checked is not deterministic!
It is important to note that not all Predicate classes will work with this annotation. The mechanism with which the predicate is evaluated will initialize the given class via reflection and needs a default constructor to work!
Collection and Streams are currently NOT supported!
Examples
public interface FooPage extends Page {
@PostConstructMustBe(Visible.class)
@IdentifyUsing("#foo")
FooWidget widget();
...
}
public interface FooWidget extends PageFragment {
@PostConstructMustBe(Visible.class)
@IdentifyUsing("#one")
TextField fieldOne();
@PostConstructMustBe(Visible.class)
@IdentifyUsing("#two")
TextField fieldTwo();
...
}
9.7.1. Combination with @WaitUntil
The @PostConstructMustBe
annotation can be used in combination with
WaitUntil
. This is especially useful in AJAX heavy applications where a
fragment might be created with a short delay.
Example
In this example the widget
is checked as soon as BarPage
is initialized.
But WaitUntil
will be triggered when invoking the method assuring that the
widget is present before checking if it is visible.
public interface BarPage extends Page {
@PostConstructMustBe(Visible.class)
@WaitUntil(Present.class)
@IdentifyUsing("#bar")
BarWidget widget();
...
}
9.8. @Produces
This annotation can be used on methods of PageFragment
sub-classes in order to
trigger the creation and firing of an Event
whenever that method is executed.
9.8.1. Constraints
-
Only
Event
classes extendingAbstractPageFragmentEvent
are supported -
Only works in
PageFragment
classes, notPage
-
Used
Event
classes need to define aPageFragmentEventBuilder
inner class
9.8.2. PageFragmentEventBuilder
Instances of this interface are used to build Event
instances when @Produces
is used. The implementation classes need to be declared as static inner classes
of the Event
named Builder
. This convention allows for the dynamic used of
these builders without having to manage all builder types manually.
public class ClickedEvent extends AbstractPageFragmentEvent {
public ClickedEvent(PageFragment fragment) {
super(fragment);
}
@Override
public String describe() {
return "clicked: " + getPageFragmentName();
}
public static class Builder extends AbstractPageFragmentEventBuilder<ClickedEvent> {
@Override
protected ClickedEvent buildWith(PageFragment fragment) {
return new ClickedEvent(fragment);
}
}
}
In cases where data from the WebElement
is needed to build the Event
there
are two hooks which are called by WebTester before and after invoking the
annotated method. These are only used by the framework if the builder specifies
that it needs the data.
public class TextSetEvent extends AbstractPageFragmentEvent {
private final String before;
private final String after;
public TextSetEvent(PageFragment fragment, String before, String after) {
super(fragment);
this.before = before;
this.after = after;
}
@Override
public String describe() {
return "text of '" + getPageFragmentName() + "' was set to '" + after + "' (was '" + before + "')";
}
public static class Builder extends AbstractPageFragmentEventBuilder<TextSetEvent> {
private String before;
private String after;
@Override
public boolean needsBeforeData() {
return true;
}
@Override
public PageFragmentEventBuilder<TextSetEvent> setBeforeData(WebElement webElement) {
this.before = webElement.getAttribute("value");
return this;
}
@Override
public boolean needsAfterData() {
return true;
}
@Override
public PageFragmentEventBuilder<TextSetEvent> setAfterData(WebElement webElement) {
this.after = webElement.getAttribute("value");
return this;
}
@Override
protected TextSetEvent buildWith(PageFragment fragment) {
return new TextSetEvent(fragment, before, after);
}
}
}
9.9. @WaitUntil
This annotation can be added to @IdentifyUsing
annotated methods of Page
or
PageFragment
subclasses. When the annotated method is invoked a 'wait until'
operation is executed using the annotations condition. The condition is provided
via a class reference in order to support custom conditions. See
Conditions for a set of provided page fragment related
predicates.
It is important to note that not all Predicate classes will work with this annotation. The mechanism with which the predicate is evaluated will initialize the given class via reflection and needs a default constructor to work!
Collection and Streams are currently NOT supported!
A timeout can be configured by setting the timeout
and unit
properties of
the annotation. If no custom timeout is set the host browser’s configuration
defaults are used.
Examples
public interface FooPage extends Page {
@WaitUntil(Visible.class)
@IdentifyUsing("#foo")
FooWidget widget();
...
}
public interface FooWidget extends PageFragment {
@WaitUntil(Visible.class)
@IdentifyUsing("#one")
TextField fieldOne();
@WaitUntil(value=Visible.class, timeout=500, unit=TimeUnit.MILLISECONDS)
@IdentifyUsing("#two")
TextField fieldTwo();
...
}
10. Utilities
10.1. Conditions
The utility class Conditions
provides several factory methods for creating
Condition
instances. These are specialized subclasses of Java 8’s Predicate
interface and can be used in the following sub-systems:
-
Waiting:
@Wait
andWait.until(..)
-
Post Construct Assertions:
@PostConstructMustBe(…)
-
Filtering of
PageFragement
Streams
// ad-hoc finding of page fragments with filter
browser.findMany(".textfield").filter(Conditions.is(Conditions.visible()));
// waiting until a certain condition is met
Wait.until(textField).has(Conditions.text("foo"));
10.1.1. Syntax
Conditions are designed to be readable. They take a lot of inspiration from
Hamecrest’s Matcher
and AssertJ’s fluent API. It is generally recommended to
use static imports when working with the Conditions
class.
We provide two kinds of out of the box conditions in the
info.novatec.testit.webtester.conditions
package:
-
Syntax operations like
Has
,Is
,Not
andEither
in order to make conditions more readable -
Page fragment conditions like
Attribute
,Visible
,Selected
etc.
10.1.2. List of current Conditions
Syntax:
-
Either
-
Has
-
Is
-
Not
Page Fragment:
-
Attribute
-
AttributeWithValue
-
Disabled
-
Editable
-
Enabled
-
Interactable
-
Invisible
-
Present
-
PresentAndVisible
-
ReadOnly
-
Selected
-
SelectedIndex
-
SelectedIndices
-
SelectedText
-
SelectedTexts
-
SelectedValue
-
SelectedValues
-
Visible
-
VisibleTextContains
-
VisibleTextEquals
10.2. By-Producers
The utility class ByProducers
provides several factory methods for creating
ByProducer
instances. These are used by WebTester as an abstraction over
Selenium’s By
classes. They are relevant to the following (sub-)systems:
// Ad-Hoc finding of page fragment
browser.findBy(ByProducers.id("username"));
10.3. Simulating Mouse Actions
The Mouse
utility class contains all kinds of methods which allow you to use
or at least simulate the use (depending on the WebDriver
implementation) of
mouse actions. These are the currently implemented mouse actions:
-
click(PageFragment)
-
doubleClick(PageFragment)
-
contextClick(PageFragment)
-
moveTo(PageFragment)
-
moveToEach(PageFragment, PageFragment…)
-
moveToEach(Collection<PageFragment>)
// clicks a button
Mouse.click(button);
// double clicks an image
Mouse.doubleClick(image);
// moves the mouse to the link
Mouse.moveTo(link);
// moves the mouse to each link as they appear
Mouse.moveToEach(fileMenu, fileMenuNew, fileMenuNewPage);
10.3.1. Mouse.click()
Executes a click on the given PageFragment
by first moving the mouse to the
center of it.
10.3.2. Mouse.doubleClick()
Executes a double click on the given PageFragment
by first moving the mouse to
the center of it.
10.3.3. Mouse.contextClick()
Executes a context click on the given PageFragment
by first moving the mouse
to the center of it.
10.3.4. Mouse.moveToEach()
Moves the mouse to each of the given `PageFragment`s in turn. The page fragments have to be visible in order to move the mouse to it. This method can be used to navigate dynamically displayed menu structures because it waits for each page fragment to be displayed before moving the mouse to it.
10.3.5. Mouse.moveTo()
Moves the mouse to the given PageFragment
. The page fragment has to be visible
in order to move the mouse to it.
10.3.6. Fluent API for Mouse Actions
In addition to these single actions the Mouse
utility class provides
several methods for execution a number of actions with a fluent syntax:
-
on(PageFragment)
-
sequence()
// actions on fragment
Mouse.on(button).click();
Mouse.on(button).doubleClick();
Mouse.on(button).contextClick();
// sequence
Mouse.sequence().moveTo(image).click();
Mouse.sequence().moveTo(image).moveTo(otherImage).click();
Mouse.sequence().click(image).doubleClick(otherImage);
10.4. Waiting
The Wait
utility class provides a fluent API for all kinds of wait operations.
This includes waiting an exact amount of time and waiting for certain conditions
with a timeout.
There are 4 distinct kinds of Wait operations:
-
Waiting an exact amount of time:
Wait.exactly(..)
-
Waiting until an object state is reached:
Wait.until(..)
-
Waiting until an object supplier return value’s state is reached:
Wait.untilSupplied(..)
-
Waiting like "2." but executing another action during waiting for a given condition:
Wait.untilWithAction(..)
Examples
// waits 5 seconds
Wait.exactly(5, TimeUnit.SECONDS);
// waits 150 milliseconds
Wait.exactly(150, TimeUnit.MILLISECONDS);
// waits 1 hour
Wait.exactly(1, TimeUnit.HOURS);
// waits until the hidden field is visible on the DOM - with default timeout
PageFragment hiddenField = ...;
Wait.until(hiddenField).is(visible());
// waits until the hidden field is visible on the DOM - with custom timeout
Wait.withTimeoutOf(10, TimeUnit.SECONDS).until(hiddenField).is(visible());
// waits until the call to 'findMany(".foo")' returns a non empty list
Wait.untilSupplied(() -> findMany(".foo")).is((foos) -> !foos.isEmpty());
// waits until the hidden field is visible on the DOM - with default timeout
PageFragment hiddenField = ...;
PageFragment wrongField = ...;
WaitingAction action = new WaitingAction(isVisible(wrongField), () -> refresh())
Wait.untilWithAction(isVisible(hiddenField), );
10.4.1. Wait.exactly(…)
This is the most primitive wait operation. It allows to wait for a specific amount of time. That amount is specified by to parameters: the amount and the time unit.
The maximum precision for the wait operation is milliseconds. If any more precise unit is defined (e.g. nanoseconds), there will be no wait.
10.4.2. Wait.until(..)
This kind of wait operation will take any object instance and allows for the definition of several conditions to be waited on in order. It is important to note that the conditions will always be evaluated against the initially specified instance!
In the above example you can see a command which will wait until a 'hidden'
field is visible. This will work because the given object is a PageFragment
.
Since page fragments act as proxies for WebElement
instances, which are not
cached, the check on visibility can return a different result for each
invocation.
But let’s say, as an example, the given object is a list of page fragments and you want to wait until the list has a certain size. In this case the size of the list will never change unless it’s contents is manipulated asynchronously.
In order to check something like this take a look at Wait.untilSupplied(..)
.
10.4.3. Wait.untilWithAction(..)
This behaves essentially like the Wait.until(..) operation, but with the option to execute a given command. This is managed by an instance of the WaitingAction class. This class takes an a condition in form of an object supplier to check if the given command has to be executed. The command can be any method call provided by an empty lamda.
The operation is designed to help you stabilize tests in flaky environments.
10.4.4. Wait.untilSupplied(..)
This kind of wait operation will take an object supplier as its parameter. The supplier is invoked every time a condition is checked. With this approach you can wait until a dynamic object - like a list of page fragments - has a certain state (e.g. size).
11. Support Modules
11.1. AssertJ 3 Assertions
The support module webtester-support-assertj3
provides AssertJ 3 assertion
implementations for many properties of page fragments:
-
ButtonAssert
-
GenericTextFieldAssert
-
MultiSelectAssert
-
PageFragmentAssert
-
SelectableAssert
-
SingleSelectAssert
All of these can be accessed through a single utility class named
WebTesterAssertions
. Which extends AssertJ’s Assertions class and therefore
provides all of AssertJ’s default assertions as well.
TextField username = ...;
WebTesterAssertions.assertThat(username).hasText("fooUser");
11.2. Hamcrest Matchers
The support module webtester-support-hamcrest
provides Hamcrest Matcher
implementations for many properties of page fragments:
-
AttributeMatcher
-
AttributeValueMatcher
-
ButtonLabelMatcher
-
DisabledMatcher
-
EnabledMatcher
-
InvisibleMatcher
-
NoOptionsMatcher
-
NoSelectedOptionsMatcher
-
NumberOfOptionsMatcher
-
NumberOfSelectedOptionsMatcher
-
OptionsMatcher
-
OptionsTextsMatcher
-
OptionsValuesMatcher
-
PresentMatcher
-
SelectedMatcher
-
SelectedOptionsMatcher
-
SelectionIndexMatcher
-
SelectionIndicesMatcher
-
SelectionTextMatcher
-
SelectionTextsMatcher
-
SelectionValueMatcher
-
SelectionValuesMatcher
-
TagMatcher
-
TextContainingMatcher
-
TextMatcher
-
VisibleMatcher
-
VisibleTextContainingMatcher
-
VisibleTextMatcher
All of these can be accessed through a single utility class named
WebTesterMatchers
. Which extends Hamcres’s Matchers class and therefore
provides all of Hamcrest’s default matchers as well.
TextField username = ...;
// without static imports
WebTesterMatchers.assertThat(username, WebTesterMatchers.has(WebTesterMatchers.text("fooUser")));
// with static imports
assertThat(username, has(text("fooUser")));
11.3. JUnit 4 Runner
The support module webtester-support-junit4
provides a custom JUnit Runner.
This runner includes the following features:
-
Life cycle management of class and test level
Browser
instances. -
Automatic navigation to an entry point to the application under test before each test.
-
Injection of custom configuration properties into the following basic types:
String
,Integer
,Long
,Float
,Double
andBoolean
11.3.1. Test Runner Life Cycle
The WebTesterJUnitRunner
is based on the default BlockJUnit4ClassRunner
and
extends its life cycle at certain points. The following shows the workflow of a
test class with two test methods.
-
static rules'
before()
methods -
static
Browser
creation and injection -
injection of configuration properties into static fields annotated with
@ConfigurationValue
-
static methods annotated with
@BeforeClass
-
instance rules'
before()
methods -
instance
Browser
creation and injection -
injection of configuration properties into instance fields annotated with
@ConfigurationValue
-
instance methods annotated with
@Before
-
test method 1
-
instance methods annotated with
@After
-
instance
Browsers
are closed -
instance rules'
after()
methods -
instance rules'
before()
methods -
instance
Browser
creation and injection -
injection of configuration properties into instance fields annotated with
@ConfigurationValue
-
instance methods annotated with
@Before
-
test method 2
-
instance methods annotated with
@After
-
instance
Browsers
are closed -
instance rules'
after()
methods -
static methods annotated with
@AfterClass
-
static
Browsers
are closed -
static rules'
after()
methods
11.3.2. Browser Life Cycle Management
All Browser’s
life cycle is managed on class as well as test level (static /
non static fields). To use this feature simply declare a Browser
field in your
test class and annotate it with @Resource
. Every uninitialized (null) field
annotated in that way will be injected with a new Browser
instance. Fields
which are already initialized with a Browser
will be included in the life
cycle handling as well, but no new instances are created by the runner.
@RunWith ( WebTesterJUnitRunner.class )
public class DifferentBrowserFieldModifiersTest {
// A pre-initialized Browser which will not be initialized with a new
// browser but the instance will be handled as part of the life cycle.
@Resource
static Browser preInitializedBrowser = new Browser(new FirefoxDriver());
// A static Browser which will be initialized with a new instance before
// the first and closed after the last test is executed.
@Resource
@CreateUsing ( ... )
static Browser classScopedBrowser;
// An instance Browser which will be initialized with a new instance
// before and closed after each test is executed.
@Resource
@CreateUsing ( ... )
Browser testScopedBrowser;
// An instance Browser field which will be ignored by the runner since
// the @Resource annotation is missing.
Browser notAManagedBrowser;
...
}
11.3.3. Configuring a BrowserFactory
In order to configure the BrowserFactory
used to create the Browser
instances, the @CreateUsing
annotation must be used.
@RunWith ( WebTesterJUnitRunner.class )
public class DifferentBrowserFactoriesTest {
// Uses the FirefoxFactory to create Firefox instances.
@Resource
@CreateUsing ( FirefoxFactory.class )
static Browser firefox;
// Uses the InternetExplorerFactoryto create IE instances.
@Resource
@CreateUsing ( InternetExplorerFactory.class )
Browser internetExplorer;
...
}
11.3.4. Defining an Entry Point
In order for a Browser
to be automatically navigated to an applications entry
point before each test, the @EntryPoint
annotation can used. The navigation at
the beginning of each test is done whether or not a Browser
is static!
The URL can be static or contain variables. These variables are resolved against
the annotated Browser’s
Configuration
as String
values. In case a variable
could not be resolved an IllegalStateException
is thrown before the first test.
@RunWith ( WebTesterJUnitRunner.class )
public class EntryPointsTest {
// Will begin each test on Google.
@Resource
@CreateUsing ( ... )
@EntryPoint ( "http://www.google.com" )
static Browser classScopedBrowser;
// Will begin each test on Bing.
@Resource
@CreateUsing ( ... )
@EntryPoint ( "http://www.bing.com" )
Browser testScopedBrowser;
// Will use url provided by property
@Resource
@CreateUsing ( ... )
@EntryPoint("${properties.url}")
Browser variableUrl;
...
}
11.3.5. Configuration Property Injection
All custom configuration properties can be injected into the following base
field types: String
, Integer
, Long
, Float
, Double
and Boolean
. The
injection is done for all fields which are annotated with @ConfigurationValue
.
@RunWith ( WebTesterJUnitRunner.class )
public class ConfigurationValuesTest {
// Injects the integer value of "customer.integer"
@ConfigurationValue ( "custom.integer" )
static Integer customInteger;
// Injects the string value of "customer.string"
@ConfigurationValue ( "custom.string" )
String customString;
...
}
11.3.6. Multiple Browser Instances and Configuration Property Injection
Since every Browser
has it’s own Configuration
instance a "primary" Browser
has to be declared when using multiple Browser
instances and the
Configuration
property injection feature. The primary browser will be the
source for the configuration properties injected by the WebTesterJUnitRunner
.
If only one browser is managed it is automatically used as the primary browser!
In case you want to inject property values into a static field your primary browser has to be static as well!
@RunWith ( WebTesterJUnitRunner.class )
public class MultiBrowserConfigurationValuesTest {
@Primary
@Resource
@CreateUsing ( ... )
static Browser primaryBrowser;
@Resource
@CreateUsing ( ... )
Browser anotherBrowser;
@ConfigurationValue ( "custom.integer" )
static Integer customInteger;
@ConfigurationValue ( "custom.string" )
String customString;
...
}
11.4. JUnit 5 Extensions
The support module webtester-support-junit5
provides a set of JUnit 5
extensions:
-
ManagedBrowserExtension:
-
Initialization of static and instance
Browser
fields. -
Automatic opening and closing of managed
Browser
depending on field scope.
-
-
EntryPointExtension:
-
Automatic navigation to 'entry point' URL.
-
Support variables which are resolved against a
Configuration
.
-
-
RegisteredEventListenerExtension:
-
Initialization of instance
EventListener
fields. -
Automatic registration and unregistration of
EventListener
to managedBrowser
.
-
-
PageInitializerExtension:
-
Initialization of
Page
fields before each test. -
Supports multiple
Browser
instances.
-
-
ConfigurationValueExtension:
-
Injection of configuration values into instance fields of the following types:
-
String
-
Integer
-
Long
-
Float
-
Double
-
Boolean
-
-
Custom field types are supported via extensions
-
11.4.1. ManagedBrowserExtension
By annotating a Browser
field with @Managed
the extension is triggered and
will manage this field’s life cycle: - For static
fields the initialization is
done before the first @BeforeAll
annotated method is invoked and the browser
will be closed after the last @AfterAll
annotated method was invoked. - For
instance fields the initialization is done before the first @BeforeEach
annotated method is invoked and the browser will be closed after the last
@AfterEach
annotated method was invoked.
In case more than one Browser
is used, each has to have a unique name. The
name can be provided by setting the value
property of the @Managed
annotation.
In order for WebTester to know what kind of Browser should be created for each
field there are two annotations: - @CreateBrowsersUsing
can be used to
annotate a test class and set the BrowserFactory
to be used for the whole
class. - @CreateUsing
can be used to annotate a Browser
field directly in
order to set the BrowserFactory
for this field specifically. This will
override any global definition!
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed("browser-1")
static Browser staticBrowser;
@Managed("browser-2")
Browser instanceBrowser;
@Managed("browser-3")
@CreateUsing(BarFactory.class)
Browser differentFactory;
...
}
11.4.2. EntryPointExtension
By annotating any @Managed
Browser
field with @EntryPoint
you can specify
an URL which will be navigated to before each test execution. The URL can be
static or contain variables. These variables are resolved against the annotated
Browser’s
Configuration
as String
values. In case a variable could not be
resolved an UnknownConfigurationKeyException
is thrown before the first test.
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed("browser-1")
@EntryPoint("http://www.example.com")
Browser staticUrl;
@Managed("browser-2")
@EntryPoint("${properties.url}")
Browser variableUrl;
@Managed("browser-3")
@EntryPoint("http://${host}:${port}/index.html")
Browser staticMixedWithVariableUrl;
...
}
11.4.3. RegisteredEventListenerExtension
By annotating any instantiable EventListener
field with @Registered
you can
specify a browser to which the EventListener
has to be registered and
unregistered automatically.
The extension will initialize the field if it’s not pre-initialized and register
the EventListener
before the first @BeforeEach annotated method is invoked.
The unregistration will be done after the last @AfterEach annotated method was
invoked.
In case more than one Browser
is used, the target browsers must be specified
explicitly.
This extension does not support static fields!
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed
Browser browser;
@Registered
MyEventListener created; // will have new instance
@Registered
EventListener preInitialized = new MyEventListener(); // this instance will be used
...
}
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed("browser-1")
Browser browser1;
@Managed("browser-2")
Browser browser2;
@Managed("browser-3")
Browser browser3;
@Registered(targets = { "browser-1", "browser-2" })
CustomEventListener listener;
...
}
11.4.4. PageInitializerExtension
By annotating any Page
field with @Initialized
it will be initialized with a
new instance of that Page
class before the first @BeforeEach
annotated
method is invoked. In case the test class has multiple @Managed
Browser
instances the source
property of the annotation needs to specify which browser
should be used to initialize the Page
.
This extension does not support static fields!
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed("browser-1")
Browser browser1;
@Managed("browser-2")
Browser browser2;
@Initialized(source = "browser-1")
FooPage page1;
@Initialized(source = "browser-2")
BarPage page2;
...
}
11.4.5. ConfigurationValueExtension
By annotating any field with @ConfigurationValue
and providing a key by
setting the value
property the specified value will be retrieved from the
Configuration
and injected into the field. This is done before the first
@BeforeEach
annotated method is executed.
Currently the primitive object types String
, Integer
, Long
, Float
,
Double
and Boolean
are supported out of the box. Custom types can be used
when providing a matching ConfigurationUnmarshaller
implementation / class
reference as the annotation’s using
property
As with @Initialize
for Page
fields, a source
can be specified in cases
where multiple browsers are managed for a single test.
This extension does not support static fields!
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed
Browser browser;
@ConfigurationValue("stringValue")
String stringValue;
@ConfigurationValue("integerVaue")
Integer integerVaue;
@ConfigurationValue("longValue")
Long longValue;
@ConfigurationValue("floatValue")
Float floatValue;
@ConfigurationValue("doubleValue")
Double doubleValue;
@ConfigurationValue("booleanValue")
Boolean booleanValue;
@ConfigurationValue(value = "fooValue", using = FooTypeUnmarshaller.class)
FooType fooValue;
...
}
In case you are declaring multiple Browser
fields, the source
of the
configuration properties must be declared. This is necessary because each browser
might have a different configuration and the extension cannot decide which is the
'correct' one.
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {
@Managed("browser-1")
Browser browser1;
@Managed("browser-2")
Browser browser2;
@ConfigurationValue(value = "stringValue", source = "browser-1")
String stringValueOfBrowser1;
@ConfigurationValue(value = "stringValue", source = "browser-2")
String stringValueOfBrowser2;
...
}
11.5. Spring 4/5 Integration
11.5.1. Spring Configuration Adapter
The webtester-support-spring4
and webtester-support-spring5
modules each
provides a ConfigurationAdapter
implementation called SpringEnvironmentConfigurationAdapter
.
This adapter can be used to resolve properties from the existing Configuration
against a Spring Environment
. All keys which could successfully be resolved
against the environment will then be overridden in the configuration.
# WebTester Configuration
foo = hello world!
bar = welcome world!
# Spring Environment
foo = hello spring world!
# Resulting Configuration
foo = hello spring world!
bar = welcome world!
11.5.2. Factory Beans
In addition to the configuration adapter the module also provides FactoryBean
implementations which can be used to easily initialize different WebTester
services as beans.
-
DefaultSpringConfigurationFactoryBean
-
ConfigurationBuilderFactoryBean
-
PrototypeConfigurationBuilderFactoryBean
DefaultSpringConfigurationFactoryBean
Creates a Configuration
instance which results from using the
DefaultConfigurationBuilder
in conjunction with the
SpringEnvironmentConfigurationAdapter
.
ConfigurationBuilderFactoryBean
Creates a singleton ConfigurationBuilder
instance. ConfigurationAdapter
and
ConfigurationExporter
beans can be added via setters.
PrototypeConfigurationBuilderFactoryBean
Creates a prototyped ConfigurationBuilder
instance. ConfigurationAdapter
and
ConfigurationExporter
beans can be added via setters.
12. Kotlin
Since version 2.3 WebTester supports the use of Kotlin. Up until then the declarative nature of WebTester would not work with the way Kotlin is implementing default methods on interfaces.
In order to work with Kotlin, you must add the webtester-kotlin
module to your
test dependencies. Within this module you’ll find two classes:
-
info.novatec.testit.webtester.kotlin.pages.Page
-
info.novatec.testit.webtester.kotlin.pagefragments.PageFragment
Both of these are alias classes for their corresponding Java counterparts. In addition to providing a more Kotlin-esk API, their use will also act as a flag for WebTester to consider Kotlin when it’s generating code.
Other than using these special classes when creating pages and page fragments, everything else should work the same as with Java.