Helidon SE/MP Testing with JUnit 5 and Helidon Test Support

Helidon SE/MP Testing with JUnit 5 and Helidon Test Support

Helidon is Oracle's open-source Java microservices framework that comes in two flavors: Helidon SE (reactive, functional, no magic) and Helidon MP (MicroProfile-compliant, CDI, JAX-RS). Each flavor has its own testing approach, but both integrate cleanly with JUnit 5 and support Testcontainers for infrastructure dependencies.

Understanding the Two Testing Models

Helidon SE operates on a reactive event loop (Netty-based). Tests need to start the server, make HTTP requests, and assert responses — there's no annotation-driven injection framework to help wire things together.

Helidon MP follows MicroProfile conventions, meaning CDI, JAX-RS, and the familiar @Inject annotation work as expected. The test support library mirrors what you'd expect from a CDI-aware framework.

Testing Helidon SE Applications

Setting Up Dependencies

<dependency>
    <groupId>io.helidon.webserver.testing.junit5</groupId>
    <artifactId>helidon-webserver-testing-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.helidon.webclient</groupId>
    <artifactId>helidon-webclient</artifactId>
    <scope>test</scope>
</dependency>

Writing a Helidon SE Test

Helidon SE's @ServerTest (or @RoutingTest in newer versions) starts a real server on a random port:

@ServerTest
class ItemServiceTest {

    @SetUpRoute
    static void routing(HttpRouting.Builder routing) {
        routing.register("/items", new ItemService());
    }

    @Test
    void testGetItem(WebClient client) {
        ClientResponseTyped<Item> response = client
            .get("/items/123")
            .request(Item.class);

        assertThat(response.status()).isEqualTo(Status.OK_200);
        assertThat(response.entity().getId()).isEqualTo("123");
    }

    @Test
    void testItemNotFound(WebClient client) {
        ClientResponseTyped<JsonObject> response = client
            .get("/items/nonexistent")
            .request(JsonObject.class);

        assertThat(response.status()).isEqualTo(Status.NOT_FOUND_404);
    }
}

The WebClient parameter is injected by Helidon's test extension — it's pre-configured with the test server's base URL. No manual port discovery needed.

Testing POST Requests in Helidon SE

@ServerTest
class OrderHandlerTest {

    @SetUpRoute
    static void routing(HttpRouting.Builder routing) {
        routing.register("/orders", new OrderHandler());
    }

    @Test
    void testCreateOrder(WebClient client) {
        OrderRequest request = new OrderRequest("sku-101", 3);
        
        ClientResponseTyped<Order> response = client
            .post("/orders")
            .submit(request, Order.class);

        assertThat(response.status()).isEqualTo(Status.CREATED_201);
        assertThat(response.entity()).satisfies(order -> {
            assertThat(order.getId()).isNotNull();
            assertThat(order.getQuantity()).isEqualTo(3);
        });
    }

    @Test
    void testCreateOrderInvalidRequest(WebClient client) {
        // Invalid — missing required fields
        String invalidBody = "{}";
        
        ClientResponseTyped<ErrorResponse> response = client
            .post("/orders")
            .contentType(MediaTypes.APPLICATION_JSON)
            .submit(invalidBody, ErrorResponse.class);

        assertThat(response.status()).isEqualTo(Status.BAD_REQUEST_400);
        assertThat(response.entity().getMessage()).contains("required");
    }
}

Testing Services Directly (Unit Tests)

Helidon SE services can also be tested without starting an HTTP server:

class PricingServiceTest {

    private PricingService pricingService;

    @BeforeEach
    void setUp() {
        pricingService = new PricingService(new InMemoryProductCatalog());
    }

    @Test
    void testCalculatePrice() {
        Price price = pricingService.calculate("product-A", 5);
        
        assertThat(price.getTotal()).isEqualByComparingTo(BigDecimal.valueOf(49.95));
        assertThat(price.getCurrency()).isEqualTo("USD");
    }

    @Test
    void testBulkDiscount() {
        Price price = pricingService.calculate("product-A", 100);
        
        assertThat(price.getDiscountPercentage()).isGreaterThanOrEqualTo(10);
    }
}

Testing Helidon MP Applications

Setting Up Helidon MP Test Dependencies

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

@HelidonTest for MicroProfile Testing

@HelidonTest starts a full MicroProfile container and enables CDI injection in tests:

@HelidonTest
class GreetResourceTest {

    @Inject
    WebTarget webTarget;

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

        assertThat(response.getString("message"))
            .isEqualTo("Hello World!");
    }

    @Test
    void testCustomGreet() {
        JsonObject response = webTarget
            .path("/greet/Alice")
            .request()
            .get(JsonObject.class);

        assertThat(response.getString("message"))
            .isEqualTo("Hello Alice!");
    }
}

Injecting CDI Beans in Tests

Because @HelidonTest boots a CDI container, you can inject application beans directly:

@HelidonTest
class InventoryServiceTest {

    @Inject
    InventoryService inventoryService;

    @Test
    void testAddItem() {
        inventoryService.add(new Item("widget", 50));
        
        assertThat(inventoryService.getCount("widget")).isEqualTo(50);
    }

    @Test
    void testReduceStock() {
        inventoryService.add(new Item("gadget", 20));
        inventoryService.reduce("gadget", 5);
        
        assertThat(inventoryService.getCount("gadget")).isEqualTo(15);
    }
}

