Overview
Helidon provides a TestNG listener that integrates CDI to support testing with Helidon MP.
The test class is added as a CDI bean to support injection and the CDI container is started lazily during test execution.
Maven Coordinates
To enable Testing with TestNG,
add the following dependency to your project’s pom.xml (see
Managing Dependencies).
<dependency>
<groupId>io.helidon.microprofile.testing</groupId>
<artifactId>helidon-microprofile-testing-testng</artifactId>
<scope>test</scope>
</dependency>
Usage
@HelidonTest // (1)
class MyTest {
}
-
Enable the test class
|
Note
|
By default, a MicroProfile Config profile named "test" is defined. It can be changed via:
|
CDI Container Setup
By default, CDI discovery is enabled:
-
CDI beans and extensions in the classpath are added automatically
-
If disabled, the CDI beans and extensions must be added manually
|
Note
|
Customization of the CDI container on a test method changes the CDI container affinity. I.e. The test method will use a dedicated CDI container. |
|
Note
|
It is not recommended to provide a Instead, you should use |
CDI discovery can be disabled using @DisableDiscovery.
@DisableDiscovery // (1)
@AddBean(MyBean.class) // (2)
@HelidonTest
class MyTest {
}
-
Disable CDI discovery
-
Add a bean class
When disabling discovery, it can be difficult to identify the CDI extensions needed to activate the desired features.
JAXRS (Jersey) support can be added easily using @AddJaxRs.
@DisableDiscovery
@AddJaxRs // (1)
@AddBean(MyResource.class) // (2)
@HelidonTest
class MyTest {
}
-
Add JAX-RS (Jersey) support
-
Add a resource class to the CDI container
Note the following Helidon CDI extensions:
| Extension | Note |
|---|---|
Add MicroProfile Config injection support |
|
Optional if using |
|
Optional if using |
CDI Container Afinity
By default, one CDI container is created per test class and is shared by all test methods.
However, test methods can also require a dedicated CDI container:
-
By forcing a reset of the CDI container between methods
-
By customizing the CDI container per test method
@HelidonTest(resetPerTest = true)
class MyTest {
@Test
void testOne() { // (1)
}
@Test
void testTwo() { // (2)
}
}
-
testOneexecutes in a dedicated CDI container -
testTwoalso executes in a dedicated CDI container
@HelidonTest
class MyTest {
@Test
void testOne() { // (1)
}
@Test
@DisableDiscovery
@AddBean(MyBean.class)
void testTwo() { // (2)
}
}
-
testOneexecutes in the shared CDI container -
testTwoexecutes in a dedicated CDI container
Configuration
The test configuration can be set up in two exclusive ways:
-
Using the "synthetic" configuration expressed with annotations (default)
-
Using the "existing" configuration of the current environment
Use @Configuration to switch to the "existing" configuration.
@Configuration(useExisting = true)
@HelidonTest
class MyTest {
}
|
Note
|
Customization of the test configuration on a test method changes the CDI container affinity. I.e. The test method will use a dedicated CDI container. |
Synthetic Configuration
The "synthetic" configuration can be expressed using the following annotations:
| Type | Usage |
|---|---|
Key value pair |
|
Formatted text block |
|
Programmatic config source |
|
Classpath resources using |
@AddConfig(key = "foo", value = "bar")
@HelidonTest
class MyTest {
}
@AddConfigBlock("""
foo=bar
bob=alice
""")
@HelidonTest
class MyTest {
}
@AddConfigBlock(type = "yaml", value = """
my-test:
foo: bar
bob: alice
""")
@HelidonTest
class MyTest {
}
@HelidonTest
class MyTest {
@AddConfigSource
static ConfigSource config() {
return MpConfigSources.create(Map.of(
"foo", "bar",
"bob", "alice"));
}
}
@Configuration(configSources = {
"my-test1.yaml",
"my-test2.yaml"
})
@HelidonTest
class MyTest {
}
Configuration Ordering
The ordering of the test configuration can be controlled using the mechanism defined by the MicroProfile Config specification.
@AddConfigBlock(value = """
config_ordinal=120
foo=bar
""")
@HelidonTest
class MyTest {
}
The default ordering is the following
| Annotation | Ordinal |
|---|---|
1000 |
|
900 |
|
800 |
|
700 |
Injectable Types
Helidon provides injection support for types that reflect the current server. E.g. JAXRS client.
Here are all the built-in types that can be injected:
| Type | Usage |
|---|---|
A JAX-RS client configured for the current server. |
|
|
A URI representing the current server |
|
A raw URI representing the current server |
The current CDI container instance |
|
Note
|
Types that reflect the current server require ServerCdiExtension
|
@HelidonTest
class MyTest {
@Inject
WebTarget target;
}
Use @Socket to specify the socket for the clients and URIs.
@HelidonTest
class MyTest {
@Inject
@Socket("admin")
WebTarget target;
}
Test Instance Lifecyle
The test instance lifecycle is a pseudo singleton that follows the lifecycle of the CDI container.
I.e. By default, the test instance is re-used between test methods.
|
Note
|
The test instance is not re-used between CDI container, using a dedicated CDI container implies a new test instance |
Using meta-annotations
Meta-annotations are supported on both test classes and test methods and can be used as a composition mechanism.
@HelidonTest
@AddBean(FirstBean.class)
@AddBean(SecondBean.class)
@DisableDiscovery
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomMetaAnnotation {
}
@CustomMetaAnnotation
class AnnotationOnClass {
}
@AddBean(FirstBean.class)
@AddBean(SecondBean.class)
@DisableDiscovery
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTestMethod {
}
@HelidonTest
class AnnotationOnMethod {
@Test // (1)
@MyTestMethod
void testOne() {
}
@Test // (1)
@MyTestMethod
void testTwo() {
}
}
-
org.testng.annotations.Testis not inheritable and should be placed on methods
API
Here is a brief overview of the MicroProfile testing annotations:
| Annotation | Usage |
|---|---|
Add a CDI bean class to the CDI container |
|
Add a CDI extension to the CDI container |
|
Disable automated discovery of beans and extensions |
|
Shorthand to add JAX-RS (Jersey) support |
|
Define a key value pair in the "synthetic" configuration |
|
Define a formatted text block in the "synthetic" configuration |
|
Add a programmatic config source to the "synthetic" configuration |
|
Switch between "synthetic" and "existing" ; Add classpath resources to the "synthetic" configuration |
|
CDI qualifier to inject a JAX-RS client or URI for a named socket |
|
Mark a static method to be executed after the container is stopped |
Examples
Config Injection Example
The following example demonstrates how to enable the use of
@ConfigProperty without CDI discovery.
@HelidonTest
@DisableDiscovery // (1)
@AddBean(MyBean.class) // (2)
@AddExtension(ConfigCdiExtension.class) // (3)
@AddConfig(key = "app.greeting", value = "TestHello") // (4)
class MyTest {
@Inject
MyBean myBean;
@Test
void testGreeting() {
assertThat(myBean, notNullValue());
assertThat(myBean.greeting(), is("TestHello"));
}
}
@ApplicationScoped
class MyBean {
@ConfigProperty(name = "app.greeting") // (5)
String greeting;
String greeting() {
return greeting;
}
}
-
CDI discovery is disabled
-
Add
MyBeanto the CDI container -
Add
ConfigCdiExtensionto the CDI container -
Define test configuration
-
Inject the configuration
Request Scope Example
The following example demonstrates how to use @RequestScoped with
JAXRS without CDI discovery.
@HelidonTest
@DisableDiscovery // (1)
@AddJaxRs // (2)
@AddBean(MyResource.class) // (3)
class MyTest {
@Inject
WebTarget target;
@Test
void testGet() {
String greeting = target.path("/greeting")
.request().get(String.class);
assertThat(greeting, is("Hallo!"));
}
}
@Path("/greeting")
@RequestScoped
class MyResource {
@GET
Response get() {
return Response.ok("Hallo!").build();
}
}
-
CDI discovery is disabled
-
Add JAXRS (Jersey) support
-
Add
MyResourceto the CDI container
Mock Support
Mocking in Helidon MP is all about replacing CDI beans with instrumented mock classes.
This can be done using CDI alternatives, however Helidon provides an annotation to make it easy.
Maven Coordinates
To enable mock mupport add the following dependency to your project’s pom.xml.
<dependency>
<groupId>io.helidon.microprofile.testing</groupId>
<artifactId>helidon-microprofile-testing-mocking</artifactId>
<scope>test</scope>
</dependency>
Usage
Use the @MockBean annotation to inject an instrumented CDI bean in your test,
and customize it in the test method.
Example
@MockBean@HelidonTest
@AddBean(MyResource.class)
@AddBean(MyService.class)
class MyTest {
@MockBean(answer = Answers.CALLS_REAL_METHODS) // (1)
MyService myService;
@Inject
WebTarget target;
@Test
void testService() {
Mockito.when(myService.test()).thenReturn("Mocked"); // (2)
String response = target.path("/test").request().get(String.class);
assertThat(response, is("Mocked"));
}
}
@Path("/test")
class MyResource {
@Inject
MyService myService;
@GET
String test() {
return myService.test();
}
}
@ApplicationScoped
class MyService {
String test() {
return "Not Mocked";
}
}
-
Instrument
MyServiceusingAnswers.CALLS_REAL_METHODS -
Customize the behavior
Using CDI Alternative
@Alternative can be used to replace a CDI bean with an instrumented
instance.
@HelidonTest
@Priority(1) // (3)
class MyTest {
@Inject
WebTarget target;
MyService myService;
@BeforeMethod
void initMock() {
myService = Mockito.mock(MyService.class, Answers.CALLS_REAL_METHODS); // (1)
}
@Produces
@Alternative // (2)
MyService mockService() {
return myService;
}
@Test
void testService() {
Mockito.when(myService.test()).thenReturn("Mocked"); // (4)
Response response = target.path("/test").request().get();
assertThat(response, is("Mocked"));
}
}
@Path("/test")
class MyResource {
@Inject
MyService myService;
@GET
String test() {
return myService.test();
}
}
@ApplicationScoped
class MyService {
String test() {
return "Not Mocked";
}
}
-
Create the mock instance in the test class
-
Create a CDI producer method annotated with
@Alternative -
Set priority to 1 (required by
@Alternative) -
Customize the behavior
Virtual Threads
Virtual Threads pinning can be detected during tests.
A virtual thread is "pinning" when it blocks its carrier thread in a way that prevents the virtual thread scheduler from scheduling other virtual threads.
This can happen when blocking in native code, or prior to JDK24 when a blocking IO operation happens in a synchronized block.
Pinning can in some cases negatively affect application performance.
@HelidonTest(pinningDetection = true)
class MyTest {
}
Pinning is considered harmful when it takes longer than 20 milliseconds, that is also the default when detecting it within tests.
Pinning threshold can be changed with:
@HelidonTest(pinningDetection = true, pinningThreshold = 50) // (1)
class MyTest {
}
-
Change pinning threshold from default(20) to 50 milliseconds.
When pinning is detected, the test fails with a stacktrace pointing at the culprit.