Stomp도 Junit 단위 테스트로 로직을 테스트하자

Alex·2024년 11월 30일
0

Plaything

목록 보기
40/118

Stomp와 관련해서 인증 로직을 굉장히 복잡하게 작성했다.

실제 서버를 돌려서 작동하는 건 확인했지만...
역시 비즈니스 코드가 복잡해질 수록 느끼게 되는 고민 중 하나가
리팩토링이다...

리팩토링을 하고서 계속 서버를 돌리면서 로직이 정상적인지 확인해야 한다.
단위테스트가 필요하다.

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

    @LocalServerPort
    private int port;

    private WebSocketStompClient stompClient;
    private String url;
    
    @BeforeEach
    void setup() {
        stompClient = new WebSocketStompClient(new StandardWebSocketClient());
        MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
        ObjectMapper objectMapper = messageConverter.getObjectMapper();
        //LocalDateTime을 위한 모듈
        objectMapper.registerModules(new JavaTimeModule(), new ParameterNamesModule());
        stompClient.setMessageConverter(messageConverter);

        url = String.format("ws://localhost:%d/ws-stomp", port);


    }
    
    

  @Test
    void test1() throws ExecutionException, InterruptedException, TimeoutException {

        StompHeaders connectHeaders = new StompHeaders();
        String token = JWTProvider.createToken("Alex");
        connectHeaders.add("Authorization", "Bearer " + token);

        StompSession session = stompClient.connect(
                url,
                new WebSocketHttpHeaders(),
                connectHeaders,
                new StompSessionHandlerAdapter() {
                }
        ).get(2, TimeUnit.SECONDS);

        // 메시지 구독 설정
        CompletableFuture<Message> messageFuture = new CompletableFuture<>();
        session.subscribe("/sub/chat/alex", new StompFrameHandler() {
            @Override
            public Type getPayloadType(StompHeaders headers) {
                return Message.class;
            }

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

        // when
        Message testMessage = new Message("발신자", "alex", "test message");
        session.send("/pub/chat/message/alex", testMessage);

        // then
        Message receivedMessage = messageFuture.get(3, TimeUnit.SECONDS);
        assertThat(receivedMessage.message()).isEqualTo("test message");
        assertThat(receivedMessage.senderNickname()).isEqualTo("발신자");
        assertThat(receivedMessage.receiverNickname()).isEqualTo("alex");

    }
    

여러 레퍼런스와 클로드의 도움을 받아서 작성한 테스트다.
역시 작동하지 않는다 ㅎ...

어디가 문제인지 하나씩 확인해보자.

대기시간이 너무 적었던 거 같다.

시간을 10초로 늘리자.


그럼에도
유저가 없다는 걸 보니 트랜잭션 문제로 보인다.

변경된 코드

@Test
    void test1() throws ExecutionException, InterruptedException, TimeoutException {

        StompHeaders connectHeaders = new StompHeaders();
        String token = JWTProvider.createToken("dusgh1234");
        connectHeaders.add("Authorization", "Bearer " + token);

        StompSession receiverSession = stompClient.connect(
                url,
                new WebSocketHttpHeaders(),
                connectHeaders,
                new StompSessionHandlerAdapter() {
                }
        ).get(10, TimeUnit.SECONDS);

        // 메시지 구독 설정
        CompletableFuture<Message> messageFuture = new CompletableFuture<>();
        StompHeaders subscribeHeaders = new StompHeaders();
        subscribeHeaders.setDestination("/user/alex/chat");
        subscribeHeaders.add("Authorization", "Bearer " + token);

        receiverSession.subscribe(subscribeHeaders, new StompFrameHandler() {
            @Override
            public Type getPayloadType(StompHeaders headers) {
                return Message.class;
            }

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


        StompHeaders connectHeaders2 = new StompHeaders();
        String token2 = JWTProvider.createToken("dusgh12345");
        connectHeaders2.add("Authorization", "Bearer " + token2);
        // 두 번째 세션 (수신자)
        StompSession senderSession = stompClient.connect(
                url,
                new WebSocketHttpHeaders(),
                connectHeaders2,
                new StompSessionHandlerAdapter() {
                }
        ).get(15, TimeUnit.SECONDS);

        // when
        Message testMessage = new Message("발신자", "alex", "test message");
        StompHeaders sendHeaders = new StompHeaders();
        sendHeaders.setDestination("/pub/chat/message/alex");
        sendHeaders.add("Authorization", "Bearer " + token2);

        senderSession.send(sendHeaders, testMessage);

        // then
        Message receivedMessage = messageFuture.get(10, TimeUnit.SECONDS);
        assertThat(receivedMessage.message()).isEqualTo("test message");
        assertThat(receivedMessage.senderNickname()).isEqualTo("발신자");
        assertThat(receivedMessage.receiverNickname()).isEqualTo("alex");

    }
    

현재 Stomp에서 구독, 메시지 전송, 접속 모두 jwt 토큰을 활용해서 검증을 하고 있어서 이렇게 헤더에 jwt 토큰을 모두 넣어주어야 했다.

그리고, 세션을 두개로 나눠서 sender와 receiver 세션을 만들어놓고 메시지를 전송->수신하도록 만들었다.

트랜잭션을 확인해보자

logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.web.socket=DEBUG
logging.level.org.springframework.messaging=TRACE

로그를 살펴보자

테스트와 웹소켓이 서로 다른 스레드에서 작업하고 있다.
스레드마다 트랜잭션, 영속성컨텍스트가 다르다.
그렇기에

setUp 단계에서 넣어준 데이터를 커밋하고 트랜잭션을 끝내주어야 한다.

예외는 왜 안 잡히지?

 @DisplayName("자기 채널에만 구독을 할 수 있다.")
    @Test
    void test2() throws ExecutionException, InterruptedException, TimeoutException {

        StompHeaders connectHeaders = new StompHeaders();
        String token = JWTProvider.createToken("dusgh1234");
        connectHeaders.add("Authorization", "Bearer " + token);

        StompSession receiverSession = stompClient.connect(
                url,
                new WebSocketHttpHeaders(),
                connectHeaders,
                new StompSessionHandlerAdapter() {
                }
        ).get(10, TimeUnit.SECONDS);

        StompHeaders subscribeHeaders = new StompHeaders();
        subscribeHeaders.setDestination("/user/alex1/chat");
        subscribeHeaders.add("Authorization", "Bearer " + token);

        try {
            receiverSession.subscribe(subscribeHeaders, new StompFrameHandler() {
                @Override
                public Type getPayloadType(StompHeaders headers) {
                    return Message.class;
                }

                @Override
                public void handleFrame(StompHeaders headers, Object payload) {
                    // 메시지 처리 로직
                }
            });
            fail("테스트 실패");

        } catch (MessageDeliveryException e) {
            assertThat(e).isInstanceOf(MessageDeliveryException.class)
                    .hasCauseInstanceOf(CustomException.class)
                    .hasMessage("채널 구독 권한이 없습니다");
        }
        cleanUpData();
    }
    

예외는 발생했는데 catch로 잡히지 않는다.
일부로 발생시킨 fail만 동작한다.
junit을 활용해서 assertThrownBy를 써도 안잡힌다.

찾아보니 비동기에서 발생하는 예외는 호출한쪽으로 전파되지 않는다고 한다.
할수있는 방법들을 다 동원해도 안 된다...

설계를 변경하자

지금 StompHandler에서 분기를 나누고 검증 로직을 다 처리하니
검증 로직만을 테스트할 수가 없다.

분기에 따라 검증이 작동하는 걸 보는 것도 좋긴하지만
트랜잭션과 비동기 문제로 예외를 검증하기가 어렵다.

검증 로직만 따로 캡슐화를 해서 로직 테스트를 돌리기로 했다.

이렇게 validator를 만들어서 여기서 검증 로직을 담당한다.

그럼 이렇게 검증 로직만 떼어내서 테스트가 가능하다.
검증 로직만 떼어내니 속도가 훨씬 빠르고
Stomp 환경에 영향을 받지 않는다는 점이 좋다.

그전에는 예외터지고, 그 예외가 전파되길 기다리면 계쏙 소켓 타임아웃 예외가 발생해서 테스트가 불가능했따.

지금 와서 보니 변경된 설계가 더 좋아보인다
각각의 파트를 독립적으로 유지보수하고 테스트할수있기 때문이다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글