Helidon, Testcontainers, Cucumber, Kafka messaging and a lot more!

Dmitry Aleksandrov
Helidon
Published in
8 min readNov 23, 2021

Helidon is definitely great for creating microservices, for easy and fast deployment in production, with a really impressive performance!

But what about testing Helidon?

In this article we will explore several techniques how to do it.

Subject of testing

Let us have a look on the app we are going to test today. It is a simplified version of our famous Socks Shop app available here.

There are just several classes, though this should be enough to demonstrate all we want with the testing. There will be just REST APIs published with no UI. Just to keep it simple.

There is a simple Helidon MP application to serve us the SockShopResource with a simple functionality of purchasing socks. There is one method to get all the socks with their price. And there is a POST method to submit a shopping cart with the selected items. After the checkout is made a message is fired to the delivery service to ship the items. Invoicing service to prepare and store the invoice will be called via REST. All the services take care to persist their data (Regular hibernate used for that :) ).

Let us now take a look at each component to find the best strategy to test it.

SockShopResource

The Socks Shop resource is a typical REST API endpoint exposing checkout functionality:

private ShoppingService shoppingService;

@Inject
public SockShopResource(ShoppingService shoppingService) {
this.shoppingService = shoppingService;
}


@POST
public Response checkout(ShoppingCart shoppingCart){
long id = shoppingService.checkout(shoppingCart);

UriBuilder responseUri = UriBuilder.fromResource(SockShopResource.class);
responseUri.path(Long.toString(id));
return Response.created(responseUri.build()).build();
}

ShoppingService

The class implements the checkout process. The service receives the shopping cart:

@ApplicationScoped
public class ShoppingService {

private final SubmissionPublisher<String> emitter = new SubmissionPublisher<>();

@PersistenceContext(unitName = "test")
private EntityManager entityManager;

@Inject
@RestClient
private InvoicingClient invoicingClient;


@Transactional
public long checkout(ShoppingCart shoppingCart){
entityManager.persist(shoppingCart);
Long id = shoppingCart.getId();
emitter.submit(String.valueOf(id));
invoicingClient.handleInvoice(shoppingCart);
return id;
}

@Outgoing("outgoing-delivery")
public Publisher<String> preparePublisher() {
// Create new publisher for emitting to by this::process
return ReactiveStreams
.fromPublisher(FlowAdapters.toPublisher(emitter))
.buildRs();
}
}

As the checkout is performed and persisted, a message is send to the delivery service, to package the socks and dispatch them to the Customer. Also the invoicing service is called using MicroProfile ‘RestClient’.

DeliveryService

Whenever a message about new checkout comes, the delivery service is there to handle it. The @Incomming annotation points to the method which processes it.

@ApplicationScoped
public class DeliveryService {

@PersistenceContext(unitName = "test")
private EntityManager entityManager;


@Incoming("incoming-delivery")
@Transactional
public void deliverToCustomer(String cartId){
Delivery delivery = new Delivery();
delivery.setShoppingCartId(Long.parseLong(cartId));
entityManager.persist(delivery);
}
}

Additional methods are there to check status of the delivery :)

Configuration

… is very straightforward. We need only the DB Connection data and Messaging setup. We will use in memory H2 DB and again in memory ActiveMQ service:

#Database
javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
javax.sql.DataSource.test.dataSource.user=sa
javax.sql.DataSource.test.dataSource.password=

#Messaging
mp.messaging.connector.helidon-jms.jndi.env-properties.java.naming.provider.url=vm://localhost?broker.persistent=false
mp.messaging.connector.helidon-jms.jndi.env-properties.java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
mp.messaging.incoming.incoming-delivery.connector=helidon-jms
mp.messaging.incoming.incoming-delivery.type=queue
mp.messaging.incoming.incoming-delivery.destination=delivery

mp.messaging.outgoing.outgoing-delivery.connector=helidon-jms
mp.messaging.outgoing.outgoing-delivery.type=queue
mp.messaging.outgoing.outgoing-delivery.destination=delivery

This is just enough for us!

