Testing Helidon
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;
}
}
}