WebTester 2 - The Next Generation

WebTester is an open source automation framework for web applications. It is based on Selenium.
Years of working with several different automation tools have shown us the weaknesses most of these frameworks suffer from. Overly generic APIs, missing extension points or simply strange design choices. We decided to provide an intuitive, declarative and extensible API for writing effective and maintainable tests in Java. All of this is built on top of Selenium — in our opinion the number one test driver.
Top Features
- Optimized for Java 8 and above
- Intuitive Page Object Pattern with simple annotation-driven element identification
- Useful predefined element classes (e.g. Button, TextField, …)
- Simple API for runtime element identification
- Boost reuse with easy composition of pages and page fragments
- Highlighting of used elements for visual debugging
- Custom event handling: from a simple screenshot on exception to custom report generation
- Seamless integration with common frameworks like: AssertJ, Hamcrest, JUnit, Spring, etc.
- Selenium is always just a method call away!
Page Object Pattern
WebTester is designed around the Page Object pattern. You model your web application’s pages as objects and possible user interactions as methods. These page objects provide a stable layer between your tests and volatile HTML pages, making your tests much easier to maintain. Whenever you change the content on a web page, you simply update the corresponding page object, and all your tests will continue to work.
Example Application
Our application uses a simple login screen. The user should be able to login with valid credentials. If either user name or password are incorrect, an error should be displayed.
Declaring Pages
To test the login screen, we first declare three page objects in WebTester: Login, Welcome and Error
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public interface LoginPage extends Page { // (1) @IdentifyUsing("#username") // (2) TextField usernameField(); // (3) @IdentifyUsing("#password") PasswordField passwordField(); @IdentifyUsing("#login") Button loginButton(); @PostConstruct // (4) default void correctPageIsDisplayed(){ assertThat(usernameField()).isVisible(); // (5) assertThat(passwordField()).isVisible(); assertThat(loginButton()).isVisible(); } default WelcomePage login(Credentials credentials) { // (6) return setUsername(credentials.getUsername()) .setPassword(credentials.getPassword()) .clickLogin(); } default ErrorPage loginExpectingError(Credentials credentials) { return setUsername(credentials.getUsername()) .setPassword(credentials.getPassword()) .clickLoginExpectingError(); } default LoginPage setUsername(String username) { // (7) usernameField().setText(username); return this; } default LoginPage setPassword(String password) { passwordField().setText(password); return this; } default WelcomePage clickLogin() { // (8) loginButton().click(); return create(WelcomePage.class); } default ErrorPage clickLoginExpectingError() { // (9) loginButton().click(); return create(ErrorPage.class); } } |
Here’s what’s happening in detail:
- Pages are interfaces which extend the “Page” base interface. Why interfaces and not classes? Java interfaces support multiple inheritance, which we can use to easily compose larger pages.
- “@IdentifyUsing” declares a page fragment (here, the TextField for the user’s name). Per default we use CSS Selectors to identify elements, but other methods are supported as well: XPath, ID, Tag Name, Link Text etc.
- This method returns the previously declared “TextField” for the user’s name. The implementation is automatically supplied by WebTester.
- Methods annotated with “@PostConstruct” run before any tests and can validate preconditions, e.g. that the page is actually displayed in the browser.
- WebTester supports all popular assertion frameworks. Here we use AssertJ to assert that the field for username is visible.
- This is a workflow method. It uses fluent style to describe a chain of user interactions with the page (valid login in this case). Workflow methods are composed of simpler methods that describe single interactions with a page. Like all methods that describe user interactions, it returns a page object instance (in this case, the welcome page displayed after successful login).
- This is a state changing method. It describes entering the user’s name into the username text field. State changing methods always return the updated page object of the current page.
- This is a navigation method. It describes clicking on the login button and expecting to be routed to the welcome page. Navigation methods generally describe the user’s expectation of the next page being displayed, which is why they return a different page object.
- Applications can display different pages depending on the context (e.g. valid vs. invalid credentials used at login). Navigation methods describe which page is expected next, so there can be multiple navigation methods for the same basic interaction (clicking the login button), each with a different expected outcome.
1 2 3 4 5 6 7 8 9 10 11 |
public interface WelcomePage extends Page { @IdentifyUsing("#welcomeMessage") @PostConstructMustBe(Visible.class) // (1) GenericElement welcomeMessage(); // (2) default String getWelcomeMessage(){ // (3) return welcomeMessage().getVisibleText(); } } |
The Welcome page object demonstrates some more features:
- The “@PostConstructMustBe” annotation can be used as an alternative for “@PostConstruct” to ensure that the test’s preconditions are met. In this case, the welcome message element has to be visible.
- In case you don’t care about the specific functional representation of an element, you can always use the “GenericElement” type to access it. You can freely interact with a GenericElement, but there won’t be sanity checks (e.g. you can send key presses to a button, but also to a link).
- This is a getter method. These methods usually return information from the displayed page. In this case it returns the text of the welcome message.
1 2 3 4 5 6 7 8 9 10 11 |
public interface ErrorPage extends Page { @IdentifyUsing("#error") @WaitUntil(Visible.class) // (1) Error error(); // (2) default String getErrorMessage() { return error().getMessage(); } } |
The Error page object shows how WebTester handles asynchronous page updates:
- Elements can be annotated with “@WaitUntil” and a condition. Each time the element is accessed, the test will wait until the specified condition is met. Here, the error message is not returned until after the element becomes visible. Of course, things can go wrong and the element may never be displayed. In such a case, WebTester waits for a certain configurable period, before the element access fails due to a timeout.
- The page fragment described here is called “Error”, which is not one of WebTester’s standard fragments. We explain how to define custom fragments in the next section.
Defining Your Own Page Fragments
Elements on a web page are called “page fragments” in WebTester. You can easily define your own custom page fragments. They can either represent a single element of your application (e.g. a button) or a group of elements which build a logical context (e.g. a text field and a button, giving you a search widget). This nesting of page fragments maps nicely to the nesting of HTML elements.
1 2 3 4 5 6 7 8 |
@Mapping(tag = "div", attribute = "class", values = "error") // (1) public interface Error extends PageFragment { // (2) default String getMessage() { return find("#message").getVisibleText(); // (3) } } |
This is the definition of the Error page fragment:
- Page fragments are mapped to HTML code. In this case we define that an “Error” has to be a
with a class attribute of “error”. In case the “Error” fragment is ever used for another HTML tag, an exception will be thrown.
- Page fragments extend the base interface “PageFragment”.
- Instead of explicitly defining a nested page fragment, you can look them up dynamically. Here we use our “ad-hoc finding” API to look up an element within the context of the Error page fragment.
Writing Tests
The actual test for the Login page is written in JUnit 5 and uses features provided by WebTester’s extensions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@EnableWebTesterExtensions // (1) @ExtendWith(EmbeddedApplication.class) // (2) public class LoginTest { @Managed // (3) @CreateUsing(FirefoxFactory.class) // (4) @EntryPoint("http://localhost:8080/login") // (5) static Browser browser; // (6) @Initialized // (7) LoginPage loginPage; // (8) @Test void loginWithExistingCredentials() { Credentials credentials = Credentials.builder() .username("tester_de") .password("123456") .build(); WelcomePage welcomePage = loginPage.login(credentials); assertThat(welcomePage.getWelcomeMessage()).isEqualTo("You successfully logged in!"); } @Test void loginWithUnknownUser() { Credentials credentials = Credentials.builder() .username("unknown") .password("123456") .build(); ErrorPage errorPage = loginPage.loginExpectingError(credentials); assertThat(errorPage.getErrorMessage()).isEqualTo("Unknown user: 'unknown'"); } @Test void loginWithWrongPassword() { Credentials credentials = Credentials.builder() .username("tester_de") .password("wrong") .build(); ErrorPage errorPage = loginPage.loginExpectingError(credentials); assertThat(errorPage.getErrorMessage()).isEqualTo("Wrong password!"); } } |
The test methods themselves are pretty simple. There is however some WebTester-specific setup to do:
- With this convenience annotation all of WebTester’s JUnit 5 extensions are activated for the current test class.
- This extension handles the startup and shutdown of our web application. It is not a part of WebTester. We generally recommend that your tests handle the lifecycle of the application instance under test.
- @Managed tells WebTester to automatically handle initialization and shutdown of the annotated browser. It is provided by the lifecycle management extension of WebTester.
- @CreateUsing configures a factory class that WebTester will use to create browser instances. The kinds of browser used for testing and details of Selenium’s web driver configuration are managed by these factories.
- @EntryPoint provides a URL for the browser to open before each test. This URL does not have to be static, it can include placeholders for configuration properties!
- A “Browser” is an abstraction which provides methods for interacting with any supported browser (like Firefox, Chrome, Internet Explorer etc.).
- @Initialized tells WebTester to initialize the page object before each test.
- The initialized page object is our previously defined “LoginPage”. The page object matches the page displayed in the browser, which we specified as entry point.
What’s Next?
What we showed you is only a subset of the features provided by WebTester. For a full overview take a look at our documentation on GitHub. For additional details of how to set up WebTester for your project, check out the demo application here.
Recent posts






Comment article