웹소켓을 모킹없이 수신메시지를 검사하는 방법을 먼저알아보고
웹소켓 핸들러에 연결된 액터모델만 테스트하기 위해 웹소켓만 모킹을 한후 액터모델 수신메시지 검사를 하는방법을 알아보겠습니다.
이전장 : 분산처리 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'") }
- WebSocket 테스트를 하기위해 OkHttpClient가 이용
- receivedMessages : 수신메시지를 담는 List ( 검사에 이용됨 )
수신검사
@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") }
- latch.countDown() : 첫메시지를 받은후 1초간 블락킹모드가 아닌 상태로 수신대기를 할수 있습니다.
- receivedMessages.add(text) : 수신 메시지를 저장합니다.
- assertContainsText("You are connected") : 수신된 메시지중에 일치하는 메시지를 검사합니다.
- 접속하면 자동 발생되는 의도된 서버이벤트
pub/sub을 포함 다양한 시나리오에서 메시지를 검사할수 있으며 Queue를 사용해 하나씩 꺼내어 검사하면
순차검증 모드로 개선할수 있습니다.
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() } }
- testKit : 테스트 전용 ActorSystem 을 생성합니다.
- tearDown : GracefulDown 테스트를 위해 ActorSystem 종료를 명시적으로 합니다.
수신검사
@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")) }
- sessionManagerActor : 웹소켓 모듈과 연결되 pub/sub을 처리하는 액터를 생성합니다.
- session : 웹소켓은 이용이 안되기때문에 모킹으로 처리합니다.
- sessionManagerActor.tell(UserSessionCommand.AddSession(session, probe.ref)) : session1 세션추가를 요청합니다.
- probe.expectMessage(Information("Session added session1")) : session1 세션이 추가되었음을 수신검증합니다.
이벤트 드리븐 방식을 이용할때, 메시지큐 작동 코드를 블락하지 않고 수신메시지를 검사할수 있는 기능은 중요할수 있습니다.
각자의 고유 메일박스(메시지큐)를 가진 액터모델을 유닛테스트하는, 여기서 소개된 ActorTestKit 컨셉도 함께 살펴보겠습니다.
1. 비동기적인 Actor 시스템을 자연스럽게 테스트
일반적인 동기 테스트에서는 Thread.sleep()
같은 비효율적인 방법을 사용해야 하지만, ActorTestKit
은 내부적으로 TestProbe
와 ask-pattern
을 활용하여 비동기 메시지 기반 시스템을 효과적으로 테스트할 수 있습니다.
TestProbe
를 활용하면 특정 액터의 반응을 검증할 수 있음.awaitAssert
같은 기능을 사용해 폴링(polling) 없이 테스트 결과를 안전하게 검증할 수 있음.
2. 독립적인 Actor 시스템 환경 제공
ActorTestKit
은 테스트마다 새로운 Actor 시스템을 생성하여 실행되므로, 공유 상태(shared state)로 인해 발생할 수 있는 문제를 방지할 수 있습니다.
- 매 테스트마다 새로운
ActorSystem
이 제공되므로 테스트 간 간섭 방지. TestKit.shutdownTestKit()
을 통해 테스트가 끝난 후 자원을 자동 정리 가능.
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. 블로킹 없이 안정적인 테스트
ActorTestKit
은 Future
, ask-pattern
, TestProbe
등의 기능을 활용하여 블로킹 없이도 메시지의 처리 결과를 검증할 수 있습니다.
ask-pattern
을 사용하면CompletableFuture
또는Deferred
를 반환받아 비동기적으로 결과를 확인할 수 있음.TestProbe.expectMessage()
를 사용해 특정 메시지가 도착할 때까지 기다릴 수 있음.
val responseFuture = testKit.spawn(SampleActor()).ask { replyTo -> SampleActor.Message("ping", replyTo) } val response = responseFuture.await() assertEquals("pong", response)
4. 타임아웃 기반의 신뢰성 높은 테스트
ActorTestKit
은 기본적으로 타임아웃을 설정할 수 있어, 특정 시간이 지나도 메시지가 도착하지 않으면 테스트를 실패로 처리할 수 있습니다.
expectMessage(Duration.ofSeconds(2))
같은 기능을 사용해 일정 시간 내 응답이 없으면 실패 처리.- 예기치 않은 메시지 지연으로 인해 테스트가 끝없이 대기하는 문제 방지.
5. 가짜 시간(Fake Time) 및 시뮬레이션 가능
테스트 환경에서 시간 제어가 필요할 때 TestKit
은 **가짜 시간(Fake Time)**을 활용할 수 있습니다.
TestKit.scheduler().advance()
를 사용해 특정 시간 후의 동작을 테스트 가능.Scheduler.scheduleOnce()
와 함께 미래의 이벤트를 미리 트리거할 수 있음.
val probe = testKit.createTestProbe<String>() val scheduler = testKit.scheduler() scheduler.scheduleOnce(100.milliseconds, probe.ref) { "delayed-message" } probe.expectMessage("delayed-message") // 100ms 후 메시지가 도착하는지 검증
정리
장점 | 설명 |
---|---|
비동기 메시지 기반 테스트 | TestProbe 와 ask-pattern 을 활용해 자연스럽게 비동기 시스템을 검증 가능 |
독립적인 Actor 시스템 제공 | 테스트 간 공유 상태 문제 없이 독립적 실행 |
블로킹 없이 테스트 가능 | ask 와 expectMessage 를 활용해 Future 기반으로 동작 |
타임아웃 기반 신뢰성 | 메시지 수신 여부를 일정 시간 내 검증 가능 |
가짜 시간(Fake Time) 활용 가능 | TestScheduler 로 지연 메시지 및 타이머 이벤트 테스트 가능 |
즉, ActorTestKit
은 비동기 Actor 시스템을 효율적이고 안정적으로 검증할 수 있는 강력한 테스트 도구입니다. 🚀