Using Alternatives for Mocking in Helidon MP

CDI alternatives allow you to replace production beans with test doubles:

// Test alternative
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
public class MockWeatherService implements WeatherService {

    @Override
    public WeatherData getCurrentWeather(String city) {
        return new WeatherData(city, 22.0, "Sunny");
    }
}

// Test class using the alternative
@HelidonTest
@AddBean(MockWeatherService.class)
class WeatherResourceTest {

    @Inject
    WebTarget webTarget;

    @Test
    void testGetWeather() {
        JsonObject response = webTarget
            .path("/weather/London")
            .request()
            .get(JsonObject.class);

        assertThat(response.getString("condition")).isEqualTo("Sunny");
        assertThat(response.getJsonNumber("temperature").doubleValue()).isEqualTo(22.0);
    }
}

@AddBean is a Helidon extension for including additional CDI beans in tests without modifying beans.xml.

Configuration Override in Tests

Override MicroProfile Config properties per test:

@HelidonTest
@AddConfig(key = "greeting.prefix", value = "Hi")
@AddConfig(key = "feature.experimental", value = "true")
class ConfigurableGreetingTest {

    @Inject
    WebTarget webTarget;

    @Test
    void testCustomPrefix() {
        JsonObject response = webTarget
            .path("/greet/Bob")
            .request()
            .get(JsonObject.class);

        assertThat(response.getString("message"))
            .isEqualTo("Hi Bob!");
    }
}

Integration Testing with Testcontainers

Both SE and MP flavors work with Testcontainers:

@HelidonTest
class UserRepositoryIntegrationTest {

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb");

    @BeforeAll
    static void startContainers() {
        postgres.start();
        // Helidon MP reads from System properties or microprofile-config
        System.setProperty("javax.sql.DataSource.test.dataSourceClassName",
            "org.postgresql.ds.PGSimpleDataSource");
        System.setProperty("javax.sql.DataSource.test.dataSource.url",
            postgres.getJdbcUrl());
        System.setProperty("javax.sql.DataSource.test.dataSource.user",
            postgres.getUsername());
        System.setProperty("javax.sql.DataSource.test.dataSource.password",
            postgres.getPassword());
    }

    @AfterAll
    static void stopContainers() {
        postgres.stop();
    }

    @Inject
    UserRepository userRepository;

    @Test
    void testPersistUser() {
        User user = new User("carol@example.com", "Carol");
        userRepository.save(user);
        
        Optional<User> found = userRepository.findByEmail("carol@example.com");
        assertThat(found).isPresent();
    }
}

Testing Health Checks and Metrics

Helidon MP includes built-in MicroProfile Health and Metrics support. Test them like any other endpoint:

@HelidonTest
class HealthEndpointTest {

    @Inject
    WebTarget webTarget;

    @Test
    void testLivenessCheck() {
        Response response = webTarget
            .path("/health/live")
            .request()
            .get();

        assertThat(response.getStatus()).isEqualTo(200);
        
        JsonObject body = response.readEntity(JsonObject.class);
        assertThat(body.getString("status")).isEqualTo("UP");
    }

    @Test
    void testReadinessCheck() {
        Response response = webTarget
            .path("/health/ready")
            .request()
            .get();

        assertThat(response.getStatus()).isEqualTo(200);
    }
}

Testing MicroProfile REST Client

When your Helidon service calls other services via @RegisterRestClient, use CDI alternatives or WireMock for tests:

@RegisterRestClient(baseUri = "http://products-service")
public interface ProductClient {
    @GET
    @Path("/{id}")
    Product getProduct(@PathParam("id") String id);
}

@Alternative
@Priority(Interceptor.Priority.APPLICATION)
@ApplicationScoped
@RestClient
public class MockProductClient implements ProductClient {
    @Override
    public Product getProduct(String id) {
        return new Product(id, "Mock Product", 9.99);
    }
}

@HelidonTest
@AddBean(MockProductClient.class)
class OrderResourceWithMockClientTest {
    // Your tests here
}

Running Helidon Tests in CI

Helidon tests are standard JUnit 5 — they work with Maven Surefire or Gradle's test task:

# Maven
mvn <span class="hljs-built_in">test

<span class="hljs-comment"># Gradle
gradle <span class="hljs-built_in">test

<span class="hljs-comment"># Run only integration tests
mvn verify -Pintegration-tests

For native image testing with Helidon's GraalVM support:

mvn package -Pnative-image
./target/helidon-service

Going Beyond Unit Tests

Helidon's test framework ensures your service logic is correct. But verifying complete user flows — that a multi-service transaction completes successfully, that your health endpoints respond correctly under load — requires a behavioral testing layer.

HelpMeTest provides 24/7 monitoring for your Helidon services. Use helpmetest health helidon-api 30s to catch downtime instantly, and write plain-English tests that verify your API contracts without needing deep framework knowledge.

Summary

  • Helidon SE: use @ServerTest with injected WebClient — no CDI, pure functional service testing
  • Helidon MP: use @HelidonTest with injected WebTarget — full CDI container with JAX-RS
  • @AddBean and CDI @Alternative replace beans in tests without modifying production config
  • @AddConfig overrides MicroProfile Config properties per test class
  • Both flavors integrate with Testcontainers for real infrastructure in tests
  • Health check endpoints are just HTTP — test them like any other endpoint

Read more