Sven Ruppert

jUnit 5 Extensions

In this tutorial, you will learn how to implement a jUnit 5 extension that is used for testing a Vaadin application with testbench.

Download base project

This tutorial uses the flow-helloworld-maven-meecrowave as a base. Read more about it here

You can find the latest version of the source code for this tutorial @github

Preparations for this tutorial

For this tutorial, we assume to have the following piece of code. We discussed how to test this piece of code in the first part of this tutorial series. You can find the first part https://here.

public class Service {
  public int add(int a, int b){
    return a + b;
  }
}

However, now we want to use this service implementation inside a Vaadin app. For this, we need two input fields, one output field, and a button to start the calculation.

@Route("")
@Theme(value = Lumo.class, variant = Lumo.DARK)
public class VaadinApp extends Composite<Div> {

  //fully static part - layout and elements
  private final TextField tfA = new TextField();
  private final TextField tfB = new TextField();
  private final TextField tfResult = new TextField();
  private final HorizontalLayout inputFields = new HorizontalLayout(
      tfA , new Span("+") , tfB , new Span("=") , tfResult);

  private final Button ok = new Button(getTranslation("btnOK"));

  private final VerticalLayout parentLayout = new VerticalLayout(
      inputFields ,
      ok);

  private final Service service = new Service();

  public VaadinApp() {
    postProcess();
    getContent().add(parentLayout);
  }

  private void postProcess() {
    ok.addClickListener(event -> {
      int result = service
          .add(parseInt(tfA.getValue()) ,
               parseInt(tfB.getValue()));
      tfResult.setValue(valueOf(result));
    });
  }
}

The main question is, how to test the complete app now? Testing the Service class was straight forward. For every execution of a test method we could create per constructor a fresh instance of the Service class. However, now we need a complete web stack.

The main steps are:

  1. start the web container

  2. start/deploy the Vaadin app

  3. create the webdriver

  4. load the web page you want to test

  5. validate what you want to validate

  6. quit the webdriver

  7. stop the web container

The direct way to go

Now, we have a life cycle around a test. There are many more questions, but let’s start with a simplified abstract example. The general idea is that we need a bunch of steps that are in a strict order. Some information is used in more than one step, so we have to decide which steps are needed for every test and which ones are only needed once.

jUnit5 Events and Callbacks

With jUnit5 we got an event based life cycle. There different ways to catch these events. One is the usage of Annotations like it was in jUnit4.

public class LifeCycleTest {

  @BeforeAll
  static void step001(TestReporter testReporter){
    testReporter.publishEntry("step001");
  }

  @BeforeAll
  static void step002(TestReporter testReporter){
    testReporter.publishEntry("step002");
  }

  @BeforeEach
  private void step003(TestReporter testReporter){
    testReporter.publishEntry("step003");
  }

  @BeforeEach
  private void step004(TestReporter testReporter){
    testReporter.publishEntry("step004");
  }


  @AfterAll
  static void step005(TestReporter testReporter){
    testReporter.publishEntry("step005");
  }

  @AfterAll
  static void step006(TestReporter testReporter){
    testReporter.publishEntry("step006");
  }

  @AfterEach
  private void step007(TestReporter testReporter){
    testReporter.publishEntry("step007");
  }

  @AfterEach
  private void step008(TestReporter testReporter){
    testReporter.publishEntry("step008");
  }

  @Test
  void test001(TestReporter testReporter){
    testReporter.publishEntry("test001");
  }
}

The output will be the following:

timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step001
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step002
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step003
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step004
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = test001
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step007
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step008
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step005
timestamp = XXX-XX-XXTXX:XX:XX.XX, value = step006
Process finished with exit code 0

What you can see here is that the steps are in a logical order based on lifecycle step and position inside the class. However, don’t rely on the order of the execution from the same lifecycle step. If you have multiple methods with the same annotation, the execution order is not guaranteed. Same if we are dealing with inheritance!

Guaranteed is only the order of the lifecycle step types. The lifecycle of a test is more complicated as it shows until now. The complete list of events is the following:

  • BeforeAllCallback

  • BeforeEachCallback

  • BeforeTestExecutionCallback

  • AfterTestExecutionCallback

  • AfterEachCallback

  • AfterAllCallback

This lifecycle could change, for sure, so may you should have a look at the original documentation as well. The actual documentation you can find here.

All of these callbacks are defined in a functional interface. To deal with this, you have to implement the corresponding interface.

The next question is, what is the right place for the implementation?

Inheritance

One way could be the inheritance directly inside the test class. For sure, you could extract this into a parent class. However, in the end, it is leading to a more complex inheritance structure for your test classes itself. However, let’s see how it works, first.