This data is in the microprofile-config.properties file. So it is very portable.

Some other files

AppInitialiser class is used to fill in some test data and start the Messaging.

@ApplicationScoped
public class AppInitializer {

@PersistenceContext(unitName = "test")
private EntityManager entityManager;

@Transactional
void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event) {
Socks model1 = new Socks(1L, "Model1", 10.00);
entityManager.persist(model1);
Socks model2 = new Socks(2L, "Model2", 20.00);
entityManager.persist(model2);

Client client1 = new Client(1L, "Joe", "Doe", "Somewhere", "12345");
entityManager.persist(client1);

ShoppingCart cart = new ShoppingCart();
cart.setId(1L);
cart.setClient(client1);
cart.setCart(List.of(model1, model2));
entityManager.persist(cart);

entityManager.flush();
}

private void makeConnections(@Observes @Priority(PLATFORM_AFTER + 1) @Initialized(ApplicationScoped.class) Object event) throws Throwable{

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost?broker.persistent=false");
Connection connection = connectionFactory.createConnection();
connection.createSession(false, 1);

}
}

As you may notice, we are observing the moment when all ApplicationScoped beans are up and running, and after the event for that is fired, we are able to fill in some data. This is pure CDI. This code is 100% portable.

Test this!

Ok, let us now test this Socks shop!

Unit Testing

This is probably the easiest testing — yet very important one!

Everything that requires working with DB or any external services that require initialization, or running inside CDI container, can be easily mocked. Let us use mockitofor that:

@ExtendWith(MockitoExtension.class)
public class SockShopResourceMockTest {

private List<Socks> socksList = List.of(new Socks(1L, "Model1", 10.00),
new Socks(2L, "Model2", 20.00));

@InjectMocks
private SockShopResource sockShopResource;

@Mock
private ShoppingService shoppingService;

@BeforeEach
private void init() {
Mockito.lenient().doCallRealMethod().when(shoppingService).allSocks();
}

@Test
void allSocksTest() {
Mockito.doReturn(socksList).when(shoppingService).allSocks();

String response = sockShopResource.allSocks();
assertEquals(response, "[{\"id\":1,\"model\":\"Model1\",\"price\":10.0},{\"id\":2,\"model\":\"Model2\",\"price\":20.0}]");
Mockito.verifyNoMoreInteractions(shoppingService);

}
}

As you may see ShoppingService is mocked and we test SockShopResource independently from the underlying infrastructure. The tests should run very fast since no server is running actually.

@HelidonTest

Ok, now let us jump deeper and see how to test functionality inside running Helidon.

Let us start with the most used annotation — @HelidonTest. By wrapping the class with this annotation, it takes all the care for starting the container initialization and wiring up everything.

@HelidonTest
public class TestSocksResource {

@Inject
WebTarget webTarget;

@Test
void testAllSocks(){

JsonArray jsonObject = webTarget.path("/api/shop/allSocks")
.request()
.get(JsonArray.class);

assertEquals("[{\"model\":\"Model1\",\"price\":10.0},{\"model\":\"Model2\",\"price\":20.0}]",jsonObject.toString());
}
}

It is very straightforward! As you see, with just one annotation we actually start Helidon MP, everything in CDI container is been wired and before test everything is running. We can use WebTarget to test our rest methods.

With additional annotations like:

@DisableDiscovery
@AddExtension(ConfigCdiExtension.class)
@AddBean(SocksTest.ConfiguredBean.class)
@AddConfig(key = "test.message", value = "Socks!")

tests can be highly customisable!

More about using this annotation you can read in @tomas.langer ‘s post https://medium.com/helidon/testing-helidon-9df2ea14e22

Integration testing with Testcontainers

Testcontainers are really a masterpiece technology, which brings integration testing to another level. With it we can test Helidon running inside a container:

protected static final String NETWORK_ALIAS_APPLICATION = "application";

protected static final Network NETWORK = Network.newNetwork();

