Testy WebSocketów - nie działający subscriber

0

Próbuje przetestować działanie WebSocketów w Springu i nie bardzo rozumiem czemu te testy nie działają. Dodam tylko, że apikacja w połączeniu z frontendem działa jak powinna, problem pojawia się jedynie przy testach. Może ktoś z Was miał z tym styczność i jest w stanie pomóc?

Cała aplikacja (a raczej MVP) wygląda tak

@Configuration
@EnableWebSocketMessageBroker
internal class WsConfig : WebSocketMessageBrokerConfigurer {

	override fun configureMessageBroker(config: MessageBrokerRegistry) {
		config.enableSimpleBroker("/queue")
		config.setApplicationDestinationPrefixes("/app")
	}

	override fun registerStompEndpoints(registry: StompEndpointRegistry) {
		registry.addEndpoint("/ws").withSockJS()
	}
}

@Controller
class WsController(private val messagingTemplate: SimpMessagingTemplate) {

	@MessageMapping("/chat")
	fun sendChatMessage(@Payload message: String) = messagingTemplate.convertAndSend("/queue/chat", message)
}

No a wspomniane testy tak

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WsTest(@LocalServerPort private val port: Int) {

    private val wsClient = createWebSocketClient()
    private val lock = CountDownLatch(1)

    @Test
    fun test1() {
        val subscriber = connect()
        val publisher = connect()
        val capturedMessages = mutableListOf<String>()

        subscriber.subscribe("/queue/chat", frameHandler(capturedMessages))
        publisher.send("/app/chat", "lorem ipsum")

        lock.await(5, TimeUnit.SECONDS)
        assertEquals(1, capturedMessages.size)
    }


    private fun createWebSocketClient(): WebSocketStompClient {
        val webSocketClient = WebSocketStompClient(SockJsClient(listOf(WebSocketTransport(StandardWebSocketClient()))))
        webSocketClient.messageConverter = MappingJackson2MessageConverter()
        return webSocketClient
    }

    private fun connect() = wsClient.connect("ws://localhost:$port/ws", object : StompSessionHandlerAdapter() {}).get(2, TimeUnit.SECONDS)

    private fun frameHandler(messages: MutableList<String>) =
            object : StompFrameHandler {
                override fun getPayloadType(headers: StompHeaders): Type {
                    return String::class.java
                }

                override fun handleFrame(headers: StompHeaders, payload: Any?) {
                    messages.add(payload as String)
                }
            }
}

Problem w tym, że handleFrames() nigdy nie jest odpalane, a przedebugowałem to dokładnie - subscriber jest podłączony pod właściwy destination, na który przez publishera wysyłane są wiadomości.

0

Jakby ktoś miał ten sam problem, udało mi się znaleźć obejście
Wysyłałem Stringa, nie dociekam już dlaczego ale MappingJackson2MessageConverter() powodował że ten String się gdzieś gubił i nie trafiał do subscribera. Powyższy kod, z jedną małą zmianą, mianowicie - webSocketClient.messageConverter = StringMessageConverter() działa bez problemu, test jest w końcu na zielono (może warto wspomnieć, wcześniej nie było żadnego błędu, po prostu leciał błąd asercji bo lista była pusta)

No ale, to było szybkie MVP, w prawdziwej aplikacji nie był to String tylko data class. No i tutaj StringMessageConverter() nie działał, z tą różnicą że leciał po prostu błąd mapowania i test był na czerwono a nie na żółto. Wspomnę tylko, że kombinacja data class + MappingJackson2MessageConverter() oznaczała to co na początku, czyli błąd asercji, ale bez żadnego błędu.

Jako ciekawostka - na tym etapie sprawdziłem jak zachowa POJO zamiast data class, no i tak jak myślałem, pomogło. Mało tego, po stronie faktycznego kodu w controllerze nadal mogłem mieć data class, a wystarczyło że po stronie testów wysyłałem POJO który w środku miał identyczną strukturę, tzn.

public class ChatMessagePOJO {
    private String payload;

    public String getPayload() {
        return payload;
    }

    public void setPayload(String payload) {
        this.payload = payload;
    }
}

oraz

data class ChatMessage(val payload: String)

No ale koniec końców, doszedłem do jeszcze jednego rozwiązania, przy którym już chyba zostanę, mianowicie - POJO oczywiście wylatuje, zostaje MappingJackson2MessageConverter(), ale frameHandler() działa po prostu na Any zamiast na konkretnym typie

private fun frameHandler(messages: MutableList<Any>) =
            object : StompFrameHandler {
                override fun getPayloadType(headers: StompHeaders): Type {
                    return Any::class.java
                }

                override fun handleFrame(headers: StompHeaders, payload: Any?) {
                    if (payload != null) {
                        messages.add(payload)
                    }
                }
            }

Jest to trochę mniej wygodne przy sprawdzaniu odpowiedzi, bo dostajemy byte[] i trzeba to konwertować, ale działa.

        val objectMapper = jacksonObjectMapper() //jackson-module-kotlin
        val msg = objectMapper.readValue(capturedMessages[0] as ByteArray, ChatMessage::class.java)
        assertEquals("lorem ipsum", msg.payload)

1 użytkowników online, w tym zalogowanych: 0, gości: 1