Micronaut (3 of 6): Testing

Lydtech
Micronaut (3 of 6): Testing

Introduction

Unit tests, integration tests and component tests form the foundation of testing an application. These tests live close to the code providing fast feedback on failures to the developer. They can verify the correctness and integrity of the application before code is even committed or merged, and before further QA and system testing is undertaken. These same tests that the developer runs locally are also automated to run in the pipeline, ensuring regressions do not occur as other changes are merged in.

This article takes the demo Micronaut application covered in the previous article and demonstrates using these three types of test, and how Micronaut features and libraries facilitate them. The companion application is provided both in the Kotlin and Java, and the source code for each is available here: [Kotlin version | Java version].

This is the third article in a six part series on the Micronaut framework.

  1. Framework Features
  2. Demo Application
  3. Testing (this article)
  4. Native Builds (coming soon)
  5. Postgres Integration (coming soon)
  6. Kafka Integration (coming soon)

Unit Testing

To unit test the Micronaut application each class has an associated unit test class defined. Only the functionality defined in the class is tested, so where a call is made to another class then that class is typically mocked. For the Kotlin application the MockK mocking library is used, and for the Java application the Mockito library is used. MockK is designed specifically for Kotlin so it naturally supports its language features. Mockito meanwhile is a mature and popular library in the Java ecosystem.

In the following example in the ItemControllerTest in the demo Kotlin project, to test the create method the controller is instantiated with a mocked service. The service mock is then called to verify that it has been called once with the expected request:

class ItemControllerTest {

   private lateinit var serviceMock: ItemService
   private lateinit var controller: ItemController

   @BeforeEach
   fun setUp() {
       serviceMock = mockk()
       controller = ItemController(serviceMock)
       clearMocks(serviceMock)
   }

   @Test
   fun testCreateItem_Success() {
       val itemId = UUID.randomUUID()
       val request = TestRestData.buildCreateItemRequest(RandomStringUtils.randomAlphabetic(8))
       every { serviceMock.createItem(request) } returns itemId
       val response = controller.createItem(request)
       assertEquals(HttpStatus.CREATED, response.status)
       assertEquals(itemId.toString(), response.header("Location"))
       verify(exactly = 1) { serviceMock.createItem(request) }
   }

The unhappy paths should also be tested in the unit tests. In this example the service is mocked to throw a runtime exception. The test asserts that this results in the controller returning an internal server error (500) response.

@Test
fun testCreateItem_ServiceThrowsException() {
   val request = TestRestData.buildCreateItemRequest(RandomStringUtils.randomAlphabetic(8))
   every { serviceMock.createItem(request) } throws RuntimeException("Service failure")
   val response = controller.createItem(request)
   assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.status)
   verify(exactly = 1) { serviceMock.createItem(request) }
}

The equivalent happy path test in the demo Java project, ItemControllerTest uses Mockito to mock the service class:

public class ItemControllerTest {

   @Mock
   private ItemService serviceMock;
   private ItemController controller;

   @BeforeEach
   public void setUp() {
       MockitoAnnotations.initMocks(this);
       controller = new ItemController(serviceMock);
   }

   @Test
   public void testCreateItem_Success() {
       UUID itemId = randomUUID();
       CreateItemRequest request = TestRestData.buildCreateItemRequest(RandomStringUtils.randomAlphabetic(8));
       when(serviceMock.createItem(request)).thenReturn(itemId);
       HttpResponse response = controller.createItem(request);
       assertThat(response.status(), equalTo(HttpStatus.CREATED));
       assertThat(response.header("Location"), equalTo(itemId.toString()));
       verify(serviceMock, times(1)).createItem(request);
   }

The unit and integration tests are run using gradle with the following command:

./gradlew clean test

Integration Testing

While unit testing verifies that units of code behave correctly in isolation, integration testing verifies that these units work as expected when integrated with one another. Generally then components are not mocked as the intention is to test the actual code flows. This however poses the question as to how the code that integrates with external resources, such as a database, message broker, and third party service, can be expected to function as necessary to run an end to end integration flow. The answer is to use where possible in-memory versions of resources like the broker or database, and wiremock for calls made to external services. An example of integration testing with a Postgres database will be covered in an upcoming section (part 5).

For the REST demo application it is necessary to start up an embedded web server to host the application, simulating a real environment. This happens transparently with Micronaut tests, annotated with @MicronautTest. The integration test interacts with the server by sending HTTP requests using the HTTP client to the application endpoints, testing the API functionality. In the Kotlin project this setup code is demonstrated in the EndToEndIntegrationTest:

@MicronautTest
class EndToEndIntegrationTest {

