Contents

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

Basic usage
@HelidonTest // (1)
class MyTest {
}
  1. Enable the test class

Note

By default, a MicroProfile Config profile named "test" is defined.

It can be changed via:

  • @AddConfig(key = "mp.config.profile", value = "otherProfile")

  • @Configuration(profile = "otherProfile")

  • Using mp.config.profile property and @Config(useExisting = true)

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 beans.xml along the test classes, as it would combine beans from all tests.

Instead, you should use @AddBean to specify the beans per test or method.

CDI discovery can be disabled using @DisableDiscovery.

Disable discovery
@DisableDiscovery // (1)
@AddBean(MyBean.class) // (2)
@HelidonTest
class MyTest {
}
  1. Disable CDI discovery

  2. 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.

Add JAX-RS (Jersey)
@DisableDiscovery
@AddJaxRs // (1)
@AddBean(MyResource.class) // (2)
@HelidonTest
class MyTest {
}
  1. Add JAX-RS (Jersey) support

  2. Add a resource class to the CDI container

Note the following Helidon CDI extensions:

Extension Note

ConfigCdiExtension

Add MicroProfile Config injection support

ServerCdiExtension

Optional if using @AddJaxRs

JaxRsCdiExtension

Optional if using @AddJaxRs

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

Reset the CDI container between methods
@HelidonTest(resetPerTest = true)
class MyTest {

    @Test
    void testOne() { // (1)
    }

    @Test
    void testTwo() { // (2)
    }
}
  1. testOne executes in a dedicated CDI container

  2. testTwo also executes in a dedicated CDI container

Customize the CDI container per method
@HelidonTest
class MyTest {

    @Test
    void testOne() { // (1)
    }

    @Test
    @DisableDiscovery
    @AddBean(MyBean.class)
    void testTwo() { // (2)
    }
}
  1. testOne executes in the shared CDI container

  2. testTwo executes 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.

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

@AddConfig

Key value pair

@AddConfigBlock

Formatted text block

@AddConfigSource

Programmatic config source

@Configuration

Classpath resources using

Add a key value pair
@AddConfig(key = "foo", value = "bar")
@HelidonTest
class MyTest {
}
Add a properties text block
@AddConfigBlock("""
        foo=bar
        bob=alice
        """)
@HelidonTest
class MyTest {
}
Add a YAML text block
@AddConfigBlock(type = "yaml", value = """
        my-test:
          foo: bar
          bob: alice
        """)
@HelidonTest
class MyTest {
}
Add config programmatically
@HelidonTest
class MyTest {

    @AddConfigSource
    static ConfigSource config() {
        return MpConfigSources.create(Map.of(
                "foo", "bar",
                "bob", "alice"));
    }
}
Add classpath resources
@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.

Add a properties text block with ordinal
@AddConfigBlock(value = """
        config_ordinal=120
        foo=bar
        """)
@HelidonTest
class MyTest {
}

The default ordering is the following

Annotation Ordinal

@AddConfig

1000

@AddConfigBlock

900

@AddConfigSource

800

@Configuration

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

WebTarget

A JAX-RS client configured for the current server.

URI

A URI representing the current server

String

A raw URI representing the current server

SeContainer

The current CDI container instance

Note
Types that reflect the current server require ServerCdiExtension
Inject a JAX-RS client for the default socket
@HelidonTest
class MyTest {

    @Inject
    WebTarget target;
}

Use @Socket to specify the socket for the clients and URIs.

Inject a JAX-RS client for the admin socket
@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.

Class-level meta-annotation
@HelidonTest
@AddBean(FirstBean.class)
@AddBean(SecondBean.class)
@DisableDiscovery
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomMetaAnnotation {
}

@CustomMetaAnnotation
class AnnotationOnClass {
}
Method-level meta-annotation
@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() {
    }
}
  1. org.testng.annotations.Test is not inheritable and should be placed on methods

API

Here is a brief overview of the MicroProfile testing annotations:

Annotation Usage

@AddBean

Add a CDI bean class to the CDI container

@AddExtension

Add a CDI extension to the CDI container

@DisableDiscovery

Disable automated discovery of beans and extensions

@AddJaxRs

Shorthand to add JAX-RS (Jersey) support

@AddConfig

Define a key value pair in the "synthetic" configuration

@AddConfigBlock

Define a formatted text block in the "synthetic" configuration

@AddConfigSource

Add a programmatic config source to the "synthetic" configuration

@Configuration

Switch between "synthetic" and "existing" ; Add classpath resources to the "synthetic" configuration

@Socket

CDI qualifier to inject a JAX-RS client or URI for a named socket

@AfterStop

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.

Config Injection Example
@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;
    }
}
  1. CDI discovery is disabled

  2. Add MyBean to the CDI container

  3. Add ConfigCdiExtension to the CDI container

  4. Define test configuration

  5. Inject the configuration

Request Scope Example

The following example demonstrates how to use @RequestScoped with JAXRS without CDI discovery.

Request Scope Example
@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();
    }
}
  1. CDI discovery is disabled

  2. Add JAXRS (Jersey) support

  3. Add MyResource to 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

Mocking using @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";
    }
}
  1. Instrument MyService using Answers.CALLS_REAL_METHODS

  2. Customize the behavior

Using CDI Alternative

@Alternative can be used to replace a CDI bean with an instrumented instance.

Mocking using CDI Alternative
@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";
    }
}
  1. Create the mock instance in the test class

  2. Create a CDI producer method annotated with @Alternative

  3. Set priority to 1 (required by @Alternative)

  4. 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.

Enable pinning detection
@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:

Configure pinning threshold
@HelidonTest(pinningDetection = true, pinningThreshold = 50) // (1)
class MyTest {
}
  1. Change pinning threshold from default(20) to 50 milliseconds.

When pinning is detected, the test fails with a stacktrace pointing at the culprit.

Reference