Testing Micronaut Application in Kotlin

In this, practical tutorial, I will show you how to test a REST API created with Micronaut, Kotlin and MongoDB.
Image is a featured image for the post about testing in Micronaut with Kotlin, JUnit 5, MockK, and REST Assured. It consist of the Micronaut logo in the foreground and the blurred photo of people sitting next to their computers in the background.

Hello and welcome to my next article, in which I will show you how to test a Micronaut application in Kotlin.

To be even more specific- we will work with an application that exposes a REST API and connects to MongoDB. If you would like to learn how to do that step-by-step, then you can check out my other article. (But don’t worry, we will see its code snippets in this tutorial, too).

For testing, we’re going to use JUnit 5, MockK, REST Assured, as well as the @MicronautTest and Micronaut Test Resources.

Video Tutorial

If you prefer video content, then check out my video:

If you find this content useful, please leave a subscription  😉

Micronaut Project Overview

Let’s start everything by checking the project we will test today. Of course, we will focus on the most important parts, and for the full source code, I invite you to this GitHub repository.

Application Properties

In our project, we introduce 3 application properties files:

// application.properties
micronaut.application.name=micronaut-testing

// application-mongo.properties
mongodb.uri=mongodb://localhost:27017/example

// application-test.properties
mongodb.package-names=com.codersee.model

The application name is the default property inserted when generating a new project.

Nevertheless, the two remaining are created on purpose. With their names- *mongo, *test– we instruct Micronaut that it should consider them only when mongo and test profiles are active.

But why this way? Well, to avoid two issues.

MongoDB Connection Issues with Micronaut Test Resources

So, let’s start everything by explaining why we don’t put the Mongo URI inside the main properties.

Well, when we use the Micronaut Test Resources, MongoDB support will start a MongoDB container and provide the value of the mongodb.uri property.

Nevertheless, this won’t happen when we explicitly set the URI in the application properties. And that’s why we want to make this setting available only when we set the environment to mongo.

CodecCacheKey Issue

Additionally, if we don’t specify explicitly the package name, where our model classes live, we’re going to end up with the following issue when running our Micronaut tests:

[io-executor-thread-1] ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Can't find a codec for CodecCacheKey{clazz=class com.codersee.model.AppUser, types=null}.
org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for CodecCacheKey{clazz=class com.codersee.model.AppUser, types=null}.
  at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:87)
  at java.base/java.util.Optional.orElseGet(Optional.java:364)
  at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)
  at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:50)
  at com.mongodb.internal.operation.Operations.createFindOperation(Operations.java:188)
...

To me, looks like a bug. But thankfully, we can easily avoid that by putting that in the application-test.properties (which is used in tests by default).

Models

Following, we introduce a bunch of models:

// Address.kt: 

import io.micronaut.serde.annotation.Serdeable

@Serdeable.Serializable
@Serdeable.Deserializable
data class Address(
  val street: String,
  val city: String,
  val code: Int
)

// AppUser.kt:

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity

@MappedEntity
data class AppUser(
  @field:Id
  @field:GeneratedValue
  val id: String? = null,
  val firstName: String,
  val lastName: String,
  val email: String,
  val address: Address
)

// AppUserRequest.kt: 

import io.micronaut.serde.annotation.Serdeable

@Serdeable.Deserializable
data class AppUserRequest(
  val firstName: String,
  val lastName: String,
  val email: String,
  val street: String,
  val city: String,
  val code: Int
)

// SearchRequest.kt:

import io.micronaut.serde.annotation.Serdeable

@Serdeable.Deserializable
data class SearchRequest(val name: String)

Those classes are either used to persist/retrieve data from MongoDB or when deserializing JSON payloads into the objects.

Repository

Following, we have a repository layer responsible for communicating with our storage:

// AppUserRepository.kt: 

