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.
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.
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:
-
start the web container
-
start/deploy the Vaadin app
-
create the webdriver
-
load the web page you want to test
-
validate what you want to validate
-
quit the webdriver
-
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:
-
start the web container
-
start and deploy the vaadin app
-
create the webdriver
-
load the web page you want to test
-
validate what you want to validate
-
quit the webdriver
-
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.