public class LifeCycleInheritanceTest implements
    BeforeAllCallback,
    BeforeEachCallback,
    BeforeTestExecutionCallback,
    AfterTestExecutionCallback,
    AfterEachCallback,
    AfterAllCallback {
  @Override
  public void afterAll(ExtensionContext ctx) throws Exception {

  }

  @Override
  public void afterEach(ExtensionContext ctx) throws Exception {

  }

  @Override
  public void afterTestExecution(ExtensionContext ctx) throws Exception {

  }

  @Override
  public void beforeAll(ExtensionContext ctx) throws Exception {

  }

  @Override
  public void beforeEach(ExtensionContext ctx) throws Exception {

  }

  @Override
  public void beforeTestExecution(ExtensionContext ctx) throws Exception {

  }
}

For every callback, there is a method to implement, the param of type ExtensionContext will give you the possibility to share information between callbacks. Don’t use the instance of the implementation itself to hold pieces of information!

For example: How to share an instance of a class between beforeEach and afterEach ?

For this functionality, we need to implement the two listed interfaces,

  • BeforeEachCallback

  • AfterEachCallback

  public class MyExtension implements
      BeforeEachCallback,
      AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext ctx) throws Exception {
      final List<String> values = new ArrayList<>();
      values.add("something magic");
      ctx
          .getStore(ExtensionContext.Namespace.create("my-storage"))
          .put("instance" , values);
    }

    @Override
    public void afterEach(ExtensionContext ctx) throws Exception {
      final List<String> values = ctx
          .getStore(ExtensionContext.Namespace.create("my-storage"))
          .get("instance" , List.class);

      values.forEach(System.out::println);
    }
  }

This extension is a regular class that is implementing the two lifecycle interfaces. Every method will have the parameter of type ExtensionContext. This context is something like a Map that is managed by the test engine and shared between the lifecycle callbacks from a test method.

Before using the extension, it must be registered. There are several ways to do this, and the different ways are still part of the development. The secure way right now is the usage and registration via annotations.

The test method or the test class that should use the extension must be annotated with the annotation called ExtendWith. The following example demonstrates the usage at the method level.

  @Test
  @ExtendWith(MyExtension.class)
  void test001() {
        //some usefull tests here
  }

Extensions Demo

Remember what we need for testing a web app based on Vaadin Flow.

The main steps are:

  1. start the web container

  2. start and deploy the vaadin app

  3. create the webdriver

  4. load the web page you want to test

  5. validate what you want to validate

  6. quit the webdriver

  7. stop the web container

Now, we can implement a test that can ramp up the infrastructure, testing the app also, shutting down everything.

As preparation, we are doing all the necessary things inside the test itself.

public class VaadinAppTest extends TestBenchTestCase {

  @Test
  void test001() {
    BasicTestUIRunner.start();

    System.setProperty("webdriver.chrome.driver",
                       "_data/webdrivers/chromedriver-mac-64bit");

    final ChromeDriver webDriver = new ChromeDriver();
    setDriver(webDriver);

    getDriver().get("http://localhost:8080/");

    final TextFieldElement tfA = $(TextFieldElement.class).id(VaadinApp.TF_A);
    final TextFieldElement tfB = $(TextFieldElement.class).id(VaadinApp.TF_B);
    final TextFieldElement tfResult = $(TextFieldElement.class).id(VaadinApp.TF_RESULT);

    tfA.setValue("2");
    tfB.setValue("2");

    ButtonElement btnOk = $(ButtonElement.class).first();
    btnOk.click();

    final String result = tfResult.getValue();

    // Check the the value of the button is "Clicked"
    Assertions.assertEquals("4" , result);

    getDriver().quit();
    BasicTestUIRunner.stop();
  }
}

Now, we start shifting one step into an Extension. For this, we create a class with the name ContainerExtension For every test class, we want to start and stop the Servlet-Container. All tests inside a test class should use the same deployment. The right lifecycle callback pair for this is Before-/AfterAllCallback.

public class ContainerExtension
       implements BeforeAllCallback ,
                  AfterAllCallback {
  @Override
  public void afterAll(ExtensionContext ctx) throws Exception {
    BasicTestUIRunner.stop();
  }

  @Override
  public void beforeAll(ExtensionContext ctx) throws Exception {
    BasicTestUIRunner.start();
  }
}

To activate the extension, the test class VaadinAppExtensionsTest must be annotated with @ExtendWith(ContainerExtension.class) After cleaning up the test method, removing the statements that were used to start-/stop the container, we could start using the test itself.

Conclusion

With this extension, we can manage a basic part of the needed steps to test a web application based on Flow. However, this is must be more specific if we want to be able to run tests in parallel.

We’ll discuss how to achieve this in the next part of the series.

Vaadin is an open-source framework offering the fastest way to build web apps on Java backends
GET STARTED

Comments (0)