Spring Boot Redis integration test with Testcontainers in Kotlin

Spring Boot Redis integration test with Testcontainers in Kotlin

Like any other external dependency, we can write integration tests using Testcontainers for a Spring Boot application. However, unfortunately, Testcontainers does not have any library for Redis, and using Kotlin makes it even trickier. In this article, we explore how to write Spring Boot Redis integration test with Testcontainers in Kotlin.

Introduction

There are two challenges in writing a Redis integration test using Testcontainers in Kotlin.

Firstly, Testcontainers supports a variety of products such as MySQL, PostgreSQL, Kafka, and so on (visit here for the comprehensive list). Unfortunately, Redis is currently unsupported. That essentially renders using Testcontainers more complex. But thanks to the Redis community, there is a library, testcontainers-redis-junit, developed that provides Testcontainers capability out of the box. It is fully compatible and uses the same approach and annotations.

The second challenge is Kotlin itself. Using the @DynamicPropertySource annotation, which is often used in conjunction with Testctainer, in Kotlin is not as straightforward as Java, especially for those that recently embarked on the Kotlin journey. It took me a few hours to find the proper solution to make it work. Of course, once you know about it, it becomes super easy.

Adding Redis JUnit library

As mentioned, we need to utilize the testcontainers-redis-junit developed by the Redis community.

The default assumption is that you have already set up your project and know how to utilize Redis. In case you don’t know, it is worth reading getting started with Spring Data Redis with Kotlin and Redis Pub/Sub with Spring Boot articles first.

Add the library to your project,

<dependency>
    <groupId>com.redis.testcontainers</groupId>
    <artifactId>testcontainers-redis-junit</artifactId>
    <version>1.6.4</version>
    <scope>test</scope>
</dependency>

Using Redis Testcontainers to write integration tests

After adding the library, the next step is to write some integration tests. We start drafting a simple test case to ensure Redis Testcontainers runs and everything else works.

package com.madadipouya.redis.springdata.example

import com.redis.testcontainers.RedisContainer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName

@Testcontainers
@ExtendWith(SpringExtension::class)
@SpringBootTest
internal class ActorRepositoryTest {

    companion object {
        @Container
        private val redisContainer = RedisContainer(DockerImageName.parse("redis:latest"))

        @JvmStatic
        @DynamicPropertySource
        fun redisProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.data.redis.repositories.enabled") { true }
            registry.add("spring.redis.host") { redisContainer.host }
            registry.add("spring.redis.port") { redisContainer.firstMappedPort }
            registry.add("spring.redis.topic") { "movie.update" }
        }
    }

    @Test
    fun contextLoads() {
    }
}

There are two crucial points about the above code to note:

  • You need to wrap container creation and property initialization in a companion object
  • Merely adding the @DynamicPropertySource does not initialize the rediProperties method. You need to add the @JvmStatic method as well

Upon running the test, it should pass.

Now that we successfully wired Redis Testcontainers, let’s enrich the test to read and write to Redis using Spring Data Repository.

package com.madadipouya.redis.springdata.example.repository

import com.madadipouya.redis.springdata.example.model.Actor
import com.redis.testcontainers.RedisContainer
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
import java.time.LocalDate

@Testcontainers
@ExtendWith(SpringExtension::class)
@SpringBootTest
internal class ActorRepositoryTest {

    @Autowired
    private lateinit var actorRepository: ActorRepository

    companion object {
        @Container
        private val redisContainer = RedisContainer(DockerImageName.parse("redis:latest"))

        @JvmStatic
        @DynamicPropertySource
        fun redisProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.data.redis.repositories.enabled") { true }
            registry.add("spring.redis.host") { redisContainer.host }
            registry.add("spring.redis.port") { redisContainer.firstMappedPort }
            registry.add("spring.redis.topic") { "movie.update" }
        }
    }

    @BeforeEach
    fun setUp() {
        actorRepository.deleteAll()
    }

    @Test
    fun `should find an Actor by id`() {
        val actorId = createActor("Keanu", "Reeves", LocalDate.of(1964, 9, 2))

        val result = actorRepository.findById(actorId)

        assertTrue(result.isPresent)
        val actor = result.get()
        assertEquals("Keanu", actor.firstName)
        assertEquals("Reeves", actor.lastName)
        assertEquals(LocalDate.of(1964, 9, 2), actor.birthDate)
    }

    @Test
    fun `should update an Actor`() {
        val actorId = createActor("Keanu", "Reeves", LocalDate.of(1964, 9, 2))

        val actor = actorRepository.findById(actorId).map {
            val updatedActor = Actor(it.firstName, it.lastName, LocalDate.of(1969, 9, 9))
            updatedActor.id = it.id
            updatedActor
        }.get()

        actorRepository.save(actor)

        val result = actorRepository.findById(actorId)
        assertTrue(result.isPresent)
        val updatedActor = result.get()
        assertEquals("Keanu", updatedActor.firstName)
        assertEquals("Reeves", updatedActor.lastName)
        assertEquals(LocalDate.of(1969, 9, 9), updatedActor.birthDate)
    }

    fun createActor(firstName: String, lastName: String, birthDay: LocalDate): String {
        val result = actorRepository.save(Actor(firstName, lastName, birthDay))
        return result.id!!
    }
}

In the above code, we tested the create and save (upsert) methods of Sring Data Redis.

Conclusion

In this tutorial, we covered how to write Spring Boot Redis integration test with Testcontainers in Kotlin. Testcontainers does not support Redis, unlike other products such as Kafka. For that reason, we need to use the testcontainers-redis-junit library. Thankfully, that is fully compatible and even uses sample annotations.

As always, the fully functional demo project is available on GitHub at the following link:
https://github.com/kasramp/spring-data-redis-example-kotlin

Inline/featured images credits