   @Inject
   @field:Client("/")
   lateinit var client: HttpClient

The @field:Client("/") annotation ensures the HttpClient instance is injected with the required root path into the generated field for client. Kotlin has a more complex property system than Java, abstracting the field along with its getter and setter methods under the single property declaration. Therefore Kotlin needs to know which of these to apply the annotation to, and in this case it is the field. In the Java version of EndToEndIntegrationTest, the annotation is declared slightly differently:

@Inject
@Client("/")
HttpClient client;

In Java, the annotation system assumes that an annotation placed on a field by default should modify the field itself - so both @Inject and @Client(“\") are directly applied to the HttpClient field.

The test itself then exercises each of the CRUD endpoints, creating and retrieving the item, then updating it, before finally deleting it. This snippet of the Kotlin version of the test shows the initial create item testing:

@Test
fun testItemCRUD() {
   // Create the item.
  val createItemRequest = TestRestData.buildCreateItemRequest(RandomStringUtils.randomAlphabetic(8).lowercase())
   val createItemHttpRequest: HttpRequest<*> = HttpRequest.POST("/v1/items", createItemRequest).accept(MediaType.APPLICATION_JSON)
   val createItemResponse = client.toBlocking().exchange(createItemHttpRequest, Void::class.java)
   MatcherAssert.assertThat(createItemResponse.status(), Matchers.equalTo(HttpStatus.CREATED))
   MatcherAssert.assertThat(createItemResponse.header("Location"), Matchers.notNullValue())
   val itemId = createItemResponse.header("Location").toString()
   MatcherAssert.assertThat(itemId, Matchers.notNullValue())

The testing here is more coarse grained than the unit tests. It does not include fine grained assertions such as correctness of every entity’s values, rather it focuses on the expectations around the API requests and responses.

Component Testing

While integration testing proves that end to end flows can be exercised and that the application can integrate with in-memory or wiremocked resources, component testing verifies that an actual running instance of the application can run against real resources such as databases, messaging brokers, and simulated third party services. The application is treated as a black box, with the test interacting with the exposed API. This does not however mean that the application must be run up in a dedicated remote environment to achieve this testing. By using an approach that utilises the Testcontainers testing library for example, lightweight docker containers that include the application under test and its dependent resources such as a database can be quickly spun up and torn down for the purpose of the test run.

Lydtech’s open source Component Test Framework is a straightforward way to write component tests. This orchestrates Testcontainers behind the scenes to spin up the required docker containers. By providing the necessary simple test configuration, such as enable a Postgres database or enable a Kafka message broker, and annotating a standard JUnit test with the annotation @ExtendWith(ComponentTestExtension::class), the test spins up the containers transparently, runs the test or tests, and tears down the containers at the end.

Example component tests are provided in the demo projects, EndToEndCT [Kotlin | Java].

Beyond the differing language semantics, the tests are identical. Taking the Kotlin version, the class has the required annotation:

@ExtendWith(ComponentTestExtension::class)
class EndToEndCT {

It uses the Component Test Framework utility class ServiceClient to get the application’s base URL. This is derived from the docker container host and port that have been spun up at the outset of the test:

@BeforeEach
fun setup() {
   val serviceBaseUrl: String = ServiceClient.getInstance().getBaseUrl()
   RestAssured.baseURI = serviceBaseUrl
}

The test then exercises the CRUD endpoints.

Figure 1: Component testing the Micronaut application

Figure 1: Component testing the Micronaut application

This snippet of the test shows the call to create an item, asserting that the expected location header of the newly created resource is returned synchronously:

@Test
fun testItemCRUD() {

   // Test the POST endpoint to create an item.
   val createRequest = TestRestData.buildCreateItemRequest(
       RandomStringUtils.randomAlphabetic(8).lowercase() + "1"
   )
   val createItemResponse: Response = sendCreateItemRequest(createRequest)
   val itemId: String = createItemResponse.header("Location")
   MatcherAssert.assertThat(itemId, Matchers.notNullValue())

In both demo projects gradle is used as the build tool. For this simple application there are no other resources to spin up, so the only build property in gradle.properties [Kotlin | Java] required for the test is to stipulate the application health endpoint. This tells the Component Test Framework the endpoint to check to be sure the container has successfully started before executing the tests. When Postgres and Kafka are added in upcoming articles (part 5 and part 6 respectively), additional properties will be required here.

systemProp.service.startup.health.endpoint=/health

The project could equally use Maven as the build tool, with the component test properties specified in the pom.xml.

To run the component tests, the application must first be built, followed by the Docker container for the application. Then the tests can be run, using the componentTest task. This hooks into the task that is defined in the build.gradle [Kotlin | Java] , that runs the tests named with a CT suffix.

./gradlew clean build
docker build -t ct/micronaut-rest-kotlin:latest .
./gradlew componentTest --rerun-tasks

The component tests can be run both locally and automated in the CI pipeline. For more on the Component Test Framework, see the ReadME.

Summary

Effective application development relies on thorough testing, starting with fine-grained unit tests, broader integration tests, and coarse grained component tests to ensure the quality of the application. Micronaut supports unit testing with robust mocking libraries that support the framework’s features, enabling efficient testing. For integration testing, the Micronaut testing annotation is used to simulate a real environment, enabling the testing of flows through the application. Component testing Micronaut applications is easily achievable using Testcontainers and the Component Test Framework, making it straightforward to validate full end to end flows and use cases.

Source Code

There are two flavours of the Micronaut application, one in Kotlin and one in Java, and the source code is available here:

Kotlin version: https://github.com/lydtechconsulting/micronaut-rest-kotlin/tree/v1.0.0
Java version: https://github.com/lydtechconsulting/micronaut-rest-java/tree/v1.0.0

Next... Native Builds

In the fourth article in the series (coming soon) the benefits of building Micronaut applications as native images using GraalVM is covered.


View this article on our Medium Publication.