import com.codersee.model.AppUser
import io.micronaut.data.mongodb.annotation.MongoFindQuery
import io.micronaut.data.mongodb.annotation.MongoRepository
import io.micronaut.data.repository.CrudRepository

@MongoRepository
interface AppUserRepository : CrudRepository<AppUser, String> {

  fun findByFirstNameEquals(firstName: String): List<AppUser>

  @MongoFindQuery("{ firstName: { \$regex: :name}}")
  fun findByFirstNameLike(name: String): List<AppUser>
}

Service

Nextly, the service layer, where we inject the repository and encapsulate its methods:

// AppUserService.kt:

import com.codersee.model.Address
import com.codersee.model.AppUser
import com.codersee.model.AppUserRequest
import com.codersee.repository.AppUserRepository
import io.micronaut.http.HttpStatus
import io.micronaut.http.exceptions.HttpStatusException
import jakarta.inject.Singleton

@Singleton
class AppUserService(
  private val appUserRepository: AppUserRepository
) {

  fun create(userRequest: AppUserRequest): AppUser =
    appUserRepository.save(
      userRequest.toAppUserEntity()
    )

  fun findAll(): List<AppUser> =
    appUserRepository
      .findAll()
      .toList()

  fun findById(id: String): AppUser =
    appUserRepository.findById(id)
      .orElseThrow { HttpStatusException(HttpStatus.NOT_FOUND, "User with id: $id was not found.") }

  fun update(id: String, updateRequest: AppUserRequest): AppUser {
    val foundUser = findById(id)

    val updatedEntity =
      updateRequest
        .toAppUserEntity()
        .copy(id = foundUser.id)

    return appUserRepository.update(updatedEntity)
  }

  fun deleteById(id: String) {
    val foundUser = findById(id)

    appUserRepository.delete(foundUser)
  }

  fun findByNameLike(name: String): List<AppUser> =
    appUserRepository
      .findByFirstNameLike(name)

  private fun AppUserRequest.toAppUserEntity(): AppUser {
    val address = Address(
      street = this.street,
      city = this.city,
      code = this.code
    )

    return AppUser(
      id = null,
      firstName = this.firstName,
      lastName = this.lastName,
      email = this.email,
      address = address
    )
  }
}

Controller

And lastly, the place where we expose our REST endpoints:

// AppUserController.kt: 

import com.codersee.model.AppUserRequest
import com.codersee.service.AppUserService
import com.codersee.model.SearchRequest
import com.codersee.model.AppUser
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@Controller("/users")
@ExecuteOn(TaskExecutors.IO)
class AppUserController(
  private val appUserService: AppUserService
) {

  @Get
  fun findAllUsers(): List<AppUser> =
    appUserService.findAll()

  @Get("/{id}")
  fun findById(@PathVariable id: String): AppUser =
    appUserService.findById(id)

  @Post
  @Status(HttpStatus.CREATED)
  fun createUser(@Body request: AppUserRequest): AppUser =
    appUserService.create(request)

  @Post("/search")
  fun searchUsers(@Body searchRequest: SearchRequest): List<AppUser> =
    appUserService.findByNameLike(
      name = searchRequest.name
    )

  @Put("/{id}")
  fun updateById(
    @PathVariable id: String,
    @Body request: AppUserRequest
  ): AppUser =
    appUserService.update(id, request)

  @Delete("/{id}")
  @Status(HttpStatus.NO_CONTENT)
  fun deleteById(@PathVariable id: String) =
    appUserService.deleteById(id)
}

Simple Kotlin Testing in Micronaut

Excellent. At this point, we know what exactly we are going to test.

And as the first approach we’re going to see will be a plain, old unit test.

Why?

Because at the end of the day, that’s what we will be dealing with most of the time.

So with that said, let’s take a look at the example test:

  import com.codersee.repository.AppUserRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class AppUserServiceTest {

  private val appUserRepository = mockk<AppUserRepository>()

  private val appUserService = AppUserService(appUserRepository)

  @Test
  fun `should return empty user list`() {
    every {
      appUserRepository.findAll()
    } returns emptyList()

    val result = appUserService.findAll()

    assertTrue(result.isEmpty())

    verify(exactly = 1) { appUserRepository.findAll() }
  }
}

