stomp test 작성

greenTea·2024년 5월 12일
0

stomp test 작성

stomp를 사용하고 있는 와중에 테스트 코드 작성 방법에 찾아보았고 여러 자료를 찾아본 끝에 아래와 같은 코드를 작성하였습니다. (많이 부족한 코드입니다...)

setUp

@UnitTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StompTest {

	@MockBean
	private JwtUtils jwtUtils;

	@MockBean
	private HandleMessageUseCase handleMessageUseCase;

	@Autowired
	private SimpMessagingTemplate simpMessagingTemplate;

	@LocalServerPort
	private int port;

	private final String SEND_MESSAGE_ENDPOINT = "/send/chat";
	private final String SUBSCRIBE_ENDPOINT = "/single";
	private String URL;

	WebSocketStompClient stompClient;
	CompletableFuture<MessageDto> arriveMessage;
	StompSessionHandlerAdapter handler;

	@BeforeEach
	void setUp() {
		URL = "ws://localhost:%d/ws".formatted(port);
		arriveMessage = new CompletableFuture<>();
		// websocket, sockjs, stomp 준비, 직렬화 설정
		stompClient = new WebSocketStompClient(
			new SockJsClient(List.of(new WebSocketTransport(new StandardWebSocketClient()))));

		MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
		ObjectMapper objectMapper = messageConverter.getObjectMapper();
		objectMapper.registerModules(new JavaTimeModule(), new ParameterNamesModule());
		stompClient.setMessageConverter(messageConverter);

		// stomp message handler
		handler = new StompSessionHandlerAdapter() {
			@Override
			public Type getPayloadType(StompHeaders headers) {
				return MessageDto.class;
			}

			@Override
			public void handleFrame(StompHeaders headers, Object payload) {
				arriveMessage.complete((MessageDto)payload);
			}
		};
	}
}

위 코드의 경우 완벽한 코드는 아니고 참고만 해주시면 좋을 것 같습니다.(통합테스트 환경을 구축한다면 더욱 간단해질 것 같지만 시간상 일단 이렇게 구현해봤습니다.)

의존성 주입 및 변수

@UnitTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StompTest {

	@MockBean
	private JwtUtils jwtUtils;

	@MockBean
	private HandleMessageUseCase handleMessageUseCase;

	@Autowired
	private SimpMessagingTemplate simpMessagingTemplate;

	@LocalServerPort
	private int port;
    
    	private final String SEND_MESSAGE_ENDPOINT = "/send/chat";
	private final String SUBSCRIBE_ENDPOINT = "/single";
	private String URL;

	WebSocketStompClient stompClient;
	CompletableFuture<MessageDto> arriveMessage;
	StompSessionHandlerAdapter handler;

구현 코드를 다 보기에는 양이 많아 간단하게 어떤 기능인지만 설명하겠습니다.

  • @UnitTest는 단순 유닛 테스트,통합 테스트 구분을 위한 어노테이션입니다.
  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)랜덤 포트로 띄웠습니다. 보다 격리된 환경에서 진행 할 수 있습니다.
  • @LocalServerPort 현재 띄워진 서버의 포트를 가져올 수 있습니다.
private final String SEND_MESSAGE_ENDPOINT = "/send/chat";
private final String SUBSCRIBE_ENDPOINT = "/single";
private String URL;

구독 및 메시지를 보낼 주소입니다.

stomp 설정

@BeforeEach
void setUp() {
	URL = "ws://localhost:%d/ws".formatted(port);
	arriveMessage = new CompletableFuture<>();
	// websocket, sockjs, stomp 준비, 직렬화 설정
	stompClient = new WebSocketStompClient(
	new SockJsClient(List.of(new WebSocketTransport(new StandardWebSocketClient()))));

	MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
	ObjectMapper objectMapper = messageConverter.getObjectMapper();
	objectMapper.registerModules(new JavaTimeModule(), new ParameterNamesModule());
	stompClient.setMessageConverter(messageConverter);

	// stomp message handler
	handler = new StompSessionHandlerAdapter() {
		@Override
		public Type getPayloadType(StompHeaders headers) {
			return MessageDto.class;
		}

		@Override
		public void handleFrame(StompHeaders headers, Object payload) {
			arriveMessage.complete((MessageDto)payload);
		}
	};
}
  1. URL = "ws://localhost:%d/ws".formatted(port);: 테스트할 WebSocket 서버의 URL입니다.

  2. arriveMessage = new CompletableFuture<>();: 테스트 중에 도착한 메시지를 처리하는 데 사용되는 CompletableFuture 객체를 초기화합니다. 이를 통해 비동기적으로 일어나는 메시지 수신 과정을 컨트롤 할 수 있습니다.

  3. stompClient = new WebSocketStompClient(...);: WebSocketStompClient 객체를 생성하여 STOMP 프로토콜을 사용하여 WebSocket 통신을 설정합니다.

  4. MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();: Jackson 라이브러리를 사용하여 JSON 형식의 메시지를 STOMP 메시지로 변환하고 역변환하는 데 사용될 MessageConverter 객체를 생성합니다.

  5. stompClient.setMessageConverter(messageConverter);: WebSocketStompClient에 MessageConverter를 설정합니다. 메시지 수신을 위한 준비단계라고 생각하시면 됩니다.

  6. handler = new StompSessionHandlerAdapter() { ... }: WebSocket 연결에서 발생하는 이벤트를 처리하기 위한 StompSessionHandlerAdapter 객체를 생성합니다. getPayloadType으로 수신 받을 메시지를 지정하며 handleFrame으로 메시지 수신시 처리할 과정을 적으시면 됩니다.

