웹소켓을 모킹없이 수신메시지를 검사하는 방법을 먼저알아보고

웹소켓 핸들러에 연결된 액터모델만 테스트하기 위해 웹소켓만 모킹을 한후 액터모델 수신메시지 검사를 하는방법을 알아보겠습니다.


이전장 : 분산처리 Reactive 웹소켓 by 액터모델

SocketActorHandlerTest 

class SocketActorHandlerTest {
    companion object {
        private lateinit var client: OkHttpClient
        private lateinit var request: Request
        private val receivedMessages = mutableListOf<String>()

        @BeforeAll
        @JvmStatic
        fun setup() {
            client = OkHttpClient()
            request = Request.Builder().url("ws://localhost:8080/ws-actor").build()
        }
    }

    fun assertContainsText(text: String) {
        val objectMapper = jacksonObjectMapper()
        val messages = receivedMessages.map { objectMapper.readValue<Map<String, Any>>(it)["message"] as String }
        if (messages.any { it.contains(text) }) {
            return
        }
        throw AssertionError("The list does not contain the text '$text'")
    }


수신검사

    @Test
    fun testWebSocketConnection() {
        val latch = CountDownLatch(1) // 1초동안 수신대기목적
        val listener = object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                webSocket.send("hello")
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                println("Received message: $text")
                receivedMessages.add(text)
                latch.countDown()
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                t.printStackTrace()
                latch.countDown()
            }
        }

        client.newWebSocket(request, listener)
        latch.await(10, TimeUnit.SECONDS)

        assertContainsText("You are connected")
    }


pub/sub을 포함 다양한 시나리오에서 메시지를 검사할수 있으며  Queue를 사용해 하나씩 꺼내어 검사하면 

순차검증 모드로 개선할수 있습니다.


전체 코드 : https://github.com/psmon/kopring-reactive-labs/blob/main/KotlinBootReactiveLabs/src/test/kotlin/org/example/kotlinbootreactivelabs/ws/actor/SocketActorHandlerTest.kt



ActorTestKit을 이용하는 경우 관찰자를 통해 유사한 방식으로 수신메시지를 꺼내 코어로직 블락킹없는 순차검증이 가능합니다.

SessionManagerActorTest

class SessionManagerActorTest {

    companion object {
        private val testKit = ActorTestKit.create()

        @BeforeAll
        @JvmStatic
        fun setup() {
            // Setup code if needed
        }

        @AfterAll
        @JvmStatic
        fun tearDown() {
            testKit.shutdownTestKit()
        }
    }

수신검사

    @Test
    fun testAddSession() {
        val sessionManagerActor: ActorRef<UserSessionCommand> = testKit.spawn(SessionManagerActor.create(), "session-manager-actor")
        val probe: TestProbe<UserSessionCommandResponse> = testKit.createTestProbe()

        val session = Mockito.mock(WebSocketSession::class.java)
        Mockito.`when`(session.id).thenReturn("session1")
        val message = Mockito.mock(WebSocketMessage::class.java)
        Mockito.`when`(session.textMessage(Mockito.anyString())).thenReturn(message)
        Mockito.`when`(session.send(Mockito.any())).thenReturn(Mono.empty())

        sessionManagerActor.tell(UserSessionCommand.AddSession(session, probe.ref))

        probe.expectMessage(Information("Session added session1"))

    }


전체 코드 :  https://github.com/psmon/kopring-reactive-labs/blob/main/KotlinBootReactiveLabs/src/test/kotlin/org/example/kotlinbootreactivelabs/ws/actor/SessionManagerActorTest.kt



이벤트 드리븐 방식을 이용할때, 메시지큐 작동 코드를 블락하지 않고 수신메시지를 검사할수 있는 기능은 중요할수 있습니다.

각자의 고유 메일박스(메시지큐)를 가진 액터모델을 유닛테스트하는, 여기서 소개된 ActorTestKit 컨셉도 함께 살펴보겠습니다.


1. 비동기적인 Actor 시스템을 자연스럽게 테스트

일반적인 동기 테스트에서는 Thread.sleep() 같은 비효율적인 방법을 사용해야 하지만, ActorTestKit은 내부적으로 TestProbeask-pattern을 활용하여 비동기 메시지 기반 시스템을 효과적으로 테스트할 수 있습니다.


2. 독립적인 Actor 시스템 환경 제공

ActorTestKit테스트마다 새로운 Actor 시스템을 생성하여 실행되므로, 공유 상태(shared state)로 인해 발생할 수 있는 문제를 방지할 수 있습니다.

class SampleActorTest : WordSpec({
    val testKit = ActorTestKit.create()
    val probe = testKit.createTestProbe<String>()
    
    "SampleActor" should {
        "reply with expected message" {
            val sampleActor = testKit.spawn(SampleActor())

            sampleActor.tell("ping", probe.ref)

            probe.expectMessage("pong")
        }
    }

    afterTest {
        testKit.shutdownTestKit()
    }
})




3. 블로킹 없이 안정적인 테스트

ActorTestKitFuture, ask-pattern, TestProbe 등의 기능을 활용하여 블로킹 없이도 메시지의 처리 결과를 검증할 수 있습니다.


val responseFuture = testKit.spawn(SampleActor()).ask { replyTo -> 
    SampleActor.Message("ping", replyTo)
}

val response = responseFuture.await()
assertEquals("pong", response)




4. 타임아웃 기반의 신뢰성 높은 테스트

ActorTestKit은 기본적으로 타임아웃을 설정할 수 있어, 특정 시간이 지나도 메시지가 도착하지 않으면 테스트를 실패로 처리할 수 있습니다.


5. 가짜 시간(Fake Time) 및 시뮬레이션 가능

테스트 환경에서 시간 제어가 필요할 때 TestKit은 **가짜 시간(Fake Time)**을 활용할 수 있습니다.


val probe = testKit.createTestProbe<String>()
val scheduler = testKit.scheduler()
scheduler.scheduleOnce(100.milliseconds, probe.ref) { "delayed-message" }

probe.expectMessage("delayed-message") // 100ms 후 메시지가 도착하는지 검증




정리

장점설명
비동기 메시지 기반 테스트TestProbeask-pattern을 활용해 자연스럽게 비동기 시스템을 검증 가능
독립적인 Actor 시스템 제공테스트 간 공유 상태 문제 없이 독립적 실행
블로킹 없이 테스트 가능askexpectMessage를 활용해 Future 기반으로 동작
타임아웃 기반 신뢰성메시지 수신 여부를 일정 시간 내 검증 가능
가짜 시간(Fake Time) 활용 가능TestScheduler로 지연 메시지 및 타이머 이벤트 테스트 가능

즉, ActorTestKit비동기 Actor 시스템을 효율적이고 안정적으로 검증할 수 있는 강력한 테스트 도구입니다. 🚀