As we can see above, we simply mock (with MockK) the AppUserRepository and inject it into the AppUserService instance.

Then, we specify that all invocations of the findAll method must return the empty list (with every).

Lastly, we assert that the findAll method from our service returns an empty list (assertTrue) and that the findAll method from the repository was invoked only once (verify).

Nevertheless, we’re not going to spend too much here right now as this tutorial focuses on Micronaut testing in Kotlin. If you are interested in learning more about these tools, then let me know in the comments section 🙂

Integration Testing with @MicronautTest

With all of that done, let’s see how the Micronaut framework helps us with testing and a few interesting cases that will help us in real life.

What is a @MicronautTest?

Let’s start everything by explaining what exactly is the @MicronautTest.

In simple words, it’s an annotation that we can put on the test class to mark it a Micronaut test (no sh… Sherlock 🙂 :

import io.micronaut.test.extensions.junit5.annotation.MicronautTest

@MicronautTest
class SomeTest {
  // tests
}

In practice, it means that when we run a test with that annotation, it will run a real application. It will use internal Micronaut features with no mocking. So at this point, we can clearly see that this will be the right solution for integration testing.

Moreover, this annotation can be used not only with JUnit 5, but also with Spock, Kotest, and Kotest 5.

@MicronautTest Configuration Options

The @MicronautTest allows us to configure a few properties, like the environments we want to run our test with, the packages to scan, or whether the automatic transaction wrapping should be enabled, or not:

import io.micronaut.test.extensions.junit5.annotation.MicronautTest

@MicronautTest(
  environments = ["env-1, env-2"],
  packages = ["com.codersee.somepackage"],
  transactional = false
)
class SomeTest {
  // tests
}

For the full list of options, I highly encourage you to check out the docs (we can do that for instance in IntelliJ IDEA by clicking on the annotation with the left mouse button when we keep the left ctrl pressed).

What Are Micronaut Test Resources?

At the beginning of this tutorial, I mentioned the Micronaut Test Resources and I would like to make sure that we are on the same page with them.

So basically, Micronaute Test Resources integrates seamlessly with Testcontainers to provide throwaway containers for testing. But, we need to remember that Testcontainers require Docker-API compatible container runtime (in simple words- either Docker installed locally, or the Testcontainers Cloud).

In our case, we would like to do integration tests that require MongoDB. We could make use of the real database (which sometimes may be the case for you), provide some H2 database (not the best approach), or integrate Testcontainers. Thankfully, we don’t need to do that and the only thing we need is the appropriate import in our project:

plugins {
    id("io.micronaut.test-resources") version "4.2.1"
}

As a reminder, I want to emphasize that test resources will be used only when the datasources.default.url is missing. (and that’s why we removed the URI for MongoDB from tests).

Integration Tests With REST Assured

With all of that said, let’s combine everything together and test our Kotlin Micronaut application with JUnit 5 and REST Assured.

To do so, we must remember to add the necessary import:

testImplementation("io.micronaut.test:micronaut-test-rest-assured")

Verify Status Codes and Headers

Let’s start with a simple request to the GET /users endpoint:

@MicronautTest
class AppUserControllerTestWithoutMocking {

  @Test
  fun `should return 200 OK on GET users`(spec: RequestSpecification) {
    spec
      .`when`()
      .get("/users")
      .then()
      .statusCode(200)
      .header("Content-Type", "application/json")
  }

}

As I mentioned previously- by default, with @MicronautTest the real application is started. Moreover, nothing here is mocked, so the Mongo test container delivered by the Micronaut Test Resources is used.

Thanks to the dedicated module we added, we can inject the RequestSpecification into our tests and easily validate our endpoint.

In this case, we perform a GET request and verify that the response status code is 200 OK and the response Content-Type header value is application/json.

Verify 404 Not Found

Similarly, we can verify that no entry is present in the database:

@Test
fun `should return 404 Not Found on GET user by ID`(spec: RequestSpecification) {
  spec
    .`when`()
    .get("/users/123123123123123123121231")
    .then()
    .statusCode(404)
}

This test will be also good proof that entries inserted to MongoDB in other tests do not affect other tests.

Extract and JSON Array in REST Assured

Nextly, let’s take a look at the more complicated example:

@Test
fun `should create a user`(spec: RequestSpecification) {
  val request = AppUserRequest(
    firstName = "Piotr",
    lastName = "Wolak",
    email = "contact@codersee.com",
    street = "Street",
    city = "City",
    code = 123,
  )

  spec
    .`when`()
    .contentType(ContentType.JSON)
    .body(request)
    .post("/users")
    .then()
    .statusCode(201)

  val list = spec
    .`when`()
    .get("/users")
    .then()
    .statusCode(200)
    .body("size()", `is`(1))
    .extract()
    .`as`(object : TypeRef<List<AppUser>>() {})

  assertEquals(1, list.size)

  val createdUser = list.first()

  assertEquals(request.firstName, createdUser.firstName)
  assertEquals(request.street, createdUser.address.street)
}

This time, we do a bunch of more interesting things.

Firstly, we make the POST request with a request body and verify that 201 Creates is returned.

After that, we call the GET /users endpoint to get a list of users. When we get the response, we verify that the JSON array size is equal to 1 (with the .body("size()", is(1)) call). Lastly, we make use of the extract().`as` to convert the payload into the List of AppUser instances. Thanks to that we can easily perform assertions with JUnit 5 functions.

Add Kotlin Utils For REST Assured

As you probably know, when and as are keywords in Kotlin and that’s why we had to use backticks (“).

And to make our lives easier, let’s add the util package in test and create the TestUtil.kt:

import io.restassured.common.mapper.TypeRef
import io.restassured.response.ValidatableResponse
import io.restassured.specification.RequestSpecification


fun RequestSpecification.whenever(): RequestSpecification {
  return this.`when`()
}

fun <T> ValidatableResponse.extractAs(clazz: Class<T>) =
  this.extract()
    .`as`(clazz)

fun <T> ValidatableResponse.extractAs(typeRef: TypeRef<T>) =
  this.extract()
    .`as`(typeRef)

As a result, from now on we can simply use the whenever() instead of `when`() and extractAs() instead of extract().`as`().

Reference The Server / Context

As the next step, let’s take a look at how to reference the server or current application context:

@MicronautTest
class InjectingExample{

  @Inject
  private lateinit var context: ApplicationContext

  @Inject
  private lateinit var server: EmbeddedServer

  // tests
}

Sometimes it can be useful, and as we can see, we can simply inject them using the @Inject annotation.

Conditionally Run Tests

After that, let’s see how we can skip tests execution.

Why would we need that?

Well, integration tests sometimes can take a lot of time, and the bigger our project becomes, the bigger the chance they become annoying. And because of that, generally, it’s a good approach to somehow separate them from the faster unit tests.

When testing in Micronaut, we can easily achieve that using the @Requires annotation:

import io.micronaut.context.annotation.Requires
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

@MicronautTest
@Requires(env = ["integration-test"])
class SomeIntegrationTest {

  @Test
  fun `should perform integration test`() {
    assertTrue(false)
  }

}

The @MicronautTest annotation turns tests into beans. And that’s why we can use the @Requires, just like we would do with a “standard” bean.

As a result, tests inside the SomeIntegrationTest will run only, when the integration-test the environment is active. (And we can set that for example by setting the environment variable- MICRONAUT_ENVIRONMENTS=integration-test).

Testing Micronaut With MockK Mocks

As the last thing in our tutorial about testing Micronaut with Kotlin, let’s take a look at how we can mock things using the MockK.

Could we use Mockito? Yes. However, I find MockK better for Kotlin (DSLs <3 ).

In order to mock a bean in Micronaut, the only thing we need to do is annotate the method/inner class with the @MockBean annotation:

@MicronautTest
class AppUserControllerTestWithMocking {

  private val repo: AppUserRepository = mockk<AppUserRepository>()

  @MockBean(AppUserRepository::class)
  fun appUserRepository(): AppUserRepository = repo
}

As we can see, we introduce the AppUserRepository mock as the repo property, so that later, we will be able to refer to it easily in our test cases. Additionally, we add the function annotated with @MockBean, thus informing Micronaut that the AppUserRepository is the type we want to replace with our mock.

Moreover, thanks to this approach this mock will be limited only to tests defined in this class. So we can “rest assured” (hue hue) it won’t interfere with other classes.

As a result, our tests will look, as follows:

@MicronautTest
class AppUserControllerTestWithMocking {

  private val repo: AppUserRepository = mockk<AppUserRepository>()

  @Test
  fun `should return 200 OK on GET users`(spec: RequestSpecification) {
    every {
      repo.findAll()
    } returns emptyList()

    spec
      .whenever()
      .get("/users")
      .then()
      .statusCode(200)
      .header("Content-Type", "application/json")
  }

  @Test
  fun `should return user by ID`(spec: RequestSpecification) {
    val foundUser = AppUser(
      id = "123",
      firstName = "Piotr",
      lastName = "Wolak",
      email = "contact@codersee.com",
      address = Address(
        street = "street",
        city = "city",
        code = 123
      )
    )

    every {
      repo.findById("123")
    } returns Optional.of(foundUser)

    spec
      .whenever()
      .get("/users/123")
      .then()
      .statusCode(200)
      .body("id", equalTo("123"))
      .body("firstName", equalTo("Piotr"))
      .body("address.street", equalTo("street"))
      .body("address.code", equalTo(123))
  }

  @Test
  fun `should return user by ID and verify with extract`(spec: RequestSpecification) {
    val foundUser = AppUser(
      id = "123",
      firstName = "Piotr",
      lastName = "Wolak",
      email = "contact@codersee.com",
      address = Address(
        street = "street",
        city = "city",
        code = 123
      )
    )

    every {
      repo.findById("123")
    } returns Optional.of(foundUser)

    val extracted = spec
      .whenever()
      .get("/users/123")
      .then()
      .statusCode(200)
      .extractAs(AppUser::class.java)

    assertEquals(foundUser, extracted)
  }

  @Test
  fun `should create a user`(spec: RequestSpecification) {
    val request = AppUserRequest(
      firstName = "Piotr",
      lastName = "Wolak",
      email = "contact@codersee.com",
      street = "Street",
      city = "City",
      code = 123,
    )

    val createdUser = AppUser(
      id = "123",
      firstName = "Piotr",
      lastName = "Wolak",
      email = "contact@codersee.com",
      address = Address(
        street = "street",
        city = "city",
        code = 123
      )
    )

    every {
      repo.save(any())
    } returns createdUser

    val extracted = spec
      .whenever()
      .contentType(ContentType.JSON)
      .body(request)
      .post("/users")
      .then()
      .statusCode(201)
      .extractAs(AppUser::class.java)

    assertEquals(createdUser, extracted)
  }

  @MockBean(AppUserRepository::class)
  fun appUserRepository(): AppUserRepository = repo
}

Summary

And that’s all for this tutorial on how to perform testing in Micronaut with Kotlin.

Together, we’ve discovered a bunch of interesting things that I hope will be useful in your projects.

If you would like to see the codebase for this tutorial, then check out this GitHub repository. Or, if you are interested in learning more about Micronaut, then check out my other posts.

Let me know your thoughts in the comments section below! 🙂

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy