-
Notifications
You must be signed in to change notification settings - Fork 8
test: WebSocket 관련 테스트 코드 작성 #440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,8 @@ | |||||||||||||||||||||||
| import com.example.solidconnection.chat.domain.ChatReadStatus; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.domain.ChatRoom; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.dto.ChatMessageResponse; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.dto.ChatMessageSendRequest; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.dto.ChatMessageSendResponse; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.dto.ChatRoomListResponse; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.fixture.ChatAttachmentFixture; | ||||||||||||||||||||||||
| import com.example.solidconnection.chat.fixture.ChatMessageFixture; | ||||||||||||||||||||||||
|
|
@@ -29,10 +31,14 @@ | |||||||||||||||||||||||
| import org.junit.jupiter.api.DisplayName; | ||||||||||||||||||||||||
| import org.junit.jupiter.api.Nested; | ||||||||||||||||||||||||
| import org.junit.jupiter.api.Test; | ||||||||||||||||||||||||
| import org.mockito.ArgumentCaptor; | ||||||||||||||||||||||||
| import org.mockito.BDDMockito; | ||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||||||||||||||||||||||
| import org.springframework.boot.test.mock.mockito.MockBean; | ||||||||||||||||||||||||
| import org.springframework.data.domain.PageRequest; | ||||||||||||||||||||||||
| import org.springframework.data.domain.Pageable; | ||||||||||||||||||||||||
| import org.springframework.data.domain.Sort; | ||||||||||||||||||||||||
| import org.springframework.messaging.simp.SimpMessagingTemplate; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @TestContainerSpringBootTest | ||||||||||||||||||||||||
| @DisplayName("채팅 서비스 테스트") | ||||||||||||||||||||||||
|
|
@@ -62,6 +68,9 @@ class ChatServiceTest { | |||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||
| private ChatAttachmentFixture chatAttachmentFixture; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @MockBean | ||||||||||||||||||||||||
| private SimpMessagingTemplate simpMessagingTemplate; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| private SiteUser user; | ||||||||||||||||||||||||
| private SiteUser mentor1; | ||||||||||||||||||||||||
| private SiteUser mentor2; | ||||||||||||||||||||||||
|
|
@@ -363,4 +372,53 @@ void setUp() { | |||||||||||||||||||||||
| .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @Nested | ||||||||||||||||||||||||
| class 채팅_메시지를_전송한다 { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| private SiteUser sender; | ||||||||||||||||||||||||
| private ChatParticipant senderParticipant; | ||||||||||||||||||||||||
| private ChatRoom chatRoom; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @BeforeEach | ||||||||||||||||||||||||
| void setUp() { | ||||||||||||||||||||||||
| sender = siteUserFixture.사용자(111, "sender"); | ||||||||||||||||||||||||
| chatRoom = chatRoomFixture.채팅방(false); | ||||||||||||||||||||||||
| senderParticipant = chatParticipantFixture.참여자(sender.getId(), chatRoom); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||
| void 채팅방_참여자는_메시지를_전송할_수_있다() { | ||||||||||||||||||||||||
| // given | ||||||||||||||||||||||||
| final String content = "안녕하세요"; | ||||||||||||||||||||||||
| ChatMessageSendRequest request = new ChatMessageSendRequest(content); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // when | ||||||||||||||||||||||||
| chatService.sendChatMessage(request, sender.getId(), chatRoom.getId()); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // then | ||||||||||||||||||||||||
| ArgumentCaptor<String> destinationCaptor = ArgumentCaptor.forClass(String.class); | ||||||||||||||||||||||||
| ArgumentCaptor<ChatMessageSendResponse> payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| BDDMockito.verify(simpMessagingTemplate).convertAndSend(destinationCaptor.capture(), payloadCaptor.capture()); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| assertAll( | ||||||||||||||||||||||||
| () -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()), | ||||||||||||||||||||||||
| () -> assertThat(payloadCaptor.getValue().content()).isEqualTo(content), | ||||||||||||||||||||||||
| () -> assertThat(payloadCaptor.getValue().senderId()).isEqualTo(senderParticipant.getId()) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||
| void 채팅_참여자가_아니면_메시지를_전송할_수_없다() { | ||||||||||||||||||||||||
| // given | ||||||||||||||||||||||||
| SiteUser nonParticipant = siteUserFixture.사용자(333, "nonParticipant"); | ||||||||||||||||||||||||
| ChatMessageSendRequest request = new ChatMessageSendRequest("안녕하세요"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // when & then | ||||||||||||||||||||||||
| assertThatCode(() -> chatService.sendChatMessage(request, nonParticipant.getId(), chatRoom.getId())) | ||||||||||||||||||||||||
| .isInstanceOf(CustomException.class) | ||||||||||||||||||||||||
| .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+418
to
+422
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 실패 케이스에서 메시지 브로커 호출이 전혀 없음을 보장하세요.
assertThatCode(() -> chatService.sendChatMessage(request, nonParticipant.getId(), chatRoom.getId()))
.isInstanceOf(CustomException.class)
.hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage());
+ org.mockito.Mockito.verifyNoInteractions(simpMessagingTemplate);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,114 @@ | ||||||||||||||||||||||||||||||||
| package com.example.solidconnection.websocket; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||||||||||||||||||||||||||||||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||||||||||||||||||||||||||||||
| import static org.assertj.core.api.ThrowableAssert.catchThrowable; | ||||||||||||||||||||||||||||||||
| import static org.junit.jupiter.api.Assertions.assertAll; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import com.example.solidconnection.auth.service.AccessToken; | ||||||||||||||||||||||||||||||||
| import com.example.solidconnection.auth.service.AuthTokenProvider; | ||||||||||||||||||||||||||||||||
| import com.example.solidconnection.siteuser.domain.SiteUser; | ||||||||||||||||||||||||||||||||
| import com.example.solidconnection.siteuser.fixture.SiteUserFixture; | ||||||||||||||||||||||||||||||||
| import com.example.solidconnection.support.TestContainerSpringBootTest; | ||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||
| import java.util.concurrent.ArrayBlockingQueue; | ||||||||||||||||||||||||||||||||
| import java.util.concurrent.BlockingQueue; | ||||||||||||||||||||||||||||||||
| import java.util.concurrent.ExecutionException; | ||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.AfterEach; | ||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.BeforeEach; | ||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.DisplayName; | ||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.Nested; | ||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.Test; | ||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||||||||||||||||||||||||||||||
| import org.springframework.boot.test.web.server.LocalServerPort; | ||||||||||||||||||||||||||||||||
| import org.springframework.messaging.converter.MappingJackson2MessageConverter; | ||||||||||||||||||||||||||||||||
| import org.springframework.messaging.simp.stomp.StompHeaders; | ||||||||||||||||||||||||||||||||
| import org.springframework.messaging.simp.stomp.StompSession; | ||||||||||||||||||||||||||||||||
| import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.client.HttpClientErrorException; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.WebSocketHttpHeaders; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.client.standard.StandardWebSocketClient; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.messaging.WebSocketStompClient; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.sockjs.client.SockJsClient; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.sockjs.client.Transport; | ||||||||||||||||||||||||||||||||
| import org.springframework.web.socket.sockjs.client.WebSocketTransport; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @TestContainerSpringBootTest | ||||||||||||||||||||||||||||||||
| @DisplayName("WebSocket/STOMP 통합 테스트") | ||||||||||||||||||||||||||||||||
| class WebSocketStompIntegrationTest { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @LocalServerPort | ||||||||||||||||||||||||||||||||
| private int port; | ||||||||||||||||||||||||||||||||
| private String url; | ||||||||||||||||||||||||||||||||
| private WebSocketStompClient stompClient; | ||||||||||||||||||||||||||||||||
| private StompSession stompSession; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||
| private AuthTokenProvider authTokenProvider; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Autowired | ||||||||||||||||||||||||||||||||
| private SiteUserFixture siteUserFixture; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @BeforeEach | ||||||||||||||||||||||||||||||||
| void setUp() { | ||||||||||||||||||||||||||||||||
| this.url = String.format("ws://localhost:%d/connect", port); | ||||||||||||||||||||||||||||||||
| List<Transport> transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); | ||||||||||||||||||||||||||||||||
| this.stompClient = new WebSocketStompClient(new SockJsClient(transports)); | ||||||||||||||||||||||||||||||||
| this.stompClient.setMessageConverter(new MappingJackson2MessageConverter()); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @AfterEach | ||||||||||||||||||||||||||||||||
| void tearDown() { | ||||||||||||||||||||||||||||||||
| if (this.stompSession != null && this.stompSession.isConnected()) { | ||||||||||||||||||||||||||||||||
| this.stompSession.disconnect(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 리소스 정리: 테스트 종료 시 stompClient.stop() 호출을 권장합니다.
void tearDown() {
if (this.stompSession != null && this.stompSession.isConnected()) {
this.stompSession.disconnect();
}
+ if (this.stompClient != null) {
+ this.stompClient.stop();
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Nested | ||||||||||||||||||||||||||||||||
| class WebSocket_핸드셰이크_및_STOMP_세션_수립_테스트 { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private final BlockingQueue<Throwable> transportErrorQueue = new ArrayBlockingQueue<>(1); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private final StompSessionHandlerAdapter sessionHandler = new StompSessionHandlerAdapter() { | ||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||
| public void handleTransportError(StompSession session, Throwable exception) { | ||||||||||||||||||||||||||||||||
| transportErrorQueue.add(exception); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||
| void 인증된_사용자는_핸드셰이크를_성공한다() throws Exception { | ||||||||||||||||||||||||||||||||
| // given | ||||||||||||||||||||||||||||||||
| SiteUser user = siteUserFixture.사용자(); | ||||||||||||||||||||||||||||||||
| AccessToken accessToken = authTokenProvider.generateAccessToken(authTokenProvider.toSubject(user), user.getRole()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); | ||||||||||||||||||||||||||||||||
| handshakeHeaders.add("Authorization", "Bearer " + accessToken.token()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // when | ||||||||||||||||||||||||||||||||
| stompSession = stompClient.connectAsync(url, handshakeHeaders, new StompHeaders(), sessionHandler).get(5, SECONDS); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // then | ||||||||||||||||||||||||||||||||
| assertAll( | ||||||||||||||||||||||||||||||||
| () -> assertThat(stompSession).isNotNull(), | ||||||||||||||||||||||||||||||||
| () -> assertThat(transportErrorQueue).isEmpty() | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| @Test | ||||||||||||||||||||||||||||||||
| void 인증되지_않은_사용자는_핸드셰이크를_실패한다() { | ||||||||||||||||||||||||||||||||
| // when | ||||||||||||||||||||||||||||||||
| Throwable thrown = catchThrowable(() -> { | ||||||||||||||||||||||||||||||||
| stompSession = stompClient.connectAsync(url, new WebSocketHttpHeaders(), new StompHeaders(), sessionHandler).get(5, SECONDS); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // then | ||||||||||||||||||||||||||||||||
| assertAll( | ||||||||||||||||||||||||||||||||
| () -> assertThat(thrown) | ||||||||||||||||||||||||||||||||
| .isInstanceOf(ExecutionException.class) | ||||||||||||||||||||||||||||||||
| .hasCauseInstanceOf(HttpClientErrorException.Unauthorized.class), | ||||||||||||||||||||||||||||||||
| () -> assertThat(transportErrorQueue).hasSize(1) | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 인증 실패 시 transportErrorQueue 크기 단언을 완화하세요.
- () -> assertThat(transportErrorQueue).hasSize(1)
+ () -> assertThat(transportErrorQueue).isNotEmpty()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
검증을 인자 기반(BDD then + eq)으로 바꿔, 호출 횟수/추가 호출에 둔감하게 만듭니다.
아래처럼 수정 제안드립니다.
추가로, eq·any를 자주 쓰신다면 ArgumentMatchers에 대한 static import를 고려해 주세요.
📝 Committable suggestion
🤖 Prompt for AI Agents