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.

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);
    }
}
  1. Start by invoking the WebDriverManager before any test method is invoked. TestBench does not invoke the web driver manager.

  2. 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.

  3. 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.

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 LoginTest instead, it will be run as a unit test and the server will not be started and the test will fail.

Tip
Start the server separately to speed up tests

While developing tests, it is not very efficient to run the tests as mvn -Pit verify. Instead, you can start the server manually by launching the Application class or with spring-boot:run. You can then execute the selected test in your IDE and you do not have to wait for the server to start every time.

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:

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 (public class LoginView extends VerticalLayout vs public class LoginViewElement extends VerticalLayoutElement).

Adding the @Attribute(name = "class", contains = "login-view") annotation allows you to find the LoginViewElement using the TestBench query API, for example:

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:

@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:

@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.

Open in a
new tab
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);
    }
  }
}