Testing Helidon

Tomas Langer
Helidon
Published in
4 min readOct 19, 2020

With the latest release of Helidon, we have added a long awaited feature — support for testing MicroProfile applications in JUnit 5 environment, with useful customizations.

To start using this feature, add a dependency to the testing module:

<dependency>
<groupId>io.helidon.microprofile.tests</groupId>
<artifactId>helidon-microprofile-tests-junit5</artifactId>
<scope>test</scope>
</dependency>

Now it is time to write your test.

Let's start with a simple application, such as https://helidon.io/docs/v2/#/mp/guides/02_quickstart

This application provides an endpoint /greet, and we want to make sure this endpoint is available and returns expected value.

We create a test class with an empty test method, and simply annotate it with @HelidonTest :

import io.helidon.microprofile.tests.junit5.HelidonTest;

import org.junit.jupiter.api.Test;

@HelidonTest
class GreetTest {
@Test
void testDefaultGreeting() {
}
}

The @HelidonTest annotation will cause the test extension to start a Helidon microprofile server for you, so that you do not need to manage the server lifecycle in your test. You can see this in the test output:

INFO io.helidon.microprofile.server.ServerCdiExtension: Server started on http://localhost:56293 (and all other host addresses) in 1893 milliseconds (since JVM startup).

We can see that we used a random port (even though the application starts on port 8080 by default).

The container is initialized once before the test class is instantiated, and shut down after the last test runs.

The test is only useful if it invokes the server and verifies the result. To support testing you can inject a WebTarget that is configured for the currently running server (it can also be a parameter to a test method). We can use the target to invoke our endpoint and validate the result.

Updated class:

import static org.junit.jupiter.api.Assertions.assertEquals;@HelidonTest
class GreetTest {
@Inject
WebTarget webTarget;

@Test
void testDefaultGreeting() {
JsonObject jsonObject = webTarget.path("/greet")
.request()
.get(JsonObject.class);

String expected = "Hello World!";
String actual = jsonObject.getString("message");
assertEquals(expected, actual, "Message in JSON");
}
}

The test is now complete and verifies the message.

The testing extension supports a few additional tools that allow for finer control of the test execution.

Let's dive into the details!

We can define the following to reset the container for each method:

@HelidonTest(resetPerTest = true)

This is useful when we want to modify configuration or beans between executions. In such a case, injection into fields is not possible, as we would need a different instance for each test.

We can also define a new configuration (either on class level, or method level) using

@AddConfig(key = "app.greeting", value = "Unite")

adding a single configuration key/value, and

@Configuration(configSources = "test-config.properties")

add a whole config source from classpath.

Let's combine these two approaches to execute the same endpoint with different configuration:

@HelidonTest(resetPerTest = true)
class GreetTest {
@Test
void testDefaultGreeting(WebTarget webTarget) {
validate(webTarget, "/greet", "Hello World!");
}

@Test
@AddConfig(key = "app.greeting", value = "Unite")
void testConfiguredGreeting(WebTarget webTarget) {
validate(webTarget, "/greet", "Unite World!");
}

private void validate(WebTarget webTarget,
String path,
String expected) {

JsonObject jsonObject = webTarget.path(path)
.request()
.get(JsonObject.class);

String actual = jsonObject.getString("message");
assertEquals(expected, actual, "Message in JSON");
}
}

Now let's assume we need to use beans only for testing, and probably a different bean for each test. This could not be achieved by CDI discovery, because if we place META-INF/beans.xml on the classpath, then all of our beans would be added.

For this purpose, we provide the following annotation:

@AddBean(TestBean.class)

The bean is added to the container, with ApplicationScoped scope by default. You can customize scope either by annotating the bean class with another scope or through the annotation:

@AddBean(value = TestBean.class, scope = Dependent.class)

This annotation can also be placed on a method when running in resetPerTest mode.

Sometimes a custom bean is not enough and we need to extend CDI with a test only Extension. Once again, if we use the standard way of doing this, we would need to create a META-INF/services record that would be picked up by every test class.

For this purpose, we provide the following annotation:

@AddExtension(TestExtension.class)

The extension is added to the container and can modify its behavior as a usual CDI Portable Extension

And last, for the most custom scenarios, we may want to disable discovery and only add custom extensions and beans.

For this purpose, you can use the following annotation:

@DisableDiscovery

This annotation is typically used in conjunction with AddBeans and/or AddExtension. As you have seen in standard output of the test, Helidon starts with the full MicroProfile features enabled.

Let's say we only want the basic features enabled.The following test is using just the required extensions and classes to run a bean that injects configuration value:

import javax.inject.Inject;

import io.helidon.microprofile.config.ConfigCdiExtension;
import io.helidon.microprofile.tests.junit5.AddBean;
import io.helidon.microprofile.tests.junit5.AddConfig;
import io.helidon.microprofile.tests.junit5.AddExtension;
import io.helidon.microprofile.tests.junit5.DisableDiscovery;
import io.helidon.microprofile.tests.junit5.HelidonTest;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

@HelidonTest
@DisableDiscovery
@AddExtension(ConfigCdiExtension.class)
@AddBean(GreetTest.ConfiguredBean.class)
@AddConfig(key = "test.message", value = "Hello Blog!")
class GreetTest {
@Inject
ConfiguredBean bean;

@Test
void testBean() {
assertEquals("Hello Blog!", bean.message());
}

public static class ConfiguredBean {
@Inject
@ConfigProperty(name = "test.message")
private String message;

String message() {
return message;
}
}
}

--

--