protected static final GenericContainer<?> APPLICATION = new GenericContainer<>("socks-shop:latest")
.withExposedPorts(8080)
.withNetwork(NETWORK)
.withNetworkAliases(NETWORK_ALIAS_APPLICATION)
.withEnv("JAVA_OPTS", "-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv4Addresses=true")
.waitingFor(Wait.forHealthcheck());

static {
APPLICATION.start();
}

Generic Container is just ok for that. To build the image we can use docker-maven-plugin by Spotify:

<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.13</version>
<configuration>
<repository>${project.build.finalName}</repository>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
<skipDockerInfo>true</skipDockerInfo>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>

Now as we have our app packaged as a docker image and wrapped as a Testcontainer, we can make a full-scale integration test.

A good helper for that is the Cucumber framework. Let us create a very easy scenario on buying socks:

Feature: BuySocks

Scenario: Buy one pair of socks
Given a user makes a checkout
When the checkout is performed
Then submitted to delivery

Now tell Cucumber where to read the features files from:

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"}, features = "src/test/resources/it/feature")
public class SocksShopCucumberIT {
}

We are now ready to test our app running in a test container. First, make it run:

@Before
public void beforeScenario() {
APPLICATION.withLogConsumer(new Slf4jLogConsumer(LOG));
requestSpecification = new RequestSpecBuilder()
.setPort(APPLICATION.getFirstMappedPort())
.build();
}

Now perform the first step of the scenario:

@Given("a user makes a checkout")
public void a_user_makes_a_checkout() {
Socks socks = new Socks(100l, "Model1", 10.0);
Client client1 = new Client(100L, "Joe", "Doe", "Somewhere", "12345");
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setId(100L);
shoppingCart.setClient(client1);
shoppingCart.setCart(List.of(socks));

RestAssured.given(requestSpecification)
.contentType(MediaType.APPLICATION_JSON)
.body(shoppingCart)
.when()
.post("/api/shop/")
.then()
.statusCode(Response.Status.CREATED.getStatusCode());
}

We then can check if the checkout is performed:

