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 therediProperties
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
- Featured Image by Robert Waghorn from Pixabay