Redis¶
Real Redis in a container or wired to an existing instance. Stove exposes direct Lettuce client access, so tests can use the same Redis primitives your app depends on: strings, hashes, lists, sets, sorted sets, pub/sub, and transactions.
Redis — wizard-synced snippet
Gradle
Stove configuration
Stove().with {
redis {
RedisSystemOptions(
configureExposedConfiguration = { cfg ->
listOf("spring.data.redis.url=${cfg.url}")
}
)
}
}
Test DSL
In 30 seconds
Register redis { RedisOptions(...) }. Inside stove { redis { } }, call client().connect().sync() for the Lettuce sync API. The same client supports async (.async()) and pub/sub (client().connectPubSub()). There is no high-level Redis assertion DSL; raw Lettuce is the API.
Configure¶
Stove().with {
redis {
RedisOptions(
configureExposedConfiguration = { cfg ->
listOf(
"spring.data.redis.url=${cfg.url}",
"spring.data.redis.host=${cfg.host}",
"spring.data.redis.port=${cfg.port}"
)
}
)
}
}.run()
DSL by data structure¶
Strings¶
stove {
redis {
val sync = client().connect().sync()
sync.set("user:1", "Alice")
sync.get("user:1") shouldBe "Alice"
sync.expire("user:1", 60) // TTL seconds
}
}
Hashes¶
stove {
redis {
val sync = client().connect().sync()
sync.hset("user:1", mapOf("name" to "Alice", "email" to "a@x.com"))
sync.hget("user:1", "email") shouldBe "a@x.com"
}
}
Lists / Sets / Sorted sets¶
stove {
redis {
val sync = client().connect().sync()
sync.lpush("queue:tasks", "task-1", "task-2")
sync.rpop("queue:tasks") shouldBe "task-1"
sync.sadd("tags:order:1", "urgent", "high-value")
sync.smembers("tags:order:1") shouldContain "urgent"
sync.zadd("leaderboard", 100.0, "player-1")
sync.zrange("leaderboard", 0, -1) shouldHaveSize 1
}
}
Async + pipelining¶
stove {
redis {
val async = client().connect().async()
val futures = (1..100).map { async.set("k:$it", "v") }
async.flushCommands()
futures.awaitAll()
}
}
Pub/Sub¶
stove {
redis {
val pubsub = client().connectPubSub()
val received = mutableListOf<String>()
pubsub.addListener(object : RedisPubSubAdapter<String, String>() {
override fun message(channel: String, message: String) {
received.add(message)
}
})
pubsub.sync().subscribe("notifications")
client().connect().sync().publish("notifications", "hello")
eventually(5.seconds) { received shouldContain "hello" }
}
}
Transactions¶
stove {
redis {
val sync = client().connect().sync()
sync.multi()
sync.set("a", "1")
sync.set("b", "2")
sync.exec()
}
}
Migrations¶
class SeedConfig : DatabaseMigration<RedisMigrationContext> {
override val order = 1
override suspend fun execute(ctx: RedisMigrationContext) {
ctx.client.connect().sync().set("config:flag", "enabled")
}
}
redis {
RedisOptions(/* ... */).migrations { register<SeedConfig>() }
}
Complete example¶
test("cache populated after order create") {
stove {
val orderId = UUID.randomUUID().toString()
http {
postAndExpectBody<OrderResponse>(
"/orders",
CreateOrderRequest(id = orderId).some()
) { it.status shouldBe 201 }
}
redis {
client().connect().sync().get("order:$orderId") shouldNotBe null
}
}
}
Pitfalls¶
| Symptom | Fix |
|---|---|
| Async futures hang | Call flushCommands() after batching |
| Pub/sub listener never fires | Subscribe before publishing; use eventually { } for the assert |
| TTL not applied | Use setex or expire after set |
Pairs well with¶
- Provided Instances for shared Redis (prefix keys with run ID)
- Best Practices · isolation