test

	@DisplayName("message send")
	@Test
	void success_message_send() throws ExecutionException, InterruptedException, TimeoutException {
		// given
		when(jwtUtils.isValidToken(any())).thenReturn(true);
		when(jwtUtils.parseMemberIdFromToken(any())).thenReturn("userIdFromToken");
		// doNothing().when(handleMessageUseCase).publishMessage(any());

		doAnswer(invocation -> {
			simpMessagingTemplate.convertAndSend(SUBSCRIBE_ENDPOINT, new MessageDto(
				"roomId", "senderId", "content", MessageType.SEND
			));
			return null;
		}).when(handleMessageUseCase).publishMessage(any(MessageDto.class));

		StompHeaders headers = new StompHeaders();
		headers.add("Authorization", "Bearer mytoken");
		StompSession stompSession = stompClient.connectAsync(URL, new WebSocketHttpHeaders(),
												   headers,
												   new StompSessionHandlerAdapter() {
												   })
											   .get(1, SECONDS);
		stompSession.subscribe(SUBSCRIBE_ENDPOINT, handler);

		//when
		stompSession.send(SEND_MESSAGE_ENDPOINT+"/roomId", new MessageDto("secondRoom", "123", "hello", MessageType.SEND));

		
		//then
		assertThat(stompSession.isConnected()).isTrue();
		verify(jwtUtils, times(1)).isValidToken(any());
		verify(jwtUtils, times(1)).parseMemberIdFromToken(any());
		verify(handleMessageUseCase, times(1)).publishMessage(any());
	}

먼저 테스트 코드에대한 이해를 위해 간단하게 mocking한 것에 대해서 설명하겠습니다.

mock

jwt 인증 mock

when(jwtUtils.isValidToken(any())).thenReturn(true);
when(jwtUtils.parseMemberIdFromToken(any())).thenReturn("userIdFromToken");

jwt 인증 과정을 처리하는 메소드입니다.
security 과정을 통과시키게 하기 위해 mocking하였습니다.

handle message mock

	doAnswer(invocation -> {
	simpMessagingTemplate.convertAndSend(SUBSCRIBE_ENDPOINT, new MessageDto(
				"roomId", "senderId", "content", MessageType.SEND
			));
			return null;
	}).when(handleMessageUseCase).publishMessage(any(MessageDto.class));

message가 도착한 경우 실제 로직에서는 kafka, redis, mongodb를 거쳐가며 처리하지만 이러한 과정을 생략하기 위해 when(handleMessageUseCase).publishMessage(any(MessageDto.class));을 통해 mocking 하였습니다.

이때 실제 메시지가 도착한 경우 저는 그냥 간단하게 simpMessagingTemplate.convertAndSend를 통해 현재 subscribe 중인 곳으로 메시지를 직접 날려주었습니다.

stomp connect

StompHeaders headers = new StompHeaders();
headers.add("Authorization", "Bearer mytoken");
StompSession stompSession = stompClient.connectAsync(URL, 
									new WebSocketHttpHeaders(),
									headers,
									new StompSessionHandlerAdapter() {
                                    }).get(1, SECONDS);
                                    
stompSession.subscribe(SUBSCRIBE_ENDPOINT, handler);
  1. stompClient.connectAsync를 통해 stomp 연결을 해줍니다. 이때 StompHeaders를 통해 필요한 헤더 값을 넘겨주었습니다.(여기서의 헤더는 실제 http에서의 헤더와는 다릅니다. stomp에 대한 공식문서를 확인해보시면 좋을 것 같습니다.)
    이 때 반환값이 CompletableFuture이므로 get을 통해 값을 반환받을 때까지 대기 해주었습니다.
  2. stompSession.subscribe subscribe 과정입니다. handle의 겨웅 위에서 setup 과정에서 설정한 클래스로 메시지 수신시 처리 동작을 지정해주었습니다.

Stomp Send

stompSession.send(SEND_MESSAGE_ENDPOINT+"/roomId", new MessageDto("secondRoom", "123", "hello", MessageType.SEND));

실제 메시지 전송 과정입니다. 이를 통해 메시지를 보낼 수 있습니다.

검증 코드

assertThat(stompSession.isConnected()).isTrue();
verify(jwtUtils, times(1)).isValidToken(any());
verify(jwtUtils, times(1)).parseMemberIdFromToken(any());
verify(handleMessageUseCase, times(1)).publishMessage(any());

실제로 stomp가 연결이 되었는지를 확인하였으며 나머지 mocking 한 class들이 동작을 하였는지를 검증하였습니다.

결과

테스트가 성공한 것을 볼 수 있습니다.

개선의 여지가 많은 코드이므로 필요하시다면 추가 구현을 하면 될 것 같습니다.

참고 자료

JUnit을 이용한 웹소켓 테스트 - ksp7331
StompSession BlockingQueue 메시지 전달 안되는 문제 - hyng

profile
greenTea입니다.

0개의 댓글