Redis¶
Configure¶
TestSystem()
.with {
redis {
RedisSystemOptions {
listOf(
"redis.host=${it.host}",
"redis.port=${it.port}",
"redis.password=${it.password}"
)
}
}
}.run()
Usage¶
The Redis component provides access to the underlying Lettuce Redis client, allowing you to test all Redis operations.
Accessing the Redis Client¶
Access the Redis client using the client() extension function:
TestSystem.validate {
redis {
val redisClient = client()
val connection = redisClient.connect()
// Use the connection for Redis operations
connection.close()
}
}
String Operations¶
Test basic string operations:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// SET and GET
connection.set("user:123:name", "John Doe")
val name = connection.get("user:123:name")
name shouldBe "John Doe"
// SET with expiration
connection.setex("session:abc", 3600, "session-data")
val ttl = connection.ttl("session:abc")
ttl shouldBeGreaterThan 0
// INCREMENT
connection.set("counter", "0")
connection.incr("counter")
connection.incr("counter")
val counter = connection.get("counter")
counter shouldBe "2"
// Multiple keys
connection.mset(mapOf(
"key1" to "value1",
"key2" to "value2",
"key3" to "value3"
))
val values = connection.mget("key1", "key2", "key3")
values.size shouldBe 3
}
}
Hash Operations¶
Test Redis hash operations:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// HSET and HGET
connection.hset("user:123", "name", "John Doe")
connection.hset("user:123", "email", "john@example.com")
connection.hset("user:123", "age", "30")
val name = connection.hget("user:123", "name")
name shouldBe "John Doe"
// HGETALL
val user = connection.hgetall("user:123")
user["name"] shouldBe "John Doe"
user["email"] shouldBe "john@example.com"
user["age"] shouldBe "30"
// HMSET
connection.hmset("product:456", mapOf(
"name" to "Laptop",
"price" to "999.99",
"stock" to "10"
))
// HINCRBY
connection.hincrby("product:456", "stock", -1)
val stock = connection.hget("product:456", "stock")
stock shouldBe "9"
// HDEL
connection.hdel("user:123", "age")
val age = connection.hget("user:123", "age")
age shouldBe null
}
}
List Operations¶
Test Redis list operations:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// LPUSH and RPUSH
connection.rpush("queue:tasks", "task1", "task2", "task3")
connection.lpush("queue:tasks", "urgent-task")
// LRANGE
val tasks = connection.lrange("queue:tasks", 0, -1)
tasks.size shouldBe 4
tasks.first() shouldBe "urgent-task"
// LPOP and RPOP
val firstTask = connection.lpop("queue:tasks")
firstTask shouldBe "urgent-task"
val lastTask = connection.rpop("queue:tasks")
lastTask shouldBe "task3"
// LLEN
val length = connection.llen("queue:tasks")
length shouldBe 2
}
}
Set Operations¶
Test Redis set operations:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// SADD
connection.sadd("tags:123", "kotlin", "testing", "redis")
// SMEMBERS
val tags = connection.smembers("tags:123")
tags.size shouldBe 3
tags shouldContain "kotlin"
// SISMEMBER
val isKotlin = connection.sismember("tags:123", "kotlin")
isKotlin shouldBe true
// SREM
connection.srem("tags:123", "redis")
val remainingTags = connection.smembers("tags:123")
remainingTags.size shouldBe 2
// Set operations
connection.sadd("set1", "a", "b", "c")
connection.sadd("set2", "b", "c", "d")
// SINTER (intersection)
val intersection = connection.sinter("set1", "set2")
intersection.size shouldBe 2
intersection shouldContain "b"
intersection shouldContain "c"
// SUNION
val union = connection.sunion("set1", "set2")
union.size shouldBe 4
}
}
Sorted Set Operations¶
Test Redis sorted set operations:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// ZADD
connection.zadd("leaderboard", 100.0, "player1")
connection.zadd("leaderboard", 250.0, "player2")
connection.zadd("leaderboard", 175.0, "player3")
// ZRANGE (ascending)
val ascending = connection.zrange("leaderboard", 0, -1)
ascending.size shouldBe 3
ascending.first() shouldBe "player1"
ascending.last() shouldBe "player2"
// ZREVRANGE (descending)
val descending = connection.zrevrange("leaderboard", 0, -1)
descending.first() shouldBe "player2"
// ZSCORE
val score = connection.zscore("leaderboard", "player2")
score shouldBe 250.0
// ZRANK
val rank = connection.zrank("leaderboard", "player3")
rank shouldBe 1L // 0-indexed
// ZINCRBY
connection.zincrby("leaderboard", 50.0, "player1")
val newScore = connection.zscore("leaderboard", "player1")
newScore shouldBe 150.0
}
}
Async Operations¶
Use async operations for better performance:
TestSystem.validate {
redis {
val connection = client().connect().async()
// Async SET
val setFuture = connection.set("async:key", "async:value")
setFuture.await() shouldBe "OK"
// Async GET
val getFuture = connection.get("async:key")
val value = getFuture.await()
value shouldBe "async:value"
// Pipeline multiple operations
connection.setAutoFlushCommands(false)
val futures = listOf(
connection.set("key1", "value1"),
connection.set("key2", "value2"),
connection.set("key3", "value3")
)
connection.flushCommands()
futures.forEach { it.await() shouldBe "OK" }
}
}
Pub/Sub Operations¶
Test Redis Pub/Sub:
TestSystem.validate {
redis {
val pubConnection = client().connectPubSub().sync()
val subConnection = client().connectPubSub().sync()
// Subscribe to channel
val messages = mutableListOf<String>()
subConnection.addListener(object : RedisPubSubAdapter<String, String>() {
override fun message(channel: String, message: String) {
messages.add(message)
}
})
subConnection.subscribe("notifications")
// Publish messages
pubConnection.publish("notifications", "User logged in")
pubConnection.publish("notifications", "Order created")
// Wait for messages
delay(1.seconds)
messages.size shouldBe 2
messages shouldContain "User logged in"
messages shouldContain "Order created"
subConnection.unsubscribe("notifications")
}
}
Expiration and TTL¶
Test key expiration:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// Set with expiration
connection.setex("temp:data", 5, "temporary-value")
// Check TTL
val ttl = connection.ttl("temp:data")
ttl shouldBeGreaterThan 0
ttl shouldBeLessThanOrEqual 5
// Set expiration on existing key
connection.set("permanent", "data")
connection.expire("permanent", 10)
val newTtl = connection.ttl("permanent")
newTtl shouldBeGreaterThan 0
// Remove expiration
connection.persist("permanent")
val persistedTtl = connection.ttl("permanent")
persistedTtl shouldBe -1 // No expiration
}
}
Transactions¶
Test Redis transactions:
TestSystem.validate {
redis {
val connection = client().connect().sync()
connection.multi()
connection.set("account:1:balance", "1000")
connection.decrby("account:1:balance", 100)
connection.incrby("account:2:balance", 100)
val results = connection.exec()
results.size shouldBe 3
val balance1 = connection.get("account:1:balance")
balance1 shouldBe "900"
val balance2 = connection.get("account:2:balance")
balance2 shouldBe "100"
}
}
Pause and Unpause Container¶
Test failure scenarios:
TestSystem.validate {
redis {
val connection = client().connect().sync()
// Redis is running
connection.set("test", "value")
connection.get("test") shouldBe "value"
// Pause container
pause()
// Operations should fail
shouldThrow<RedisException> {
connection.get("test")
}
// Unpause container
unpause()
// Wait for recovery
delay(2.seconds)
// Operations should work again
val value = connection.get("test")
value shouldBe "value"
}
}
Complete Example¶
Here's a complete caching test example:
test("should cache product data in redis") {
TestSystem.validate {
val productId = "product-123"
val connection = redis { client().connect().sync() }
// Product not in cache
val cached = connection.get("cache:product:$productId")
cached shouldBe null
// Fetch from database and cache
http {
get<ProductResponse>("/products/$productId") { product ->
product.id shouldBe productId
product.name shouldNotBe null
// Store in Redis cache
redis {
val conn = client().connect().sync()
conn.setex(
"cache:product:$productId",
3600, // 1 hour TTL
objectMapper.writeValueAsString(product)
)
}
}
}
// Verify cached
redis {
val conn = client().connect().sync()
val cachedData = conn.get("cache:product:$productId")
cachedData shouldNotBe null
val cachedProduct = objectMapper.readValue(cachedData, ProductResponse::class.java)
cachedProduct.id shouldBe productId
}
// Verify TTL is set
redis {
val conn = client().connect().sync()
val ttl = conn.ttl("cache:product:$productId")
ttl shouldBeGreaterThan 0
ttl shouldBeLessThanOrEqual 3600
}
}
}
Integration with Application¶
Test application caching behavior:
test("should use redis for session management") {
TestSystem.validate {
val sessionId = UUID.randomUUID().toString()
// Create session via API
http {
postAndExpectBody<SessionResponse>(
uri = "/auth/login",
body = LoginRequest(username = "user", password = "pass").some()
) { response ->
response.status shouldBe 200
response.body().sessionId shouldBe sessionId
}
}
// Verify session in Redis
redis {
val connection = client().connect().sync()
val sessionData = connection.get("session:$sessionId")
sessionData shouldNotBe null
val session = objectMapper.readValue(sessionData, Session::class.java)
session.username shouldBe "user"
session.createdAt shouldNotBe null
}
// Use session
http {
get<UserProfile>(
uri = "/profile",
headers = mapOf("X-Session-ID" to sessionId)
) { profile ->
profile.username shouldBe "user"
}
}
// Logout
http {
postAndExpectBodilessResponse(
uri = "/auth/logout",
body = LogoutRequest(sessionId = sessionId).some()
) { response ->
response.status shouldBe 200
}
}
// Verify session removed from Redis
redis {
val connection = client().connect().sync()
val sessionData = connection.get("session:$sessionId")
sessionData shouldBe null
}
}
}
Advanced: Custom Extensions¶
Create reusable extensions for common patterns:
// Custom extension functions
fun RedisSystem.shouldGet(key: String, assertion: (String?) -> Unit): RedisSystem {
val connection = client().connect().sync()
val value = connection.get(key)
assertion(value)
return this
}
fun RedisSystem.shouldSet(key: String, value: String): RedisSystem {
val connection = client().connect().sync()
connection.set(key, value)
return this
}
// Usage in tests
TestSystem.validate {
redis {
shouldSet("user:123", "John Doe")
shouldGet("user:123") { value ->
value shouldBe "John Doe"
}
}
}