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-testsFor native image testing with Helidon's GraalVM support:
mvn package -Pnative-image
./target/helidon-serviceGoing 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
@ServerTestwith injectedWebClient— no CDI, pure functional service testing - Helidon MP: use
@HelidonTestwith injectedWebTarget— full CDI container with JAX-RS @AddBeanand CDI@Alternativereplace beans in tests without modifying production config@AddConfigoverrides 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