Skip to content

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.

Open in setup wizard

Redis — wizard-synced snippet

Gradle

testImplementation("com.trendyol:stove-redis")

Stove configuration

Stove().with {
    redis {
      RedisSystemOptions(
        configureExposedConfiguration = { cfg ->
          listOf("spring.data.redis.url=${cfg.url}")
        }
      )
    }
}

Test DSL

stove {
    redis {
      client().connect().sync().get("order:1") shouldNotBe null
    }
}

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