@When("the checkout is performed")
public void the_checkout_is_performed() {
RestAssured.given(requestSpecification)
.accept(MediaType.APPLICATION_JSON)
.when()
.get("/api/shop/status/100")
.then()
.statusCode(Response.Status.OK.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(Matchers
.equalTo("{\"cart\":[{\"id\":100,\"model\":\"Model1\",\"price\":10.0}],\"client\":{\"address\":\"Somewhere\",\"firstName\":\"Joe\",\"id\":100,\"lastName\":\"Doe\",\"postcode\":\"12345\"},\"id\":100}"));

}

… and finally we can check if the delivery message was emitted, and the shipment is submitted for delivery:

@Then("submitted to delivery")
public void submitted_to_delivery() throws InterruptedException {
Thread.sleep(500);//wait the message to arrive
RestAssured.given(requestSpecification)
.when()
.get("/api/delivery/status/100")
.then()
.statusCode(Response.Status.OK.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body(Matchers
.equalTo("{\"id\":1,\"shoppingCartId\":100}"));

}

When user runs this scenario:

  1. Testcontainers take the lates image of the socks-shop application and run it;
  2. Socks-shop app boot and initialise inside of the container;
  3. Whenever it is ready `a_user_makes_a_checkout()` is be called;
  4. If everything runs smoothly `the_checkout_is_performed()` will be done to verify, that the checkout is persisted
  5. And finally `the_checkout_is_performed()` checksif the order is finalized.

The test is considered as successful if all the steps have passed.

As we are using H2 in memory database and in–memory messaging, no additional setup is required.

This way you can test your Helidon applications in “Almost production” environment. And this environments are really fast recreated on each test scenario run.

…and vice versa

With test containers and Helidon we can also do vice versa for the integration testing, we can make Helidon consume some of the resources from external services running inside Test containers.

For example we want to check if our application runs smoothly with MariaDB as a database and uses Kafka for messaging. They will be running inside Test Containers. We only need to rewrite the configuration to point the database and messaging to Test Containers.

Let us reuse @HelidonTest annotation to handle Helidon run and initialization. But since config has to be overridden we should tell about it using annotation:

@Configuration(useExisting = true)

We now can do the setup of MariaDB and Kafka Containers and declare the properties:

private static MariaDBContainer<?> db = new MariaDBContainer<>("mariadb:10.3.6")
.withDatabaseName("mydb")
.withUsername("test")
.withPassword("test");
static KafkaContainer kafka = new KafkaContainer();
@BeforeAll
public static void setup() {
kafka.start();

Map<String, String> configValues = new HashMap<>();
configValues.put("mp.initializer.allow", "true");
configValues.put("mp.messaging.incoming.from-kafka.connector", "helidon-kafka");
configValues.put("mp.messaging.incoming.from-kafka.topic", "delivery");
configValues.put("mp.messaging.incoming.from-kafka.auto.offset.reset", "latest");
configValues.put("mp.messaging.incoming.from-kafka.enable.auto.commit", "true");
configValues.put("mp.messaging.incoming.from-kafka.group.id", "helidon-group-1");

configValues.put("mp.messaging.outgoing.to-kafka.connector", "helidon-kafka");
configValues.put("mp.messaging.outgoing.to-kafka.topic", "delivery");

configValues.put("mp.messaging.outgoing.test-delivery.connector", "helidon-kafka");
configValues.put("mp.messaging.outgoing.test-delivery.topic", "delivery");

configValues.put(KafkaConnector.CONNECTOR_PREFIX + "helidon-kafka.bootstrap.servers", kafka.getBootstrapServers());
configValues.put(KafkaConnector.CONNECTOR_PREFIX + "helidon-kafka.key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
configValues.put(KafkaConnector.CONNECTOR_PREFIX + "helidon-kafka.value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
configValues.put(KafkaConnector.CONNECTOR_PREFIX + "helidon-kafka.key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
configValues.put(KafkaConnector.CONNECTOR_PREFIX + "helidon-kafka.value.deserializer", "org.apache.kafka.common.s" +
"erialization.StringDeserializer");

configValues.put("mp.initializer.allow", "true");
configValues.put("javax.sql.DataSource.test.dataSourceClassName", "org.mariadb.jdbc.MariaDbDataSource");
configValues.put("javax.sql.DataSource.test.dataSource.url", db.getJdbcUrl());
configValues.put("javax.sql.DataSource.test.dataSource.user", db.getUsername());
configValues.put("javax.sql.DataSource.test.dataSource.password", db.getPassword());
org.eclipse.microprofile.config.Config mpConfig = ConfigProviderResolver.instance()
.getBuilder()
.withSources(MpConfigSources.create(configValues))
.build();
ConfigProviderResolver.instance().registerConfig(mpConfig, Thread.currentThread().getContextClassLoader());
}

It is so nice that the guys and gals from Test Container have prepared these containers for us!

And this is it! If we run this test (even directly from the IDE) it first starts the Test Containers, and after they are initialized and running, the properties will be assigned and Helidon container will start. After that all the tests run against those containers. This means that all the database requests will be running through MariaDB and all messages will run through Kafka. This idea is just great!

Meta-Config

It is also worth mentioning a new feature of Helidon MP — meta-config.You can configure the Config using Helidon MP Config meta configuration feature.

When used, the MicroProfile Config uses configuration sources and flags configured in the meta configuration file.

The meta-config allows configuration of config sources and other configuration options, including addition of discovered sources and converters.

If a file named mp-meta-config.yaml, or mp-meta-config.properties is in the current directory or on the classpath, and there is no explicit setup of configuration in the code, the configuration will be loaded from the meta-config file. The location of the file can be overridden using system property io.helidon.config.mp.meta-config, or environment variable HELIDON_MP_META_CONFIG.

Conclusion

Helidon provides full support of all industry standard and de-facto standard testing technologies. Unit and integration testing are the essential part of every software development. Good tests guarantee the best quality of your software!

If you want to play with this code — it is available here.

Enjoy!

Special thanks to @danielkec for his support with Messaging preparation for this article!

--

--