This guide describes how to write and execute tests for your MicroProfile applications in a JUnit 5 environment using optimized customizations.

What You Need

For this 20 minute tutorial, you will need the following:

Table 1. Prerequisite product versions for Helidon 4.3.0-SNAPSHOT

Java SE 21 (Open JDK 21)

Helidon requires Java 21+ (25+ recommended).

Maven 3.8+

Helidon requires Maven 3.8+.

Docker 18.09+

If you want to build and run Docker containers.

Kubectl 1.16.5+

If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop.

Verify Prerequisites
java -version
mvn --version
docker --version
kubectl version
Setting JAVA_HOME
# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 21`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-21

Dependencies

To start using this feature, add the following dependencies to the testing module:

Maven dependencies
<dependencies>
   <dependency>
      <groupId>io.helidon.microprofile.testing</groupId>
      <artifactId>helidon-microprofile-testing-junit5</artifactId>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
   </dependency>
</dependencies>

Create a Sample Helidon MP Project

In this guide we will use the Helidon MP Quickstart project in our examples.

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

Create a Test Class

First you’ll need to create a test class with an empty test method, and annotate it with @HelidonTest:

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

The @HelidonTest annotation will cause the test extension to start a Helidon MicroProfile server so that you do not need to manage the server lifecycle in your test. The container is initialized once before the test class is instantiated, and shut down after the last test runs.

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).
Note
The @HelidonTest annotation uses a random port regardless of the port configured in the application.yaml.

Inject a WebTarget

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 with webTarget
@HelidonTest
class GreetTest {
    @Inject
    WebTarget webTarget;

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

        assertThat(jsonObject.getString("message"), is("Hello World!"));
    }
}

The test is now complete and verifies the message.

Customize the Testing Extension

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

Table 2. Optional Extension Annotations
Annotation Description

@HelidonTest(resetPerTest = true)

Resets the container for each method. 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.

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

Define additional configuration (either on class level, or method level) by adding a single configuration key/value.

@AddConfigBlock(type = "properties", value = """
some.key1=some.value1
some.key2=some.value2
""")

Define additional configuration (either on class level, or method level) by adding one or more configuration key/value pairs.

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

Adds a whole config source from classpath.

Here’s an example showing how these approaches are used 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 message = jsonObject.getString("message");
        assertThat(message, is("Message in JSON"));
    }
}

@HelidonTest
@AddConfigBlock("""
        some.key1=some.value1
        some.key2=some.value2
    """)
class AddConfigBlockTest {

    @Inject
    @ConfigProperty(name = "some.key1")
    private String value1;

    @Inject
    @ConfigProperty(name = "some.key2")
    private String value2;

    @Test
    void testValue() {
        assertThat(value1, is("some.value1"));
        assertThat(value2, is("some.value2"));
    }
}

Use Beans for Testing

If you prefer to use only beans for testing, and want to add a different bean for each test, then you must use the @AddBean annotation. This cannot be achieved by CDI discovery because if we place META-INF/beans.xml on the classpath, then all of our beans would be added.

@AddBean(TestBean.class)
class GreetTest {
}

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

@AddBean(value = TestBean.class, scope = Dependent.class)
class GreetTest {
}
Note
This annotation can also be placed on a method when running in resetPerTest mode.

Add Test Extension

When a custom bean is not enough, you may want to extend the 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 which adds the extension to the container and allows you to modify its behavior as a usual CDI Portable Extension:

@AddExtension(TestExtension.class)
class GreetTest {
}

Disable Discovery

If you want to disable discovery and only add custom extensions and beans, then use the following annotation:

@DisableDiscovery
class GreetTest {
}
Note
This annotation is typically used in conjunction with @AddBeans and/or @AddExtension. As you have seen in standard test output, by default Helidon starts with the dependencies defined in pom.xml.

Write a Basic Test

If you want just the basic test features enabled, then you only have to add a few required extensions and classes to your test. The following example uses only those extensions and classes required to run a bean that injects configuration value:

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

    @Test
    void testBean() {
        assertThat(bean.message(), is("Hello Guide!"));
    }

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

        String message() {
            return message;
        }
    }
}

Summary

This guide demonstrated how to create tests for MicroProfile applications in a JUnit 5 environment. It described some useful customizations that can be added to your testing extension and allow you to configure test outcomes for your Helidon MP applications.

Refer to the following references for additional information: