Introduction
Our story begins with a colleague and me trying to debug an issue with our project for a few hours. We needed to fetch a value from Redis storage, but whenever the service ran, the fetched value was null
. Despite our efforts, we couldn’t find a solution on the internet, and to make matters worse, our code was actually covered by tests that had all passed!
In this article, we will explore a common issue when using RedisTemplate
in Spring applications. We’ll demonstrate how misconfigurations can lead to your tests passing while failing to fetch the correct data. Additionally, we’ll show you how to configure properly
to avoid this problem.RedisTemplate
Initial Configuration
To configure our application to connect to Redis, we will add the following configurations to your application.yml
file:
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
This configuration sets up the Redis host and port, defaulting to localhost
and 6379
if no environment variables are provided.
Creating the RedisTemplate Bean
We will now create our
bean to be used as a Redis client in our codebase.RedisTemplate
@Configuration
class RedisConfiguration {
@Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Int> =
RedisTemplate<String, Int>().apply {
this.connectionFactory = connectionFactory
}
}
In this code, we define a RedisTemplate
with a String
key type and an Int
value type, associating it with the Redis connection factory.
Writing Tests
In the next step, we will create a test that uses a Redis test container:
@SpringBootTest
@TestConstructor(autowireMode = ALL)
class RedisContainerTest(private val redisTemplate: RedisTemplate<String, Int>) {
companion object {
private const val REDIS_PORT = 6379
private val redisContainer = GenericContainer<Nothing>("redis:latest").apply {
withExposedPorts(REDIS_PORT)
start()
}
@AfterAll
@JvmStatic
fun tearDown() {
redisContainer.stop()
}
}
}
We will set our Redis host and port to the values of our test container next:
@DynamicPropertySource
@JvmStatic
@Suppress("unused")
fun registerDynamicProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host") { redisContainer.host }
registry.add("spring.data.redis.port") { redisContainer.getMappedPort(REDIS_PORT) }
}
Next, we will ensure that our Redis cache is truncated between tests, and insert a predefined key and value into Redis using the redis-cli
tool.
@BeforeEach
fun setup() {
redisTemplate.delete(redisTemplate.keys("*"))
redisContainer.execInContainer(
"redis-cli",
"SET",
KEY,
VALUE.toString()
)
}
Identifying The Issue
By running the following test with a breakpoint and checking the container running the command, we can see that the key is indeed stored in our container.
$ docker exec -it <CONTAINER_NAME> redis-cli
127.0.0.1:6379> keys *
1) "key"
Now, we will write two tests. The first will try to fetch the value from the container directly using our RedisTemplate
bean.
@Test
fun `should fail to fetch value`() {
// Given predefined int key in Redis
// When we fetch the value
val value = redisTemplate.opsForValue().get(KEY)
// Then the value is null
assertNull(value)
}
The second will update the value of the key using the RedisTemplate
and fetch the key again using the template.
@Test
fun `should successfully fetch the value`() {
// Given a value stored via RedisTemplate
redisTemplate.opsForValue().set(KEY, VALUE + 1)
// When we fetch the value
val value = redisTemplate.opsForValue().get(KEY)
// Then the value is null
assertEquals(VALUE + 1, value)
}
As you can see, the first test fails to find the key in Redis and hence returns a null
value, while the second can find the key after we use our template to update the value.
Fixing the Issue
To resolve the issue, we need to add serializers to our RedisTemplate
definition. This ensures that the keys and values are correctly serialized and deserialized when interacting with Redis.
@Configuration
class RedisConfiguration {
@Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Int> =
RedisTemplate<String, Int>().apply {
this.connectionFactory = connectionFactory
this.keySerializer = StringRedisSerializer()
this.valueSerializer = GenericToStringSerializer(Int::class.java)
}
}
You can now see that by running the following test, our code will manage to fetch the key successfully:
@Test
fun `should successfully fetch value`() {
// Given predefined int key in Redis
// When we fetch the value
val value = redisTemplate.opsForValue().get(KEY)
// Then the value is correctly fetched
assertEquals(VALUE, value)
}
Where to Find the Code
All the code presented in this article is available on the GitHub Repository Redis-Template-Key-Demo.
The following branches are available:
- main – contains the code that presents the issue described in the article
- value-fetching-fix – contains the fix to the serializer, allowing all tests to pass
Conclusion
When setting up a Redis client, make sure to configure your key and value serializers to avoid unexpected problems when reading from your cache. The problem might not appear during your tests, but it can suddenly start in production. It’s always better to be explicit when configuring your persistence layer.
Credits
- Photos by Johannes Plenio on Unsplash.
Leave a Reply