Testing Vaadin Applications in the Browser With End-To-End Tests
End-to-end (e2e) tests are used to test the entire application. They’re far more coarse-grained than unit or integration tests. This makes them well suited to checking that the application works as a whole, and catching any regressions that may be missed by more specific tests.
End-to-end tests are executed in a browser window, controlled by a web driver and run on the server where the application is deployed. Vaadin TestBench takes care of these.
Note | Vaadin TestBench is a commercial product The end-to-end tests use Vaadin TestBench, which is a commercial tool that is a part of the Vaadin Pro Subscription. You can get a free trial at https://vaadin.com/trial. All Vaadin Pro tools and components are free for students through the GitHub Student Developer Pack. |
Creating the Base Test Class
To avoid repetition in each test class, it is a good idea to put common logic in an abstract class and have all tests extend this class. Most of the heavy lifting about starting browsers, etc. is handled by ParallelTest
in TestBench, but there are a couple of useful things you can add to the abstract class.
Create a new class, AbstractTest
in the com.example.application.it
package. Be sure to place it in src/test/java
and not src/main/java
.
AbstractTest.java
package com.example.application.it;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import com.vaadin.testbench.IPAddress;
import com.vaadin.testbench.ScreenshotOnFailureRule;
import com.vaadin.testbench.parallel.ParallelTest;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.slf4j.LoggerFactory;
public abstract class AbstractTest extends ParallelTest {
private static final String SERVER_HOST = IPAddress.findSiteLocalAddress();
private static final int SERVER_PORT = 8080;
private final String route;
static {
// Prevent debug logging from Apache HTTP client
Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.INFO);
}
@BeforeClass
public static void setupClass() {
WebDriverManager.chromedriver().setup(); 1
}
@Rule 2
public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this, true);
@Before
public void setup() throws Exception {
super.setup();
getDriver().get(getURL(route)); 3
}
protected AbstractTest(String route) {
this.route = route;
}
private static String getURL(String route) {
return String.format("http://%s:%d/%s", SERVER_HOST, SERVER_PORT, route);
}
}
Start by invoking the
WebDriverManager
before any test method is invoked. TestBench does not invoke the web driver manager.ScreenshotOnFailureRule
tells TestBench to grab a screenshot before exiting, if a test fails. This helps you understand what went wrong when tests do not pass.Open the browser to the correct URL before each test. For this, you need the host name where the application runs (“localhost” in development), the port the server uses (set to 8080 in application.properties), and information about the route to start from.
Testing the Login View
Now that your setup is complete, you can start developing your first test: ensuring that a user can log in. For this test you need to open the base URL.
Create a new class, LoginIT
, in the same package as AbstractTest
. The test validates that logging in with the correct user and password succeeds.
**LoginIT.java**
package com.example.application.it;
import com.vaadin.flow.component.login.testbench.LoginFormElement;
import org.junit.Assert;
import org.junit.Test;
public class LoginIT extends AbstractTest {
public LoginIT() {
super("");
}
@Test
public void loginAsValidUserSucceeds() {
// Find the LoginForm used on the page
LoginFormElement form = $(LoginFormElement.class).first();
// Enter the credentials and log in
form.getUsernameField().setValue("user");
form.getPasswordField().setValue("userpass");
form.getSubmitButton().click();
// Ensure the login form is no longer visible
Assert.assertFalse($(LoginFormElement.class).exists());
}
}
Note | Make sure tests have the correct name The name of the class should end in ”IT” for the test runner to pick it up as an integration test. If you name it |
Tip | Start the server separately to speed up tests While developing tests, it is not very efficient to run the tests as |
Start the application normally, then right-click LoginIT.java
and select Run ‘LoginIT’.
The first time you run the test, you will be asked to start a trial or validate your existing license. Follow the instructions in the browser window that opens.
Creating a View Object
You can now add a second test: validating that you cannot log in with an invalid password.
For this text, you need to write the same code to access the components in the view as you did for the first test. To make your tests more maintainable, you can create a view object (a.k.a. call page object or element class) for each view. A view object provides a high-level API to interact with the view and hides the implementation details.
For the login view, create the LoginViewElement
class in a new package, com.example.application.it.elements
:
LoginViewElement.java
package com.example.application.it.elements;
import com.vaadin.flow.component.login.testbench.LoginFormElement;
import com.vaadin.flow.component.orderedlayout.testbench.VerticalLayoutElement;
import com.vaadin.testbench.annotations.Attribute;
@Attribute(name = "class", contains = "login-view")
public class LoginViewElement extends VerticalLayoutElement {
public boolean login(String username, String password) {
LoginFormElement form = $(LoginFormElement.class).first();
form.getUsernameField().setValue(username);
form.getPasswordField().setValue(password);
form.getSubmitButton().click();
// Return true if we end up on another page
return !$(LoginViewElement.class).onPage().exists();
}
}
Caution | Class hierarchies must match To make the correct functionality available from superclasses, the hierarchy of the view object should match the hierarchy of the view ( |
Adding the @Attribute(name = "class", contains = "login-view")
annotation allows you to find the LoginViewElement
using the TestBench query API, for example:
Finding a LoginViewElement
using the TestBench query API
LoginViewElement loginView = $(LoginViewElement.class).onPage().first();
The annotation searches for the login-view
class name, which is set for the login view in the constructor. The onPage()
call ensures that the whole page is searched. By default a $
query starts from the active element.
Now that you have the LoginViewElement
class, you can simplify your loginAsValidUserSucceeds()
test to be:
LoginIT.java
@Test
public void loginAsValidUserSucceeds() {
LoginViewElement loginView = $(LoginViewElement.class).onPage().first();
Assert.assertTrue(loginView.login("user", "userpass"));
}
Add a test to use an invalid password as follows:
LoginIT.java
@Test
public void loginAsInvalidUserFails() {
LoginViewElement loginView = $(LoginViewElement.class).onPage().first();
Assert.assertFalse(loginView.login("user", "invalid"));
}
Continue testing the other views by creating similar view objects and IT classes.
The next chapter covers how to make a production build of the application and deploy it to a cloud platform.
Download free e-book.
The complete guide is also available in an easy-to-follow PDF format.
https://pages.vaadin.com/en/build-a-modern-web-app-with-spring-boot-vaadin-pdf
Show code
render-banner.ts
export class RenderBanner extends HTMLElement {
connectedCallback() {
this.renderBanner();
}
renderBanner() {
let bannerWrapper = document.getElementById('tocBanner');
if (bannerWrapper) {
return;
}
let tocEl = document.getElementById('toc');
// Add an empty ToC div in case page doesn't have one.
if (!tocEl) {
const pageTitle = document.querySelector(
'main > article > header[class^=PageHeader-module--pageHeader]'
);
tocEl = document.createElement('div');
tocEl.classList.add('toc');
pageTitle?.insertAdjacentElement('afterend', tocEl);
}
// Prepare banner container
bannerWrapper = document.createElement('div');
bannerWrapper.id = 'tocBanner';
tocEl?.appendChild(bannerWrapper);
// Banner elements
const text = document.querySelector('.toc-banner-source-text')?.innerHTML;
const link = document.querySelector('.toc-banner-source-link')?.textContent;
const bannerHtml = `<div class='toc-banner'>
<a href='${link}'>
<div class="toc-banner--img"></div>
<div class='toc-banner--content'>${text}</div>
</a>
</div>`;
bannerWrapper.innerHTML = bannerHtml;
// Add banner image
const imgSource = document.querySelector('.toc-banner-source .image');
const imgTarget = bannerWrapper.querySelector('.toc-banner--img');
if (imgSource && imgTarget) {
imgTarget.appendChild(imgSource);
}
}
}