Making Integration Test in Micronaut

Making Integration Test in Micronaut

Mar 15, 2024

The problem we are going to look at in this coffee bite is:

How to override properties in Micronaut during runtime?


In Micronaut, you can override your properties with custom values during compilation time for your tests. I've done this before and this is an example of that on my project buy-odd-yucca-concert:

@ExperimentalCoroutinesApi
@MicronautTest
@Property(name = "buy.oyc.catering.port", value = "7999")
@Property(name = "buy.oyc.concert.port", value = "7998")
@Property(name = "buy.oyc.parking.port", value = "7997")
class TicketTest @Inject constructor(
 private val ticketRepository: TicketRepository,
 private val ticketReactiveClient: TicketReactiveClient,
 private val auditLogRepository: AuditLogRepository
) : AbstractBuyOddYuccaConcertContainerTest() {

What the above does, is to use import io.micronaut.context.annotation.Property to specify that the declared properties are going to be replaced during compile time and that in this way, during runtime, I can use those values, in this case, the ports, enabling me to implement mocked endpoints using wiremock. This is all good, but let me emphasize, what this does again. If you haven't noticed, I'm trying to make sure as much possible that it is very very important to take into consideration as to where these values are going to be assigned. For these tests, the values are assigned during compile time. Compile time, means that if you get a property value, that is only available during runtime, and you need to set a property with it, you will not be able to use this strategy. This strategy, is, in other words, only available, if you know before running your integration tests, which value you want to use.


Now, this didn't solve my problem, and let me explain why. Using the Testcontainers framework, we are not always sure which port is effectively available and which host is actually available to the outside world. If we implement our Docker compose startup with Testcontainers, we can specify the port, just as long as the port is specified in the docker compose file. At this point in time, Testcontainers do not undersrtand the container_name property and they do not resolve the hostname immediately. When we force that resolution though, we can get the effective port and the effective hostname in a way that we can access one specific container that I'm interested in during the runtime of these tests. In this case, I am referring to my Kong gateway, which essentially allows me to control the flow of date going in and out of my server in a seamless way. However, and as mentioned above, I cannot set these up at compile time. I have to set these up during runtime. As I recall using Spring, this was very much possible in several ways being the most important example using the org.springframework.test.context.DynamicPropertySource. Micronaut doesn't have this, of course, but it does have an ingenious, if not more ingenious way of setting up properties on the fly.


The way I've implemented this first version of my Chain Tests, is using the Jupiter 5 integration test framework. In this way I'm able to start the integration tests. My integration tests at the moment just do this: receive payload -> send payload to Redis PubSub -> register receipt in database -> return back to client. When Redis receives the payload, the listeners will pick it up and make an HTTP request. This is basically it!. For this, I am of course starting the containers. This is the test:

@Test
fun `should run chain test and create a concert reservation`() = runTest {
 val serviceHost = dockerCompose.getServiceHost("kong_1", 8000)
 val httpClient = HttpClient.create(URL("http://$serviceHost:8000"))

 val ticketDto = TicketDto(name = "name", address = "address", birthDate = LocalDate.now())
 val dtoSingle = httpClient.retrieve(
 HttpRequest.POST("/api/yucca-api/api", ticketDto).header(
 ACCEPT, APPLICATION_JSON
 ), ResponseDto::class.java
 )

 dtoSingle.awaitFirst()

 withContext(Dispatchers.IO) {
 receiptRepository.findAll().toList().shouldHaveSize(1)
 auditLogRepository.findAll().toList().shouldHaveSize(0)
 ticketRepository.findAll().toList().shouldHaveSize(0)
 }
}

This is a very simple test, but what's important to observe is how am I getting the host: val serviceHost = dockerCompose.getServiceHost("kong_1", 8000). This essentially will return the actual name of the host that we need to connect to the container from the outside.


Micronaut provides decorators, which, when applied, allow the application made ready by Jupiter 5, to start under whatever context we want. This is the context I configured:

class CustomContextBuilder : DefaultApplicationContextBuilder() {
 init {
 eagerInitSingletons(true)
 val serviceHost = dockerCompose.getServiceHost("db_1", 5432)
 val props = mapOf(
 "r2dbc.datasources.default.url" to "r2dbc:postgresql://kong@$serviceHost:5432/yucca?currentSchema=ticket"
 )
 logger.info("Database Host configuration is $props")
 properties(props)
 }

 companion object {
 private val logger = LoggerFactory.getLogger(CustomContextBuilder::class.java)
 }
}

In this case, I am interested in knowing and fixating the database host name. It can be localhost in most cases, but in other cases, it can be something else depending on the environment. This instruction: val serviceHost = dockerCompose.getServiceHost("db_1", 5432), is how we get the actual hostname. From this point onwards, is quite straightforward to force the value of the service host we wanted to configure using: val props = mapOf("r2dbc.datasources.default.url" to "r2dbc:postgresql://kong@$serviceHost:5432/yucca?currentSchema=ticket") and properties(props). Having our context configured, we can then apply it by setting it up with @MicronautTest(contextBuilder = [ at the top of the test class:

@ExperimentalCoroutinesApi
@MicronautTest(contextBuilder = [CustomContextBuilder::class])
class ChainTest @Inject constructor(
 val auditLogRepository: AuditLogRepository,
 val receiptRepository: ReceiptRepository,
 val ticketRepository: TicketRepository,
 val concertDayReservationRepository: ConcertDayReservationRepository,
 val parkingReservationRepository: ParkingReservationRepository,
 val drinkReservationRepository: DrinkReservationRepository,
 val mealReservationRepository: MealReservationRepository,
) : AbstractContainersTest() {

This is the way I was able to change properties during runtime in Micronaut. My project is available here and these classes are located on this commit here.


A word about Kotlin

As I have been explaining for quite a while, Kotlin, is a programming language that has fascinated me, but not necessarily in a positive way. This project, older projects and newer projects of my own, are being developed in Kotlin. I've seen so far many positives and negatives in Kotlin, but I have not seen any major argument yet to credibly say that it's better than Java. In many ways I see Kotlin as just another language in the market that potentially may be reachable to less technical people given how much shortcuts, what I usually name sugar coating, or just plain sugar code it has. It is quite an elegant programming language indeed, but I still fail to see the actual engineering benefits of using it. In practice, I've seen and experienced exactly the same issues, if not more using Kotlin than using Java. In this project I've experience the same loops and hoops I have experienced in Java when learning something new. I've seen some odd behaviour in how the code runs under coroutines and reified functions for example. Here I found unexpected random issues in terms of serialization to which the Exceptions gave me no clear indication of what was wrong. The function in question is this:

inline fun <reified T : BuyOycType> Rx3HttpClient.sendObject(
 buyOycType: T,
 url: String,
 auditLogRepository: AuditLogRepository
): Single<ResponseDto> {
 val retrieve =
 retrieve(HttpRequest.POST(url, buyOycType).header(ACCEPT, APPLICATION_JSON), ResponseDto::class.java)
 retrieve.doOnError {
 logger.error("ERROR", it)
 }.subscribe()
 val buyOycValue: Single<ResponseDto> =
 retrieve.firstOrError()
 val singleScheduler = SingleScheduler()
 buyOycValue.subscribeOn(singleScheduler)
 .doOnError {
 logger.error("ERROR", it)
 }
 .doOnSuccess {
 CoroutineScope(Dispatchers.IO).launch {
 auditLogRepository.save(
 AuditLog(
 auditLogType = buyOycType.type,
 payload = buyOycType.toString()
 )
 )
 }
 }.subscribe()
 singleScheduler.start()
 return buyOycValue
}

If I didn't use reified, which I'm purposefully using in this case, somehow, I got no issues with serialization, but using it made serialization randomly impossible. I kept getting this exception:

buy-oyc-api | Caused by: java.lang.ClassCastException: class org.jesperancinha.concert.buy.oyc.commons.dto.TicketDto cannot be cast to class io.netty.buffer.ByteBuf (org.jesperancinha.concert.buy.oyc.commons.dto.TicketDto and io.netty.buffer.ByteBuf are in unnamed module of loader 'app')

buy-oyc-api | at io.micronaut.http.client.netty.NettyClientHttpRequest.toFullHttpRequest(NettyClientHttpRequest.java:272)

buy-oyc-api | at io.micronaut.http.client.netty.NettyClientHttpRequest.toHttpRequest(NettyClientHttpRequest.java:322)

buy-oyc-api | at io.micronaut.http.netty.NettyHttpRequestBuilder.toHttpRequest(NettyHttpRequestBuilder.java:75)

buy-oyc-api | at io.micronaut.http.client.netty.DefaultHttpClient.buildNettyRequest(DefaultHttpClient.java:1825)

buy-oyc-api | at io.micronaut.http.client.netty.DefaultHttpClient.sendRequestThroughChannel(DefaultHttpClient.java:2071)

buy-oyc-api | at io.micronaut.http.client.netty.DefaultHttpClient.lambda$null$36(DefaultHttpClient.java:1275)

I was using Rx3StreamingHttpClient instead of Rx3HttpClient. The solution for me was, unfortunately to finally use the Rx3HttpClient. Something tells me that there is a difference between the reified types and the actual types. All I know about this difference is that refied tells the compiler to keep the type and makes it mandatory to use inline which makes the compiler make copies of the function wherever it is used. This also means that, with reified, the type doesn't come from memory, but directly from the bytecode, which is already a significant difference. However if this difference was enough to break my code than that, is actually a very, very negative sign about Kotlin.


What's important for me with this coffee bite is that I was able to transmit to you one way to change properties in Micronaut during runtime and that Kotlin may still be just a beautiful language, so please be careful with this current trend. It may not be what it is set out to be. Hope you enjoyed it!

Enjoy this post?

Buy João Esperancinha a coffee

More from João Esperancinha