Skip to content

Commit df64624

Browse files
committed
Introduce RedisTestBase for integration tests using Testcontainers, add new test classes for event and request/response handling, and refactor LambdaMetafactory usage in RequestResponseBus and RedisEventBus to improve testability and maintainability.
1 parent 7863944 commit df64624

File tree

14 files changed

+260
-480
lines changed

14 files changed

+260
-480
lines changed

build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id("dev.slne.surf.surfapi.gradle.core") version "1.21.11+"
3+
// id("dev.slne.surf.surfapi.gradle.standalone") version "1.21.11+" /* Uncomment to use tests */
34
}
45

56
group = "dev.slne"
@@ -9,6 +10,12 @@ version = findProperty("version") as String
910
dependencies {
1011
// Lettuce Redis client
1112
implementation("io.lettuce:lettuce-core:7.2.1.RELEASE")
13+
14+
testImplementation(kotlin("test"))
15+
testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.3")
16+
testImplementation("com.redis:testcontainers-redis:2.2.4")
17+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
18+
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
1219
}
1320

1421
tasks.test {

src/main/kotlin/de/slne/redis/event/RedisEventBus.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ class RedisEventBus internal constructor(private val api: RedisApi) {
198198
continue
199199
}
200200

201-
val eventConsumer = createRedisEventConsumer(listener, method)
201+
val eventConsumer = createRedisEventConsumer(listener, method, firstParamType)
202202
eventHandlers.computeIfAbsent(firstParamType) { ObjectArrayList() }
203203
.add(eventConsumer)
204204
}
@@ -214,21 +214,28 @@ class RedisEventBus internal constructor(private val api: RedisApi) {
214214
private fun createRedisEventConsumer(
215215
listener: Any,
216216
method: Method,
217+
eventType: Class<out RedisEvent>
217218
): RedisEventConsumer {
218-
val eventConsumerMethodType = MethodType.methodType(Void.TYPE, RedisEvent::class.java)
219-
val handle = lookup.unreflect(method).bindTo(listener).asType(eventConsumerMethodType)
219+
val listenerClass = listener.javaClass
220+
val listenerLookup = MethodHandles.privateLookupIn(listenerClass, lookup)
221+
222+
val samMethodType = MethodType.methodType(Void.TYPE, Any::class.java)
223+
val implMethod = listenerLookup.unreflect(method)
224+
val instantiatedMethodType = MethodType.methodType(Void.TYPE, eventType)
225+
val invokedType = MethodType.methodType(RedisEventConsumer::class.java, listenerClass)
226+
220227

221228
val callSite = LambdaMetafactory.metafactory(
222-
lookup,
229+
listenerLookup,
223230
"accept",
224-
MethodType.methodType(RedisEventConsumer::class.java),
225-
eventConsumerMethodType,
226-
handle,
227-
eventConsumerMethodType
231+
invokedType,
232+
samMethodType,
233+
implMethod,
234+
instantiatedMethodType
228235
)
229236

230237
val factory = callSite.target
231-
return factory.invokeExact() as RedisEventConsumer
238+
return factory.invoke(listener) as RedisEventConsumer
232239
}
233240

234241
/**
@@ -308,6 +315,6 @@ class RedisEventBus internal constructor(private val api: RedisApi) {
308315
@Suppress("unused")
309316
@FunctionalInterface
310317
private fun interface RedisEventConsumer {
311-
fun accept(event: RedisEvent)
318+
fun accept(event: Any)
312319
}
313320
}

src/main/kotlin/de/slne/redis/request/RequestResponseBus.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,24 +356,25 @@ class RequestResponseBus internal constructor(
356356
* near-direct invocation performance.
357357
*/
358358
private fun createRequestConsumer(instance: Any, method: Method): RequestConsumer {
359-
val consumerMethodType = MethodType.methodType(Void.TYPE, RequestContext::class.java)
359+
val handlerClass = instance.javaClass
360+
val handlerLookup = MethodHandles.privateLookupIn(handlerClass, lookup)
360361

361-
val impl = lookup.unreflect(method)
362-
.bindTo(instance)
363-
.asType(consumerMethodType)
362+
val samMethodType = MethodType.methodType(Void.TYPE, RequestContext::class.java)
363+
val implMethod = handlerLookup.unreflect(method)
364+
val invokedType = MethodType.methodType(RequestConsumer::class.java, handlerClass)
364365

365366
val callSite = LambdaMetafactory.metafactory(
366367
lookup,
367368
"accept",
368-
MethodType.methodType(RequestConsumer::class.java),
369-
consumerMethodType,
370-
impl,
371-
consumerMethodType
369+
invokedType,
370+
samMethodType,
371+
implMethod,
372+
samMethodType
372373
)
373374

374375
val target = callSite.target
375376

376-
return target.invokeExact() as RequestConsumer
377+
return target.invoke(instance) as RequestConsumer
377378
}
378379

379380
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package de.slne.redis
2+
3+
import kotlinx.coroutines.test.runTest
4+
import org.junit.jupiter.api.Test
5+
import kotlin.test.assertTrue
6+
7+
class RedisSmokeTest : RedisTestBase() {
8+
9+
@Test
10+
fun `redis container responds to ping`() = runTest {
11+
assertTrue(redisApi.isAlive())
12+
}
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package de.slne.redis
2+
3+
import com.redis.testcontainers.RedisContainer
4+
import io.lettuce.core.RedisURI
5+
import org.junit.jupiter.api.AfterEach
6+
import org.junit.jupiter.api.BeforeEach
7+
import org.testcontainers.junit.jupiter.Container
8+
import org.testcontainers.junit.jupiter.Testcontainers
9+
10+
@Testcontainers
11+
abstract class RedisTestBase {
12+
companion object {
13+
@JvmStatic
14+
@Container
15+
val redisContainer =
16+
RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME.withTag(RedisContainer.DEFAULT_TAG))
17+
}
18+
19+
lateinit var redisApi: RedisApi
20+
21+
@BeforeEach
22+
fun setUpRedisApi() {
23+
val api = RedisApi.create(RedisURI.create(redisContainer.redisURI))
24+
beforeApiFreeze(api)
25+
api.freezeAndConnect()
26+
redisApi = api
27+
}
28+
29+
open fun beforeApiFreeze(api: RedisApi) = Unit
30+
31+
@AfterEach
32+
fun tearDownRedisApi() {
33+
if (::redisApi.isInitialized) {
34+
redisApi.disconnect()
35+
}
36+
}
37+
}
Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,43 @@
11
package de.slne.redis.event
22

3-
import kotlinx.coroutines.runBlocking
3+
import de.slne.redis.RedisApi
4+
import de.slne.redis.RedisTestBase
5+
import kotlinx.coroutines.test.runTest
46
import kotlinx.serialization.Serializable
5-
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.AfterEach
8+
import kotlin.test.Test
69
import kotlin.test.assertEquals
7-
import kotlin.test.assertTrue
810

9-
class RedisEventTest {
10-
11+
class RedisEventBusTest: RedisTestBase() {
12+
1113
@Serializable
12-
data class TestEvent(val message: String, val value: Int) : RedisEvent()
13-
14-
@Test
15-
fun `test event creation includes timestamp`() {
16-
val event = TestEvent("test", 42)
17-
assertTrue(event.timestamp > 0)
18-
assertTrue(event.timestamp <= System.currentTimeMillis())
19-
}
20-
21-
@Test
22-
fun `test event properties are accessible`() {
23-
val event = TestEvent("hello", 123)
24-
assertEquals("hello", event.message)
25-
assertEquals(123, event.value)
26-
}
27-
}
14+
data class TestEvent(val message: String) : RedisEvent()
2815

29-
class SubscribeAnnotationTest {
30-
31-
@Test
32-
fun `test Subscribe annotation can be applied to methods`() {
33-
val method = TestListener::class.java.getDeclaredMethod("onTestEvent", TestEvent::class.java)
34-
assertTrue(method.isAnnotationPresent(OnRedisEvent::class.java))
16+
override fun beforeApiFreeze(api: RedisApi) {
17+
api.subscribeToEvents(TestListener)
3518
}
36-
37-
@Serializable
38-
data class TestEvent(val data: String) : RedisEvent()
39-
40-
class TestListener {
41-
@OnRedisEvent
42-
fun onTestEvent(event: TestEvent) {
43-
// Test method
44-
}
19+
20+
@AfterEach
21+
fun cleanup() {
22+
TestListener.receivedEvents.clear()
4523
}
46-
}
4724

48-
class RedisEventBusTest {
49-
50-
@Serializable
51-
data class TestEvent(val message: String) : RedisEvent()
52-
5325
@Test
54-
fun `test listener registration scans Subscribe methods`() = runBlocking {
55-
// This test verifies that listener registration doesn't throw exceptions
56-
// Full integration testing requires a running Redis instance
57-
val listener = TestListener()
58-
59-
// Note: We can't fully test without a Redis connection
60-
// but we can verify the API works
61-
try {
62-
// This will fail to connect but validates the API
63-
val eventBus = RedisEventBus("redis://localhost:6379")
64-
eventBus.registerListener(listener)
65-
eventBus.unregisterListener(listener)
66-
eventBus.close()
67-
} catch (e: Exception) {
68-
// Expected when Redis is not running
69-
assertTrue(e.message?.contains("Unable to connect") == true ||
70-
e.message?.contains("Connection refused") == true ||
71-
e.cause?.message?.contains("Connection refused") == true)
72-
}
26+
fun testEventBusListenerReceivesEvent() = runTest {
27+
val event = TestEvent("Hello, Event Bus!")
28+
val received = redisApi.publishEvent(event).await()
29+
30+
assertEquals(1, received, "Event should be received by one listener")
31+
assertEquals(1, TestListener.receivedEvents.size, "Listener should have received one event")
32+
assertEquals(event, TestListener.receivedEvents[0], "Received event should match the published event")
7333
}
74-
75-
class TestListener {
76-
var receivedEvents = mutableListOf<TestEvent>()
77-
34+
35+
object TestListener {
36+
val receivedEvents = mutableListOf<TestEvent>()
37+
7838
@OnRedisEvent
7939
fun onTestEvent(event: TestEvent) {
8040
receivedEvents.add(event)
8141
}
8242
}
83-
}
43+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package de.slne.redis.event
2+
3+
import kotlinx.serialization.Serializable
4+
import org.junit.jupiter.api.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertTrue
7+
8+
9+
class RedisEventTest {
10+
@Serializable
11+
data class TestEvent(val message: String, val value: Int) : RedisEvent()
12+
13+
@Test
14+
fun `test event creation includes timestamp`() {
15+
val event = TestEvent("test", 42)
16+
assertTrue(event.timestamp > 0)
17+
assertTrue(event.timestamp <= System.currentTimeMillis())
18+
}
19+
20+
@Test
21+
fun `test event properties are accessible`() {
22+
val event = TestEvent("hello", 123)
23+
assertEquals("hello", event.message)
24+
assertEquals(123, event.value)
25+
}
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package de.slne.redis.event
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlin.test.Test
5+
import kotlin.test.assertTrue
6+
7+
class SubscribeAnnotationTest {
8+
9+
@Test
10+
fun `test Subscribe annotation can be applied to methods`() {
11+
val method = TestListener::class.java.getDeclaredMethod("onTestEvent", TestEvent::class.java)
12+
assertTrue(method.isAnnotationPresent(OnRedisEvent::class.java))
13+
}
14+
15+
@Serializable
16+
data class TestEvent(val data: String) : RedisEvent()
17+
18+
19+
class TestListener {
20+
@OnRedisEvent
21+
fun onTestEvent(event: TestEvent) {
22+
// Test method
23+
}
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package de.slne.redis.request
2+
3+
import kotlinx.serialization.Serializable
4+
import org.junit.jupiter.api.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertTrue
7+
8+
class RedisRequestTest {
9+
10+
@Serializable
11+
data class TestRequest(val message: String, val value: Int) : RedisRequest()
12+
13+
@Test
14+
fun `test request creation includes timestamp`() {
15+
val request = TestRequest("test", 42)
16+
assertTrue(request.timestamp > 0)
17+
assertTrue(request.timestamp <= System.currentTimeMillis())
18+
}
19+
20+
@Test
21+
fun `test request properties are accessible`() {
22+
val request = TestRequest("hello", 123)
23+
assertEquals("hello", request.message)
24+
assertEquals(123, request.value)
25+
}
26+
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package de.slne.redis.request
2+
3+
import kotlinx.serialization.Serializable
4+
import org.junit.jupiter.api.Test
5+
import kotlin.test.assertEquals
6+
import kotlin.test.assertTrue
7+
8+
class RedisResponseTest {
9+
@Serializable
10+
data class TestResponse(val data: String) : RedisResponse()
11+
12+
@Test
13+
fun `test response creation includes timestamp`() {
14+
val response = TestResponse("test data")
15+
assertTrue(response.timestamp > 0)
16+
assertTrue(response.timestamp <= System.currentTimeMillis())
17+
}
18+
19+
@Test
20+
fun `test response properties are accessible`() {
21+
val response = TestResponse("hello world")
22+
assertEquals("hello world", response.data)
23+
}
24+
}

0 commit comments

Comments
 (0)